Apps Script用Sheet生成動態網頁(25): 登入才能使用的功能分項

關於登入後才能用的功能分項,我之前一直都只有硬幹的想法。直到看到了Protected Routes and Authentication with React Router 心想:這就是我要的東西阿!所以我們就來套用囉。

這篇裡頭都是前端的功能調整,是處理選單及登出入狀態,後端調整則是要使用者的權限結合,不會在這裡。

這篇裡頭都是前端的功能調整,是處理選單及登出入狀態,後端調整則是要使用者的權限結合,不會在這裡。

1. 建立驗證登入的Hook

先建立一個驗證登入的hook,放在hooks/useAuth.js,這裡參考 Protected Routes and Authentication with React Router 的作法。

  • 使用 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. 成果展示

使用者未登入,可看到選單中只有首頁、連結列表、登入


使用者登入後,可看到選單變長了,並且我們的登入頁面也會直接顯示您已登入。


留言