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 .
{ ... "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
:
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
:
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:
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 .
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.
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.
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ứ.
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.
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.
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:
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 ApolloProvider
và Query
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:
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.
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}
và {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
, Login
và Points
. 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:
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/>
và <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
.
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 đó.
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
:
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()
và 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 đó.
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
.
.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 .
Khi bạn nhập thông tin đăng nhập, bạn sẽ đăng nhập.
Nhấp vào Quản lý điểm để xem danh sách điểm.
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.
export class CurrentUser { constructor(public id: string, public firstName: string, public lastName: string) {} }
Nhập OktaJwtVerifier
và CurrentUser
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.
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.
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.
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.
Các tin liên quan