Apps Script用Sheet生成動態網頁(8): 製作會員註冊系統

有了前面7篇的的一些經驗,我們已經。這之後在修改權限檢查的機制,將上傳、刪除等功能開放給會員使用。

因為這篇得程式碼較多,所以就馬上開始吧!

這裡要製作的會員系統會包含,註冊頁面、資料檢核、寄出確認信、確認註冊完成等4步驟。會使用到JWT(JSON Web Token)作為會員確認註冊用的資料。

1.  引用JWT程式

下面得JWT使用,請拷貝createJwt()程式碼成指令碼檔案jwt.js

How to Create JSON Web Token (JWT) with Google Apps Script

2. Apps Script的部份(後端/伺服端)

為了隔離一些區域用的函數及變數,這裡要開新的指令碼檔案register.js,把以下內容放進去。
  • registerUser()部份會對於會註冊的輸入進行資料檢核。
  • addUser()部份會檢查既有的帳號是否曾經註冊,若沒有註冊,就產生新的JWT,然後寄送mail到會員的信箱中。
  • findIndexInColumn()用來查找資料表內特定資料欄的index。
  • _getUsersSheet()部份是用來取使用者資料表。

const _COLUMN_IDX_OF_NAME = 0;
const _COLUMN_IDX_OF_PWD = 1;
const _COLUMN_IDX_OF_ACCESSTOKEN = 2;
const _COLUMN_IDX_OF_CONFIRMED = 3;

function _getUsersSheet() {
const USERS_SHEET_NAME = '使用者';
let book = SpreadsheetApp.openByUrl(SHEET_URL);
return book.getSheetByName(USERS_SHEET_NAME) || book.insertSheet(USERS_SHEET_NAME);
}

function findIndexInColumn(name, column, sheet) {
let list = sheet.getRange(1, 1 + column, 1 + sheet.getLastRow(), 1).getValues();
return list.findIndex(r => name === r[column]);
}

function addUser(name, pwd) {
let sheet = _getUsersSheet();
let row_idx = findIndexInColumn(name, _COLUMN_IDX_OF_NAME, sheet);
if (row_idx >= 0) {
let confirmed = sheet.getRange(1 + row_idx, 1 + _COLUMN_IDX_OF_CONFIRMED).getValue();
if (confirmed) throw new Error("你已經註冊完成囉!");
throw new Error("你已經註冊了,但是還沒點擊確認信,請查看你的信箱!");
}
const accessToken = createJwt({
privateKey: ScriptApp.getScriptId(), // 請改成你喜歡的
expiresInHours: 24, data: { iss: name },
});
sheet.appendRow([name, pwd, accessToken]);
const url = `${getServerUrl()}?confirmToken=${accessToken}`;
GmailApp.sendEmail(name, "確認註冊", '', {
noReply: true, name: '註冊系統', htmlBody:
`<p>您的註冊時間: ${new Date().toString()}</p>
<p>請點擊此連結以確認<a href="${url}">${url}</a></p>`,
});
SpreadsheetApp.flush();
return { status: 201, message: '已建立使用者' };
}

function registerUser(form) {
let name = form.username;
if (typeof name !== 'string' || !/^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z]+$/.test(name))
throw new Error("帳號格式錯誤:為E-mail帳號");
if (form.password !== form['confim-password'])
throw new Error("兩次密碼不同");
let password = form.password;
if (typeof password !== 'string' || !/^[A-Za-z][A-Za-z0-9]{8,16}$/.test(password))
throw new Error("密碼格式錯誤:首字需為英文,其他為大小寫英數字,長度8到16之間");
return addUser(name, password);
}


在主程式的doGet()裡面我們要加入處理點擊確認信會得到的confirmToken參數,給予handleConfirm()進行確認。如果確認成功,就會在畫面上顯示"註冊確認成功";反之,顯示註冊確認失敗。

function doGet(req) {
if (req.parameter['confirmToken']) {
let content;
try {
let result = handleConfirm(req.parameter['confirmToken']);
Logger.log(result);
content = '註冊確認成功';
} catch(error) {
Logger.log(error.stack);
content = '註冊確認失敗' + error.message;
}
return HtmlService.createHtmlOutput(content);
}
// ...
}

handleConfirm()這個函數要放到指令碼檔案register.js,針對註冊信的token部份進行解碼,取用JWT的第二部份。

注意:實務上,這裡應該要確認JWT第三部份的簽章,不過這裡是作為展示步驟。
function handleConfirm(token) {
let json = {};
try {
let payload = token.split('.')[1];
let decoded = Utilities.base64DecodeWebSafe(payload);
json = JSON.parse(Utilities.newBlob(decoded).getDataAsString());
} catch (error) {
Logger.log('轉換失敗' + error.stack);
}
let sheet = _getUsersSheet();
let row_idx = findIndexInColumn(json.name, _COLUMN_IDX_OF_NAME, sheet);
if (row_idx < 0) {
Logger.log(json.name + '尚未註冊');
throw new Error('尚未註冊');
}
const range = sheet.getRange(1 + row_idx, 1 + _COLUMN_IDX_OF_ACCESSTOKEN, 1, 2);
let [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: '註冊已確認' };
}

3. HTML部份(前端/使用者端)

前端部份這裡僅建構一個註冊頁面,使用前篇上傳檔案的網頁,稍做修改完成。
使用者點擊E-mail的確認連結,是直接顯示確認成功或失敗字樣,所以就不製作確認頁面的前端。

注意:實務上,密碼應該要雜湊或加密一下再傳給後端。

<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head><base target="_top"><meta charset="UTF-8"></head>
<body>
<h1>註冊使用者</h1>
<form onsubmit="onSubmit(event)">
<label>帳號(E-mail)<input type="text" name="username" autocomplete="username" required /></label>
<label>密碼<input type="password" name="password" autocomplete="current-password" required /></label>
<label>確認密碼<input type="password" name="confim-password" autocomplete="current-password" required /></label>
<input type="submit" value="註冊" />
</form>
<div id="msg"></div>
<br />
<script>
function showText(txt) {
var target = document.getElementById('msg');
if (target) target.textContent = typeof txt !== "string" ? JSON.stringify(txt) : txt;
}
function onSubmit(event) {
event.preventDefault();
showText('註冊中,請稍候...');
try {
google.script.run
.withSuccessHandler(onCompleted)
.withFailureHandler(onFailure)
.registerUser(event.target);
} catch (error) {
onFailure(error);
}
return false;
}
function onCompleted(response) {
console.log(response);
showText(response.message);
}
function onFailure(error) {
console.error(error);
showText('錯誤訊息:' + error.message);
}
</script>
</body>
</html>

4. 成果展示



留言