Skip to content

Commit

Permalink
Login API integration (#43)
Browse files Browse the repository at this point in the history
* Handle 401

* Update login api

* Post with CSRF token

* Save csrf token in local storage
  • Loading branch information
jizhang committed Jan 14, 2023
1 parent 228cc95 commit c508180
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 45 deletions.
7 changes: 5 additions & 2 deletions mock/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,11 @@ function getPrimaryData(req, res) {
}

sendJson(req, res, {
payload: {
measures: [users, sessions, bounceRate, sessionDuration],
statusCode: 200,
body: {
payload: {
measures: [users, sessions, bounceRate, sessionDuration],
},
},
})
}
Expand Down
36 changes: 23 additions & 13 deletions mock/login.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
const qs = require('qs')
const url = require('url')
const sendJson = require('send-data/json')

function login(req, res) {
let { username, password } = req.body
if (username === 'admin' && password === '888888') {
sendJson(req, res, {
payload: {
statusCode: 200,
body: {
id: 1,
nickname: 'Jerry',
},
})
} else {
res.statusCode = 400
sendJson(req, res, {
code: 400,
payload: {
message: 'invalid username or password',
statusCode: 400,
body: {
code: 400,
message: 'Invalid username or password',
},
})
}
}

function logout(req, res) {
sendJson(req, res, {})
}

function getCurrentUser(req, res) {
sendJson(req, res, {
statusCode: 200,
body: {
id: 1,
nickname: 'Jerry',
},
})
}

function getCsrfToken(req, res) {
sendJson(req, res, {
payload: 'ok',
token: 'mock-token',
})
}

module.exports = {
'POST /api/login': login,
'POST /api/logout': logout,
'GET /:controller/:action': (req, res, params) => {
const { query } = url.parse(req.url)
console.log(params, qs.parse(query))
res.end()
},
'GET /api/current-user': getCurrentUser,
'GET /api/csrf': getCsrfToken,
}
18 changes: 3 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import React from 'react'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './routes/Home'
import Dashboard from './routes/Dashboard'
import Login from './routes/Login'

const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/login', element: <Login /> },
{ path: '/dashboard', element: <Dashboard /> },
])
import { RouterProvider } from 'react-router-dom'
import router from './router'

export default () => {
return (
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)
return <RouterProvider router={router} />
}
43 changes: 36 additions & 7 deletions src/common/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import _ from 'lodash'
import qs from 'qs'
import moment from 'moment'
import { Toast } from 'antd-mobile'
import router from '~/src/router'

const CSRF_STORAGE_KEY = 'csrf'

class RequestError {
constructor(public code: number, public payload: any) {}
Expand All @@ -22,27 +26,29 @@ export async function request(url: string, config?: RequestInit) {

// success
if (response.ok) {
const success = await response.json()
return success.payload
const payload = await response.json()
return payload.payload || payload
}

// 400 Bad Request
if (response.status === 400) {
const failure = await response.json()
const payload = await response.json()
// global toast
if (failure.code === 400) {
Toast.show({ icon: 'fail', content: failure.payload.message })
if (payload.code === 400) {
Toast.show({ icon: 'fail', content: payload.message })
}
// raise error for downstream processing
throw new RequestError(failure.code, failure.payload)
throw new RequestError(payload.code, payload)
}

// 401 Unauthorized
if (response.status === 401) {
// TODO redirect to /login
router.navigate('/login')
throw new RequestError(401, null)
}

// other error
Toast.show({ icon: 'fail', content: response.status + ' ' + response.statusText })
throw new RequestError(response.status, response.statusText)
}

Expand All @@ -58,6 +64,7 @@ export async function post(url: string, json?: any) {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': await getCsrfToken(),
},
}
if (!_.isEmpty(json)) {
Expand All @@ -71,10 +78,32 @@ export async function postForm(url: string, form?: any) {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': await getCsrfToken(),
},
}
if (!_.isEmpty(form)) {
config.body = qs.stringify(form)
}
return await request(url, config)
}

async function getCsrfToken() {
const value = localStorage.getItem(CSRF_STORAGE_KEY)
if (value) {
const csrf = JSON.parse(value)
if (moment().isBefore(csrf.expire)) {
return csrf.token
}
}
const payload = await request('/api/csrf')
const csrf = {
token: payload.token,
expire: moment().add(5, 'minutes'),
}
localStorage.setItem(CSRF_STORAGE_KEY, JSON.stringify(csrf))
return csrf.token
}

export function clearCsrfToken() {
localStorage.removeItem(CSRF_STORAGE_KEY)
}
11 changes: 11 additions & 0 deletions src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'
import { createBrowserRouter } from 'react-router-dom'
import Home from './routes/Home'
import Dashboard from './routes/Dashboard'
import Login from './routes/Login'

export default createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/login', element: <Login /> },
{ path: '/dashboard', element: <Dashboard /> },
])
6 changes: 5 additions & 1 deletion src/routes/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react'
import React, { useContext, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button, Space } from 'antd-mobile'
import { observer } from 'mobx-react-lite'
Expand All @@ -10,6 +10,10 @@ export default observer(() => {
const { loginStore } = useContext(RootStoreContext)
const navigate = useNavigate()

useEffect(() => {
loginStore.getCurrentUser()
}, [])

function gotoDashboard() {
navigate('/dashboard')
}
Expand Down
12 changes: 10 additions & 2 deletions src/routes/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react'
import React, { useContext, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Form, Input, Button, Toast } from 'antd-mobile'
import { observer } from 'mobx-react-lite'
Expand All @@ -11,9 +11,11 @@ import * as styles from './Login.module.less'
export default observer(() => {
const { loginStore } = useContext(RootStoreContext)
const navigate = useNavigate()
const [loginDisabled, setLoginDisabled] = useState(false)

const handleSubmit = (values: LoginRequest) => {
loginStore.login(values).then(() => {
setLoginDisabled(true)
Toast.show({
icon: 'success',
content: `Welcome, ${loginStore.currentUser.nickname}!`,
Expand All @@ -35,7 +37,13 @@ export default observer(() => {
layout="horizontal"
onFinish={handleSubmit}
footer={
<Button type="submit" color="primary" block loading={loginStore.loggingIn}>
<Button
type="submit"
color="primary"
block
loading={loginStore.loggingIn}
disabled={loginDisabled}
>
Login
</Button>
}
Expand Down
16 changes: 12 additions & 4 deletions src/services/login.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { post } from '~/src/common/request'
import { request, post, clearCsrfToken } from '~/src/common/request'

export interface LoginRequest {
username: string
password: string
}

export interface LoginResponse {
export interface CurrentUser {
id: number
nickname: string
}

export async function login(data: LoginRequest): Promise<LoginResponse> {
export async function login(data: LoginRequest): Promise<CurrentUser> {
return post('/api/login', data)
}

export async function logout() {
return post('/api/logout')
try {
return await post('/api/logout')
} finally {
clearCsrfToken()
}
}

export async function getCurrentUser(): Promise<CurrentUser> {
return request('/api/current-user')
}
9 changes: 8 additions & 1 deletion src/stores/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class LoingStore {

loggingIn = false
loggingOut = false
currentUser: service.LoginResponse = getDefaultCurrentUser()
currentUser: service.CurrentUser = getDefaultCurrentUser()

constructor(rootStore: RootStore) {
makeAutoObservable(this, { rootStore: false })
Expand Down Expand Up @@ -48,4 +48,11 @@ export default class LoingStore {
})
}
}

async getCurrentUser() {
const payload = await service.getCurrentUser()
runInAction(() => {
this.currentUser = payload
})
}
}

0 comments on commit c508180

Please sign in to comment.