Thứ năm, 12/12/2019 | 00:00 GMT+7

Cách tạo ứng dụng theo dõi sức khỏe với React, GraphQL và Okta

Trong hướng dẫn này, bạn sẽ xây dựng một ứng dụng theo dõi sức khỏe bằng cách sử dụng API GraphQL với khung công tác Vesper, TypeORM và MySQL làm database . Đây là các khung công tác Node và bạn sẽ sử dụng TypeScript cho ngôn ngữ này. Đối với ứng dụng client , bạn sẽ sử dụng React, reactstrap và Apollo Client để nói chuyện với API. Khi bạn có môi trường này hoạt động, bạn sẽ thêm xác thực user an toàn với Okta. Okta là một dịch vụ cloud cho phép các nhà phát triển tạo, chỉnh sửa và lưu trữ an toàn account user và dữ liệu account user , đồng thời kết nối chúng với một hoặc nhiều ứng dụng.

Trước khi sử dụng hướng dẫn này, hãy đăng ký account Okta dành cho nhà phát triển miễn phí .

Bước 1 - Xây dựng API với TypeORM, GraphQL và Vesper

TypeORM là một khung ORM (đối tượng-quan hệ ánh xạ ) có thể chạy trong hầu hết các nền tảng JavaScript, bao gồm Node, một trình duyệt, Cordova, React Native và Electron. Nó bị ảnh hưởng nhiều bởi Hibernate, Doctrine và Entity Framework.

Cài đặt TypeORM trên phạm vi global để bắt đầu tạo API của bạn:

  • npm i -g typeorm@0.2.7

Tạo một folder để chứa ứng dụng client React và API GraphQL:

  • mkdir health-tracker
  • cd health-tracker

Tạo một dự án mới với MySQL bằng lệnh sau:

  • typeorm init --name graphql-api --database mysql

Chỉnh sửa graphql-api/ormconfig.json để tùy chỉnh tên user , password và database .

graphql-api / ormconfig.json
{     ...     "username": "health",     "password": "pointstest",     "database": "healthpoints",     ... } 

Lưu ý: Để xem các truy vấn đang được thực thi đối với MySQL, hãy thay đổi giá trị logging trong file này thành all . Nhiều tùy chọn ghi log khác cũng có sẵn.

Cài đặt MySQL

Cài đặt MySQL nếu bạn chưa cài đặt nó. Trên Ubuntu, bạn có thể sử dụng sudo apt-get install mysql-server . Trên macOS, bạn có thể sử dụng Homebrew và brew install mysql . Đối với Windows, bạn có thể sử dụng MySQL Installer .

Một khi bạn đã có MySQL cài đặt và cấu hình với một password chủ, đăng nhập và tạo ra một healthpoints database .

  • mysql -u root -p
  • create database healthpoints;
  • use healthpoints;
  • grant all privileges on *.* to 'health'@'localhost' identified by 'points';

Điều hướng đến dự án graphql-api của bạn trong cửa sổ terminal , cài đặt các phần phụ thuộc của dự án, sau đó khởi động nó đảm bảo bạn có thể kết nối với MySQL.

  • cd graphql-api
  • npm i
  • npm start

Bạn sẽ thấy kết quả sau:

Inserting a new user into the database... Saved a new user with id: 1 Loading users from the database... Loaded users:  [ User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 } ] Here you can setup and run express/koa/any other framework. 

Cài đặt Vesper để tích hợp TypeORM và GraphQL

Vesper là một khung công tác Node tích hợp TypeORM và GraphQL. Để cài đặt nó, hãy sử dụng npm:

  • npm i vesper@0.1.9

Bây giờ đã đến lúc tạo một số mô hình GraphQL (xác định dữ liệu trông như thế nào) và một số bộ điều khiển (giải thích cách tương tác với dữ liệu ).

Tạo graphql-api/src/schema/model/Points.graphql :

graphql-api / src / schema / model / Points.graphql
type Points {   id: Int   date: Date   exercise: Int   diet: Int   alcohol: Int   notes: String   user: User } 

Tạo graphql-api/src/schema/model/User.graphql :

graphql-api / src / schema / model / User.graphql
type User {   id: String   firstName: String   lastName: String   points: [Points] } 

Tiếp theo, tạo một graphql-api/src/schema/controller/PointsController.graphql với các truy vấn và đột biến:

graphql-api / src / schema / controller / PointsController.graphql
type Query {   points: [Points]   pointsGet(id: Int): Points   users: [User] }  type Mutation {   pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points   pointsDelete(id: Int): Boolean } 

Bây giờ dữ liệu đã có metadata GraphQL, hãy tạo các thực thể sẽ được quản lý bởi TypeORM. Thay đổi src/entity/User.ts để có mã sau cho phép các điểm được liên kết với user .

src / entity / User.ts
import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; import { Points } from './Points';  @Entity() export class User {    @PrimaryColumn()   id: string;    @Column()   firstName: string;    @Column()   lastName: string;    @OneToMany(() => Points, points => points.user)   points: Points[]; } 

Trong cùng một folder src/entity , hãy tạo một lớp Points.ts với đoạn mã sau.

Điểm.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; import { User } from './User';  @Entity() export class Points {    @PrimaryGeneratedColumn()   id: number;    @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})   date: Date;    @Column()   exercise: number;    @Column()   diet: number;    @Column()   alcohol: number;    @Column()   notes: string;    @ManyToOne(() => User, user => user.points, { cascade: ["insert"] })   user: User|null; } 

Lưu ý tùy chọn cascade: ["insert"] trên chú thích @ManyToOne . Tùy chọn này sẽ tự động chèn user nếu nó có trên thực thể. Tạo src/controller/PointsController.ts để xử lý việc chuyển đổi dữ liệu từ các truy vấn và đột biến GraphQL của bạn.

src / controller / PointsController.ts
import { Controller, Mutation, Query } from 'vesper'; import { EntityManager } from 'typeorm'; import { Points } from '../entity/Points';  @Controller() export class PointsController {    constructor(private entityManager: EntityManager) {   }    // serves "points: [Points]" requests   @Query()   points() {     return this.entityManager.find(Points);   }    // serves "pointsGet(id: Int): Points" requests   @Query()   pointsGet({id}) {     return this.entityManager.findOne(Points, id);   }    // serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests   @Mutation()   pointsSave(args) {     const points = this.entityManager.create(Points, args);     return this.entityManager.save(Points, points);   }    // serves "pointsDelete(id: Int): Boolean" requests   @Mutation()   async pointsDelete({id}) {     await this.entityManager.remove(Points, {id: id});     return true;   } } 

Thay đổi src/index.ts để sử dụng Vesper's bootstrap() để cấu hình mọi thứ.

src / index.ts
import { bootstrap } from 'vesper'; import { PointsController } from './controller/PointsController'; import { Points } from './entity/Points'; import { User } from './entity/User';  bootstrap({   port: 4000,   controllers: [     PointsController   ],   entities: [     Points,     User   ],   schemas: [     __dirname + '/schema/**/*.graphql'   ],   cors: true }).then(() => {   console.log('Your app is up and running on http://localhost:4000. ' +     'You can use playground in development mode on http://localhost:4000/playground'); }).catch(error => {   console.error(error.stack ? error.stack : error); }); 

Mã này yêu cầu Vesper đăng ký bộ điều khiển, thực thể và schemas GraphQL để chạy trên cổng 4000 và cho phép CORS (chia sẻ tài nguyên nguồn root chéo).

Khởi động API của bạn bằng cách sử dụng npm start và chuyển đến http://localhost:4000/playground . Trong ngăn bên trái, nhập đột biến sau và nhấn nút phát. Hãy thử nhập mã sau để bạn có thể trải nghiệm hoàn thành mã mà GraphQL cung cấp cho bạn.

mutation {   pointsSave(exercise:1, diet:1, alcohol:1, notes:"Hello World") {     id     date     exercise     diet     alcohol     notes   } } 

Kết quả của bạn sẽ giống như thế này.

Sân chơi GraphQL

Bạn có thể nhấp vào tab SCHEMA ở bên phải để xem các truy vấn và đột biến có sẵn.

Sử dụng truy vấn points sau để xác minh dữ liệu có trong database của bạn:

query {   points {id date exercise diet notes} } 

Sửa ngày

Bạn có thể nhận thấy rằng ngày được trả về từ pointsSave và truy vấn points có định dạng mà ứng dụng JavaScript có thể khó hiểu. Bạn có thể khắc phục điều đó bằng cách cài đặt graphql-iso-date .

  • npm i graphql-iso-date@3.5.0

Sau đó, thêm nhập trong src/index.ts và cấu hình trình phân giải tùy chỉnh cho các loại ngày khác nhau. Ví dụ này chỉ sử dụng Date , nhưng sẽ hữu ích nếu biết các tùy chọn khác.

src / index.ts
import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';  bootstrap({   ...   // https://github.com/vesper-framework/vesper/issues/4   customResolvers: {     Date: GraphQLDate,     Time: GraphQLTime,     DateTime: GraphQLDateTime   },   ... }); 

Bây giờ chạy truy vấn points sẽ trả về kết quả thân thiện hơn với khách hàng.

{   "data": {     "points": [       {         "id": 1,         "date": "2018-06-04",         "exercise": 1,         "diet": 1,         "notes": "Hello World"       }     ]   } } 

Đến đây bạn đã viết một API với GraphQL và TypeScript. Trong các phần tiếp theo, bạn sẽ tạo một ứng dụng client React cho API này và thêm xác thực với OIDC. Thêm xác thực sẽ cung cấp cho bạn khả năng lấy thông tin của user và liên kết user với điểm của họ.

Bước 2 - Bắt đầu với React

Một trong những cách nhanh nhất để bắt đầu với React là sử dụng Create React App .

Cài đặt bản phát hành mới nhất bằng lệnh này:

  • npm i -g create-react-app@1.1.4

Điều hướng đến folder mà bạn đã tạo API GraphQL của bạn và tạo ứng dụng client React:

  • cd health-tracker
  • create-react-app react-client

Tiếp theo, cài đặt các phụ thuộc mà bạn cần nói chuyện để tích hợp Apollo Client với React, cũng như Bootstrap và reactstrap .

  • npm i apollo-boost@0.1.7 react-apollo@2.1.4 graphql-tag@2.9.2 graphql@0.13.2

Cấu hình Apollo Client cho API của bạn

Mở react-client/src/App.js , nhập ApolloClient từ apollo-boost và thêm điểm cuối vào API GraphQL của bạn:

react-client / src / App.js
import ApolloClient from 'apollo-boost';  const client = new ApolloClient({   uri: "http://localhost:4000/graphql" }); 

Với ba dòng mã, ứng dụng của bạn đã sẵn sàng để bắt đầu tìm nạp dữ liệu. Bạn có thể kiểm tra nó bằng lệnh các gql chức năng từ graphql-tag . Thao tác này sẽ phân tích cú pháp chuỗi truy vấn của bạn và biến nó thành tài liệu truy vấn:

import gql from 'graphql-tag';  class App extends Component {    componentDidMount() {     client.query({       query: gql`         {           points {             id date exercise diet alcohol notes           }         }       `     })     .then(result => console.log(result));   } ... } 

Đảm bảo mở các công cụ dành cho nhà phát triển của trình duyệt để bạn có thể xem dữ liệu sau khi thực hiện thay đổi này. Bạn có thể sửa đổi console.log() để sử dụng this.setState({points: results.data.points}) , nhưng sau đó bạn phải khởi tạo trạng thái mặc định trong hàm tạo. Nhưng có một cách hiệu quả hơn: bạn có thể sử dụng các thành phần ApolloProviderQuery từ ApolloProvider react-apollo .

Sau đây là version sửa đổi của react-client/src/App.js sử dụng các thành phần này.

import React, { Component } from 'react'; import logo from './logo.svg'; import './App.css'; import ApolloClient from 'apollo-boost'; import gql from 'graphql-tag'; import { ApolloProvider, Query } from 'react-apollo'; const client = new ApolloClient({   uri: "http://localhost:4000/graphql" });  class App extends Component {    render() {     return (       <ApolloProvider client={client}>         <div className="App">           <header className="App-header">             <img src={logo} className="App-logo" alt="logo" />             <h1 className="App-title">Welcome to React</h1>           </header>           <p className="App-intro">             To get started, edit <code>src/App.js</code> and save to reload.           </p>           <Query query={gql`             {               points {id date exercise diet alcohol notes}             }           `}>             {({loading, error, data}) => {               if (loading) return <p>Loading...</p>;               if (error) return <p>Error: {error}</p>;               return data.points.map(p => {                 return <div key={p.id}>                   <p>Date: {p.date}</p>                   <p>Points: {p.exercise + p.diet + p.alcohol}</p>                   <p>Notes: {p.notes}</p>                 </div>               })             }}           </Query>         </div>       </ApolloProvider>     );   } }  export default App; 

Đến đây bạn đã xây dựng một API GraphQL và một giao diện user React nói chuyện với nó. Tuy nhiên, vẫn còn nhiều việc phải làm. Trong các phần tiếp theo, bạn sẽ thêm xác thực vào React, xác minh JWT với Vesper và thêm chức năng CRUD vào giao diện user . Chức năng CRUD đã tồn tại trong API nhờ vào các đột biến bạn đã viết trước đó.

Bước 3 - Thêm xác thực cho React với OpenID Connect

Bạn cần cấu hình React để sử dụng Okta cho việc xác thực. Bạn cần tạo một ứng dụng OIDC ở Okta cho việc đó.

Đăng nhập vào account Nhà phát triển Okta của bạn (hoặc đăng ký nếu bạn chưa có account ) và chuyển đến Ứng dụng > Thêm ứng dụng . Nhấp vào Ứng dụng một trang , nhấp vào Tiếp theo và đặt tên cho ứng dụng mà bạn sẽ nhớ. Thay đổi tất cả các version của localhost:8080 thành localhost:3000 và nhấp vào Xong .

Cài đặt của bạn sẽ tương tự như ảnh chụp màn hình sau:

Cài đặt ứng dụng OIDC

SDK React của Okta cho phép bạn tích hợp OIDC vào một ứng dụng React. Để cài đặt, hãy chạy các lệnh sau:

  • npm i @okta/okta-react@1.0.2 react-router-dom@4.2.2

SDK React của Okta phụ thuộc vào react -router , do đó bạn cần cài đặt react-router-dom . Cấu hình định tuyến trong client/src/App.tsx là một thực tế phổ biến, vì vậy hãy thay thế mã của nó bằng JavaScript sau để cài đặt xác thực với Okta.

client / src / App.tsx
import React, { Component } from 'react'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import { ImplicitCallback, SecureRoute, Security } from '@okta/okta-react'; import Home from './Home'; import Login from './Login'; import Points from './Points';  function onAuthRequired({history}) {   history.push('/login'); }  class App extends Component {   render() {     return (       <Router>         <Security issuer='https://{yourOktaDomain}.com/oauth2/default'                   client_id='{yourClientId}'                   redirect_uri={window.location.origin + '/implicit/callback'}                   onAuthRequired={onAuthRequired}>           <Route path='/' exact={true} component={Home}/>           <SecureRoute path='/points' component={Points}/>           <Route path='/login' render={() => <Login baseUrl='https://{yourOktaDomain}.com'/>}/>           <Route path='/implicit/callback' component={ImplicitCallback}/>         </Security>       </Router>     );   } }  export default App; 

Đảm bảo thay thế {yourOktaDomain}{yourClientId} trong mã trước đó. Miền Okta của bạn phải giống như dev-12345.oktapreview . Đảm bảo rằng bạn không kết thúc bằng hai giá trị .com trong URL.

Mã trong App.js tham chiếu đến hai thành phần chưa tồn tại: Home , LoginPoints . Tạo src/Home.js với mã sau. Thành phần này hiển thị tuyến đường mặc định, cung cấp nút Đăng nhập và liên kết đến các điểm của bạn và đăng xuất sau khi bạn đã đăng nhập:

src / Home.js
import React, { Component } from 'react'; import { withAuth } from '@okta/okta-react'; import { Button, Container } from 'reactstrap'; import AppNavbar from './AppNavbar'; import { Link } from 'react-router-dom';  export default withAuth(class Home extends Component {   constructor(props) {     super(props);     this.state = {authenticated: null, userinfo: null, isOpen: false};     this.checkAuthentication = this.checkAuthentication.bind(this);     this.checkAuthentication();     this.login = this.login.bind(this);     this.logout = this.logout.bind(this);   }    async checkAuthentication() {     const authenticated = await this.props.auth.isAuthenticated();     if (authenticated !== this.state.authenticated) {       if (authenticated && !this.state.userinfo) {         const userinfo = await this.props.auth.getUser();         this.setState({authenticated, userinfo});       } else {         this.setState({authenticated});       }     }   }    async componentDidMount() {     this.checkAuthentication();   }    async componentDidUpdate() {     this.checkAuthentication();   }    async login() {     this.props.auth.login('/');   }    async logout() {     this.props.auth.logout('/');     this.setState({authenticated: null, userinfo: null});   }    render() {     if (this.state.authenticated === null) return null;     const button = this.state.authenticated ?         <div>           <Button color="link"><Link to="/points">Manage Points</Link></Button><br/>           <Button color="link" onClick={this.logout}>Logout</Button>         </div>:       <Button color="primary" onClick={this.login}>Login</Button>;      const message = this.state.userinfo ?       <p>Hello, {this.state.userinfo.given_name}!</p> :       <p>Please log in to manage your points.</p>;      return (       <div>         <AppNavbar/>         <Container fluid>           {message}           {button}         </Container>       </div>     );   } }); 

Thành phần này sử dụng <Container/><Button/> từ reactstrap. Cài đặt reactstrap để mọi thứ được biên dịch. Nó phụ thuộc vào Bootstrap, vì vậy hãy bao gồm cả điều đó.

  • npm i reactstrap@6.1.0 bootstrap@4.1.1

Thêm file CSS của Bootstrap dưới dạng nhập trong src/index.js .

src / index.js
import 'bootstrap/dist/css/bootstrap.min.css'; 

Bạn có thể nhận thấy có một <AppNavbar/> trong phương thức render() của thành phần Home . Tạo src/AppNavbar.js để bạn có thể sử dụng tiêu đề chung giữa các thành phần.

import React, { Component } from 'react'; import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; import { Link } from 'react-router-dom';  export default class AppNavbar extends Component {   constructor(props) {     super(props);     this.state = {isOpen: false};     this.toggle = this.toggle.bind(this);   }    toggle() {     this.setState({       isOpen: !this.state.isOpen     });   }    render() {     return <Navbar color="success" dark expand="md">       <NavbarBrand tag={Link} to="/">Home</NavbarBrand>       <NavbarToggler onClick={this.toggle}/>       <Collapse isOpen={this.state.isOpen} navbar>         <Nav className="ml-auto" navbar>           <NavItem>             <NavLink               href="https://twitter.com/oktadev">@oktadev</NavLink>           </NavItem>           <NavItem>             <NavLink href="https://github.com/oktadeveloper/okta-react-graphql-example/">GitHub</NavLink>           </NavItem>         </Nav>       </Collapse>     </Navbar>;   } } 

Trong ví dụ này, bạn sẽ nhúng Tiện ích đăng nhập của Okta . Một tùy chọn khác là chuyển hướng đến Okta và sử dụng trang đăng nhập được lưu trữ.

Cài đặt Tiện ích đăng nhập bằng npm:

  • npm i @okta/okta-signin-widget@2.9.0

Tạo src/Login.js và thêm mã sau vào đó.

src / Login.js
import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import OktaSignInWidget from './OktaSignInWidget'; import { withAuth } from '@okta/okta-react';  export default withAuth(class Login extends Component {   constructor(props) {     super(props);     this.onSuccess = this.onSuccess.bind(this);     this.onError = this.onError.bind(this);     this.state = {       authenticated: null     };     this.checkAuthentication();   }    async checkAuthentication() {     const authenticated = await this.props.auth.isAuthenticated();     if (authenticated !== this.state.authenticated) {       this.setState({authenticated});     }   }    componentDidUpdate() {     this.checkAuthentication();   }    onSuccess(res) {     return this.props.auth.redirect({       sessionToken: res.session.token     });   }    onError(err) {     console.log('error logging in', err);   }    render() {     if (this.state.authenticated === null) return null;     return this.state.authenticated ?       <Redirect to={{pathname: '/'}}/> :       <OktaSignInWidget         baseUrl={this.props.baseUrl}         onSuccess={this.onSuccess}         onError={this.onError}/>;   } }); 

Thành phần Login có tham chiếu đến OktaSignInWidget . Tạo src/OktaSignInWidget.js :

src / OktaSignInWidget.js
import React, {Component} from 'react'; import ReactDOM from 'react-dom'; import OktaSignIn from '@okta/okta-signin-widget'; import '@okta/okta-signin-widget/dist/css/okta-sign-in.min.css'; import '@okta/okta-signin-widget/dist/css/okta-theme.css'; import './App.css';  export default class OktaSignInWidget extends Component {   componentDidMount() {     const el = ReactDOM.findDOMNode(this);     this.widget = new OktaSignIn({       baseUrl: this.props.baseUrl     });     this.widget.renderEl({el}, this.props.onSuccess, this.props.onError);   }    componentWillUnmount() {     this.widget.remove();   }    render() {     return <div/>;   } }; 

Tạo src/Points.js để hiển thị danh sách các điểm từ API của bạn:

import React, { Component } from 'react'; import { ApolloClient } from 'apollo-client'; import { createHttpLink } from 'apollo-link-http'; import { setContext } from 'apollo-link-context'; import { InMemoryCache } from 'apollo-cache-inmemory'; import gql from 'graphql-tag'; import { withAuth } from '@okta/okta-react'; import AppNavbar from './AppNavbar'; import { Alert, Button, Container, Table } from 'reactstrap'; import PointsModal from './PointsModal';  export const httpLink = createHttpLink({   uri: 'http://localhost:4000/graphql' });  export default withAuth(class Points extends Component {   client;    constructor(props) {     super(props);     this.state = {points: [], error: null};      this.refresh = this.refresh.bind(this);     this.remove = this.remove.bind(this);   }    refresh(item) {     let existing = this.state.points.filter(p => p.id === item.id);     let points = [...this.state.points];     if (existing.length === 0) {       points.push(item);       this.setState({points});     } else {       this.state.points.forEach((p, idx) => {         if (p.id === item.id) {           points[idx] = item;           this.setState({points});         }       })     }   }    remove(item, index) {     const deletePoints = gql`mutation pointsDelete($id: Int) { pointsDelete(id: $id) }`;      this.client.mutate({       mutation: deletePoints,       variables: {id: item.id}     }).then(result => {       if (result.data.pointsDelete) {         let updatedPoints = [...this.state.points].filter(i => i.id !== item.id);         this.setState({points: updatedPoints});       }     });   }    componentDidMount() {     const authLink = setContext(async (_, {headers}) => {       const token = await this.props.auth.getAccessToken();       const user = await this.props.auth.getUser();        // return the headers to the context so httpLink can read them       return {         headers: {           ...headers,           authorization: token ? `Bearer ${token}` : '',           'x-forwarded-user': user ? JSON.stringify(user) : ''         }       }     });      this.client = new ApolloClient({       link: authLink.concat(httpLink),       cache: new InMemoryCache(),       connectToDevTools: true     });      this.client.query({       query: gql`         {           points {               id,               user {                   id,                   lastName               }               date,               alcohol,               exercise,               diet,               notes           }         }`     }).then(result => {       this.setState({points: result.data.points});     }).catch(error => {       this.setState({error: <Alert color="danger">Failure to communicate with API.</Alert>});     });   }    render() {     const {points, error} = this.state;     const pointsList = points.map(p => {       const total = p.exercise + p.diet + p.alcohol;       return <tr key={p.id}>         <td style={{whiteSpace: 'nowrap'}}><PointsModal item={p} callback={this.refresh}/></td>         <td className={total <= 1 ? 'text-danger' : 'text-success'}>{total}</td>         <td>{p.notes}</td>         <td><Button size="sm" color="danger" onClick={() => this.remove(p)}>Delete</Button></td>       </tr>     });      return (       <div>         <AppNavbar/>         <Container fluid>           {error}           <h3>Your Points</h3>           <Table>             <thead>             <tr>               <th width="10%">Date</th>               <th width="10%">Points</th>               <th>Notes</th>               <th width="10%">Actions</th>             </tr>             </thead>             <tbody>             {pointsList}             </tbody>           </Table>           <PointsModal callback={this.refresh}/>         </Container>       </div>     );   } }) 

Đoạn mã này bắt đầu bằng phương thức refresh()remove() . Phần quan trọng xảy ra trong componentDidMount() , nơi mã thông báo truy cập được thêm vào tiêu đề Authorization và thông tin của user được x-forwarded-user tiêu đề x-forwarded-user . ApolloClient được tạo với thông tin này, bộ nhớ cache được thêm vào và cờ connectToDevTools được bật. Điều này có thể hữu ích cho việc gỡ lỗi bằng Công cụ dành cho nhà phát triển ứng dụng client Apollo .

componentDidMount() {   const authLink = setContext(async (_, {headers}) => {     const token = await this.props.auth.getAccessToken();      // return the headers to the context so httpLink can read them     return {       headers: {         ...headers,         authorization: token ? `Bearer ${token}` : '',         'x-forwarded-user': user ? JSON.stringify(user) : ''       }     }   });    this.client = new ApolloClient({     link: authLink.concat(httpLink),     cache: new InMemoryCache(),     connectToDevTools: true   });    // this.client.query(...); } 

Xác thực với Apollo Client yêu cầu một số phụ thuộc mới. Cài đặt những cái này ngay bây giờ:

  • npm apollo-link-context@1.0.8 apollo-link-http@1.5.4

Trong JSX của trang, có một nút xóa gọi phương thức remove() trong Points . Ngoài ra còn có một thành phần <PointsModal/> . Điều này được tham chiếu cho từng mục, cũng như ở dưới cùng. Bạn sẽ nhận thấy cả hai đều tham chiếu đến phương thức refresh() , cập nhật danh sách.

<PointsModal item={p} callback={this.refresh}/> <PointsModal callback={this.refresh}/> 

Thành phần này hiển thị liên kết để chỉnh sửa thành phần hoặc nút Thêm khi không có item nào được đặt.

Tạo src/PointsModal.js và thêm mã sau vào đó.

src / PointsModal.js
import React, { Component } from 'react'; import { Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { withAuth } from '@okta/okta-react'; import { httpLink } from './Points'; import { ApolloClient } from 'apollo-client'; import { setContext } from 'apollo-link-context'; import { InMemoryCache } from 'apollo-cache-inmemory'; import gql from 'graphql-tag'; import { Link } from 'react-router-dom';  export default withAuth(class PointsModal extends Component {   client;   emptyItem = {     date: (new Date()).toISOString().split('T')[0],     exercise: 1,     diet: 1,     alcohol: 1,     notes: ''   };    constructor(props) {     super(props);     this.state = {       modal: false,       item: this.emptyItem     };      this.toggle = this.toggle.bind(this);     this.handleChange = this.handleChange.bind(this);     this.handleSubmit = this.handleSubmit.bind(this);   }    componentDidMount() {     if (this.props.item) {       this.setState({item: this.props.item})     }      const authLink = setContext(async (_, {headers}) => {       const token = await this.props.auth.getAccessToken();       const user = await this.props.auth.getUser();        // return the headers to the context so httpLink can read them       return {         headers: {           ...headers,           authorization: token ? `Bearer ${token}` : '',           'x-forwarded-user': JSON.stringify(user)         }       }     });      this.client = new ApolloClient({       link: authLink.concat(httpLink),       cache: new InMemoryCache()     });   }    toggle() {     if (this.state.modal && !this.state.item.id) {       this.setState({item: this.emptyItem});     }     this.setState({modal: !this.state.modal});   }    render() {     const {item} = this.state;     const opener = item.id ? <Link onClick={this.toggle} to="#">{this.props.item.date}</Link> :       <Button color="primary" onClick={this.toggle}>Add Points</Button>;      return (       <div>         {opener}         <Modal isOpen={this.state.modal} toggle={this.toggle}>           <ModalHeader toggle={this.toggle}>{(item.id ? 'Edit' : 'Add')} Points</ModalHeader>           <ModalBody>             <Form onSubmit={this.handleSubmit}>               <FormGroup>                 <Label for="date">Date</Label>                 <Input type="date" name="date" id="date" value={item.date}                        onChange={this.handleChange}/>               </FormGroup>               <FormGroup check>                 <Label check>                   <Input type="checkbox" name="exercise" id="exercise" checked={item.exercise}                          onChange={this.handleChange}/>{' '}                   Did you exercise?                 </Label>               </FormGroup>               <FormGroup check>                 <Label check>                   <Input type="checkbox" name="diet" id="diet" checked={item.diet}                          onChange={this.handleChange}/>{' '}                   Did you eat well?                 </Label>               </FormGroup>               <FormGroup check>                 <Label check>                   <Input type="checkbox" name="alcohol" id="alcohol" checked={item.alcohol}                          onChange={this.handleChange}/>{' '}                   Did you drink responsibly?                 </Label>               </FormGroup>               <FormGroup>                 <Label for="notes">Notes</Label>                 <Input type="textarea" name="notes" id="notes" value={item.notes}                        onChange={this.handleChange}/>               </FormGroup>             </Form>           </ModalBody>           <ModalFooter>             <Button color="primary" onClick={this.handleSubmit}>Save</Button>{' '}             <Button color="secondary" onClick={this.toggle}>Cancel</Button>           </ModalFooter>         </Modal>       </div>     )   };    handleChange(event) {     const target = event.target;     const value = target.type === 'checkbox' ? (target.checked ? 1 : 0) : target.value;     const name = target.name;     let item = {...this.state.item};     item[name] = value;     this.setState({item});   }    handleSubmit(event) {     event.preventDefault();     const {item} = this.state;      const updatePoints = gql`       mutation pointsSave($id: Int, $date: Date, $exercise: Int, $diet: Int, $alcohol: Int, $notes: String) {         pointsSave(id: $id, date: $date, exercise: $exercise, diet: $diet, alcohol: $alcohol, notes: $notes) {           id date         }       }`;      this.client.mutate({       mutation: updatePoints,       variables: {         id: item.id,         date: item.date,         exercise: item.exercise,         diet: item.diet,         alcohol: item.alcohol,         notes: item.notes       }     }).then(result => {       let newItem = {...item};       newItem.id = result.data.pointsSave.id;       this.props.callback(newItem);       this.toggle();     });   } }); 

Đảm bảo rằng chương trình backend GraphQL của bạn được khởi động, sau đó khởi động giao diện user React với npm start . Văn bản vuông góc với thanh chuyển trên cùng, vì vậy hãy thêm một số phần đệm bằng cách thêm luật trong src/index.css .

src / index.css
.container-fluid {   padding-top: 10px; } 

Bạn sẽ thấy thành phần Home và một nút để đăng nhập.

Nhấp vào Đăng nhập và bạn sẽ được yêu cầu nhập thông tin đăng nhập Okta của bạn .

Màn hình đăng nhập

Khi bạn nhập thông tin đăng nhập, bạn sẽ đăng nhập.

Màn hình chính với  user  được xác thực

Nhấp vào Quản lý điểm để xem danh sách điểm.

Màn hình điểm của bạn

Giao diện user React của bạn đã được bảo mật, nhưng API của bạn vẫn còn mở. Hãy khắc phục điều đó.

Nhận thông tin user từ JWTs

Điều hướng đến dự án graphql-api của bạn trong cửa sổ terminal và cài đặt Trình xác minh JWT của Okta:

  • npm i @okta/jwt-verifier@0.0.12

Tạo graphql-api/src/CurrentUser.ts để giữ thông tin của user hiện tại.

graphql-api / src / CurrentUser.ts
export class CurrentUser {   constructor(public id: string, public firstName: string, public lastName: string) {} } 

Nhập OktaJwtVerifierCurrentUser vào graphql-api/src/index.ts và cấu hình trình xác minh JWT để sử dụng cài đặt ứng dụng OIDC của bạn.

graphql-api / src / index.ts
import * as OktaJwtVerifier from '@okta/jwt-verifier'; import { CurrentUser } from './CurrentUser';  const oktaJwtVerifier = new OktaJwtVerifier({   clientId: '{yourClientId},   issuer: 'https://{yourOktaDomain}.com/oauth2/default' }); 

Trong cấu hình bootstrap, xác định setupContainer để yêu cầu tiêu đề authorization và đặt user hiện tại từ tiêu đề x-forwarded-user .

bootstrap({   …   cors: true,   setupContainer: async (container, action) => {     const request = action.request;     // require every request to have an authorization header     if (!request.headers.authorization) {       throw Error('Authorization header is required!');     }     let parts = request.headers.authorization.trim().split(' ');     let accessToken = parts.pop();     await oktaJwtVerifier.verifyAccessToken(accessToken)       .then(async jwt => {         const user = JSON.parse(request.headers['x-forwarded-user'].toString());         const currentUser = new CurrentUser(jwt.claims.uid, user.given_name, user.family_name);         container.set(CurrentUser, currentUser);       })       .catch(error => {         throw Error('JWT Validation failed!');       })   }   ... }); 

Sửa đổi graphql-api/src/controller/PointsController.ts để đưa CurrentUser vào làm phần phụ thuộc. Khi bạn đang ở đó, hãy điều chỉnh phương thức points() để lọc theo ID user và sửa đổi pointsSave() để đặt user khi lưu.

graphql-api / src / controller / PointsController.ts
import { Controller, Mutation, Query } from 'vesper'; import { EntityManager } from 'typeorm'; import { Points } from '../entity/Points'; import { User } from '../entity/User'; import { CurrentUser } from '../CurrentUser';  @Controller() export class PointsController {    constructor(private entityManager: EntityManager, private currentUser: CurrentUser) {   }    // serves "points: [Points]" requests   @Query()   points() {     return this.entityManager.getRepository(Points).createQueryBuilder("points")       .innerJoin("points.user", "user", "user.id = :id", { id: this.currentUser.id })       .getMany();   }    // serves "pointsGet(id: Int): Points" requests   @Query()   pointsGet({id}) {     return this.entityManager.findOne(Points, id);   }    // serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests   @Mutation()   pointsSave(args) {     // add current user to points saved     if (this.currentUser) {       const user = new User();       user.id = this.currentUser.id;       user.firstName = this.currentUser.firstName;       user.lastName = this.currentUser.lastName;       args.user = user;     }      const points = this.entityManager.create(Points, args);     return this.entityManager.save(Points, points);   }    // serves "pointsDelete(id: Int): Boolean" requests   @Mutation()   async pointsDelete({id}) {     await this.entityManager.remove(Points, {id: id});     return true;   } } 

Khởi động lại API và bây giờ nó sẽ hoàn tất.

Phương thức cộng điểm

Chỉnh sửa phương thức điểm

Mã nguồn

Bạn có thể tìm thấy mã nguồn của bài viết này trên GitHub .

Kết luận

Bài viết này đã hướng dẫn bạn cách tạo một ứng dụng React an toàn với GraphQL, TypeORM và Node / Vesper.


Tags:

Các tin liên quan