關於登入後才能用的功能分項,我之前一直都只有硬幹的想法。直到看到了Protected Routes and Authentication with React Router 心想:這就是我要的東西阿!所以我們就來套用囉。
這篇裡頭都是前端的功能調整,是處理選單及登出入狀態,後端調整則是要使用者的權限結合,不會在這裡。
這篇裡頭都是前端的功能調整,是處理選單及登出入狀態,後端調整則是要使用者的權限結合,不會在這裡。
1. 建立驗證登入的Hook
- 使用 useEffect(...,[])在初始化時,讀取本機上的使用者登入資料,並使用authLogin與後端伺服器確定是否已經登入。
- 修改login函數,導入我們的登入功能,使用loginUser與後端伺服器進行登入。
- 修改logout函數,導入我們之前製作登出的功能,參考:dropToken()。
import React, { createContext, useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import Server from '../../utils/server';
const { serverFunctions } = Server;
const NAME_OF_TOKEN = 'user-token';
const authContext = createContext();
function useAuth() {
const [authed, setAuthed] = useState(false);
useEffect(() => {
const token = localStorage.getItem(NAME_OF_TOKEN);
serverFunctions.authLogin(token).then(() => setAuthed(true));
}, []);
const login = form => {
return new Promise((res, rej) => {
serverFunctions
.loginUser(form)
.then(response => {
localStorage.setItem(NAME_OF_TOKEN, response.token);
setAuthed(true);
res(response);
})
.catch(rej);
});
};
const logout = () => {
return new Promise(res => {
localStorage.removeItem(NAME_OF_TOKEN);
setAuthed(false);
res();
});
};
return { authed, login, logout };
}
export function AuthProvider({ children }) {
const auth = useAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
AuthProvider.propTypes = {
children: PropTypes.element,
};
export default function AuthConsumer() {
return useContext(authContext);
}
接著我們將hook套用到App.jsx,為了簡化架構,我們把原本App更名為AppRoutes,然後產生的App類別。將AuthProvider設定在最頂層,表示其他子元件裡頭都可以方便的使用hook,提供驗證登入的功能。
src/client/demo-bootstrap/App.jsx
import { AuthProvider } from './hooks/useAuth';
...
const App = ()
=> {
return (
<AuthProvider>
<AppRoutes />
</AuthProvider>
);
};
這樣設定之後就是表示,這個App一開始會跑useAuth裡頭的驗證功能,之後各元件如果要使用useAuth這個hook,就是使用default匯出的AuthConsumer這個來取得目前狀態,登入及登出功能。
2. 建立PrivateRoute並修改Routes
src/client/demo-bootstrap/components/PrivateRoute.js
PrivateRoute的目的就是當使用者如果沒有登入,就導向登入頁面,反之,則正常顯示頁面。這裡透過react-router的Redirect進行轉頁,並給予state參數,請登入頁面將資訊顯示在使用者端。
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import useAuth from '../hooks/useAuth';
const PrivateRoute = ({ children, path }) => {
const { authed } = useAuth();
const ele =
authed === true ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: { message: '使用此功能需要先請您登入' },
}}
/>
);
return <Route path={path}>{ele}</Route>;
};
PrivateRoute.propTypes = {
children: PropTypes.element,
path: PropTypes.string,
};
export default PrivateRoute;
然後修改AppRoutes,前面被我們改名的App類別,我們將上傳HTML和列出Drive檔案列表的功能設定為登入才能使用的功能,從原本的Route改成剛建立的PrivateRoute類別。
src/client/demo-bootstrap/App.jsx
import PrivateRoute from './components/PrivateRoute';
...
const AppRoutes = () => {
...
<PrivateRoute path="/drive-lister">
<DirveLister />
</PrivateRoute>
...
<PrivateRoute path="/upload-html">
<UploadHtml />
</PrivateRoute>
我們改完Routes其實只是完成底層的工作,還有界面上的選單要更改,加入useAuth Hook。
src/client/demo-bootstrap/components/MyNav.jsx
import useAuth from '../hooks/useAuth';
...
const MyNav = () => {
const { authed } = useAuth();
...
{authed && (
<IconNavLink linkTo="/drive-lister" iconComp={<BsFillGridFill />}>
Drive檔案列表
</IconNavLink>
)}
...
{authed && (
<IconNavLink
linkTo="/upload-html"
iconComp={<BsFillCloudUploadFill />}
>
上傳
</IconNavLink>
)}
<IconNavLink linkTo="/login" iconComp={<BsPersonCircle />}>
{authed ? '登出' : '登入'}
</IconNavLink>
3. 修改登入頁面,並支援登出功能。
之前我們移植登入頁面後,就都沒有製作登出頁面,所以這裡就要順便製作登出功能。由於我們已經在useAuth裡頭提供了login和logout,所以登入頁面就可以簡化成呼叫useAuth提供的login函數,和logout函數。
import React, { useState } from 'react';
import { Container, Row, Alert, Button } from 'react-bootstrap';
import { Link, useLocation } from 'react-router-dom';
import LoginForm from '../components/LoginForm';
import useAuth from '../hooks/useAuth';
const Login = () => {
const { authed, login, logout } = useAuth();
const [msg, showText] = useState(null);
const location = useLocation();
const onSubmit = event => {
event.preventDefault();
showText('登入中,請稍候...');
login(event.target).catch(error => {
console.warn(error);
showText(error.message);
});
};
const onLogout = () => {
logout();
showText(null);
};
if (authed) {
return (
<Container>
<h1>您已登入</h1>
<Row>
<Button onClick={onLogout} variant="secondary">
登出
</Button>
</Row>
<Row>
<Link to="/home">點此前往首頁</Link>
</Row>
</Container>
);
}
const hint = msg || location?.state?.message;
return (
<Container>
<h1>登入</h1>
{hint && <Alert variant="secondary">{hint}</Alert>}
<LoginForm onSubmit={onSubmit} />
</Container>
);
};
export default Login;
4. 成果展示
使用者未登入,可看到選單中只有首頁、連結列表、登入
使用者登入後,可看到選單變長了,並且我們的登入頁面也會直接顯示您已登入。
留言