Apps Script用Sheet生成動態網頁(26): 移植註冊會員及確認信

我們在第八篇時製作了會員註冊系統,本篇就是要移植註冊會員頁跟確認信功能。廢話不多說,馬上開始!

因為要寄送註冊確認信,所以在appsscript.json裡頭會新增gmail的oauth權限。

"oauthScopes": [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/userinfo.email",
"https://mail.google.com/"
],

1. 製作註冊表單

這裡使用既有的LoginForm作為註冊會員的表單,這裡新增isSubmiting、buttonTitle和confirmPassword,用來呈現目前系統正在進行註冊、改變表單按鈕上的文字以及提供使用者確認密碼的欄位,這樣未來還能用在讓使用者變更密碼的地方。

src/client/demo-bootstrap/components/LoginForm.jsx

...
const LoginForm = ({ onSubmit, isSubmiting, buttonTitle, confirmPassword }) => {
...
{confirmPassword && (
<Form.Group as={Row} className="mb-3" controlId="confirmPassword">
<Form.Label column sm={2}>
確認密碼
</Form.Label>
<Col sm={10}>
<Form.Control
type="password"
name="confim-password"
placeholder="輸入密碼"
autocomplete="current-password"
required
/>
</Col>
</Form.Group>
)}
<Row>
<Button variant="primary" type="submit" disabled={isSubmiting}>
{isSubmiting && (
<Spinner as="span" animation="border" size="sm" role="status" />
)}
{buttonTitle}
</Button>
</Row>

2. 製作註冊頁面

這裡使用上面的調整後的LoginForm並設定表單按鈕文字為"註冊",這裡返回前一頁的功能使用router-dom提供的useHistory hook功能實作。當使用者按下註冊後,會透過register與後端伺服器確認。如果使用者已經登入,則不顯示註冊頁面跳到home頁面。此外,這裡如同登入頁面,使用Alert顯示註冊的提示訊息。

src/client/demo-bootstrap/pages/Registration.jsx
import React, { useState } from 'react';
import { Container, Row, Col, Alert } from 'react-bootstrap';
import { Redirect, useHistory } from 'react-router-dom';
import LoginForm from '../components/LoginForm';
import useAuth from '../hooks/useAuth';

import Server from '../../utils/server';

const { serverFunctions } = Server;

const Registration = () => {
const { authed } = useAuth();
const history = useHistory();
const [hint, showHint] = useState(null);
const [submiting, setSubmit] = useState(false);

const onSubmit = event => {
event.preventDefault();
setSubmit(true);
showHint('註冊中...');
serverFunctions
.register(event.target)
.then(response => {
event.target.reset();
showHint(response.message);
})
.catch(error => {
console.error(error);
showHint(error.message);
})
.finally(() => setSubmit(false));
};

const goBack = event => {
event.preventDefault();
history.goBack();
};

if (authed) return <Redirect to="/Home" replace />;

return (
<Container>
<h1>註冊會員</h1>
{hint && <Alert variant="secondary">{hint}</Alert>}
<LoginForm
onSubmit={onSubmit}
isSubmiting={submiting}
buttonTitle="註冊"
confirmPassword={true}
/>
<Row>
<Col>
<a className="text-secondary" href="#" onClick={goBack}>
<small>返回前一頁</small>
</a>
</Col>
</Row>
</Container>
);
};

export default Registration;

3. 新增註冊的Route

在App.jsx的AppRoutes中增加註冊的route
src/client/demo-bootstrap/App.jsx
import Registration from './pages/Registration';

const AppRoutes = () => {
...
<Route path="/register">
<Registration />
</Route>

登入頁面下方加入註冊會員的連結
src/client/demo-bootstrap/pages/Login.jsx
<Row>
<Link to="/register" className="text-secondary">
<small>註冊會員</small>
</Link>
</Row>

這樣我們就可以在登入頁面中選擇是否要註冊會員

4. 後端伺服器新增功能

在index.js裡頭加一行把register暴露出來。

src/server/index.js
global.register = userFunctions.register;

user.js中新增要暴露給前端的register函數。後頭並加上確認註冊信所需要功能。

src/server/user.js
import { RE_ACCOUNT, RE_PASSWORD, SERVER_URL } from './settings';
...
export function register(form) {
const { username, password } = form;
if (typeof username !== 'string' || !RE_ACCOUNT.test(username))
throw new Error('帳號格式錯誤:應是E-mail');
if (password !== form['confim-password']) throw new Error('兩次密碼不同');
if (typeof password !== 'string' || !RE_PASSWORD.test(password))
throw new Error(
'密碼格式錯誤:首字需為英文,其他為大小寫英數字,長度8到16之間'
);
return addUser(username, password);
}

function handleConfirm(token) {
let json = {};
try {
const payload = token.split('.')[1];
const decoded = Utilities.base64DecodeWebSafe(payload);
json = JSON.parse(Utilities.newBlob(decoded).getDataAsString());
} catch (error) {
Logger.log(`轉換失敗${error.stack}`);
throw new Error('確認資訊錯誤');
}
const sheet = getUserSheet();
const rowIdx = findIndexInColumn(json.iss, COLUMN_IDX_OF_NAME, sheet);
if (rowIdx < 0) {
Logger.log(`${json.iss} '尚未註冊`);
throw new Error('尚未註冊');
}
const range = sheet.getRange(1 + rowIdx, 1 + COLUMN_IDX_OF_ACCESSTOKEN, 1, 2);
const [accessToken, confirmed] = range.getValues()[0];
if (confirmed) return { status: 200, message: '註冊已確認' };
if (token !== accessToken) {
Logger.log('token!=', token, accessToken);
throw new Error('確認失敗');
}
range.setValues([[accessToken, new Date()]]);
SpreadsheetApp.flush();
return { status: 200, message: '註冊已確認' };
}

export function confirmRegistration(token) {
let content;
try {
handleConfirm(token);
content = '註冊確認成功';
} catch (error) {
Logger.log(error.stack);
content = `註冊確認失敗 ${error.message}`;
}
return HtmlService.createHtmlOutput(content);
}


5. 後端的註冊確認信功能

註冊確認信是透過給使用者一個超連結來完成的,所以我們無法使用暴露後端API方式來完成,而是要透過Apps Script的doGet來處理超連結來進行。這邊就要在Handlers裡頭加一個confirmToken,然後使用到上面user裡新增的confirmRegistration功能,這裡最後有一個immediateRetrun部份,就是指confirmRegistration處理完之後會直接回給使用者,不會進行其他的處理。

src/server/handlers.js
import { confirmRegistration } from './user';

const Handlers = {
...
confirmToken: {
func: confirmRegistration,
immediateRetrun: true,
},
...

6. 成果展示




留言