Apps Script用Sheet生成動態網頁(31): 使用Google登入時榜定帳號

 在第28篇裡我們整合了Google登入(文章連結),不過就直接讓所有Google帳號可以直接當成我們的帳號進行登入。這裡我們要補足之前的實作,檢查並確認榜定既有帳號,如果沒有既有帳號才會直接當成帳號進行登入。


1. Google登入機制調整

以下說明使用重構後的程式碼,看起來會比較簡潔。

因為要支援榜定既有帳號,所以當我們使用Google登入後,首先會嘗是以loginByOpenId()進行登入,檢查既有帳號中的榜定欄位內容,依照不同登入服務提供商會檢查不同的榜定欄位。這裡只有Google登入會使用;LINE登入要取得用戶E-Mail需要另外申請並附上說明資料,因為只是實驗所以也沒打算申請處理。

接著,會檢查Google登入提供的E-mail是與既有的帳號相同,若有就會請使用者進行榜定createGoogleBinding()。不榜定則會無法進行登入。

如果是其他找不到E-mail或是不存在既有帳號的話,就使用之前的登入方式loginByOAuth()。

src/server/oauth/google.js

import { GOOGLE_CONFIG as config, SERVER_URL } from '../settings';
import { loginByOAuth, loginByOpenId, createGoogleBinding } from '../user';
import { getFunForCommonOAuth } from './common';
import templates from '../templates';

export const checkState = state => state === config.loginState;

const getTokenBindingUrl = token =>
`${SERVER_URL}?show=bind-account&name=${token}`;

const OAuth = getFunForCommonOAuth(config, oauthLogin => {
try {
const { sub: openId, name } = oauthLogin;
// use the bind user for login
const openIdLogin = loginByOpenId(openId, config.providerName);
if (openIdLogin) {
return templates.getSuccess({
token: openIdLogin.token,
id: openId,
name,
provider: config.providerName,
});
}
// ask user to bind if the email has been registered.
const email = oauthLogin.email_verified ? oauthLogin.email : null;
if (email) {
const bindToken = createGoogleBinding(email, openId);
if (bindToken) {
return templates.getGoogleBinding({
bindUrl: getTokenBindingUrl(bindToken),
bindId: email,
bindName: name,
provider: config.providerName,
});
}
}
// simple login
const ourLogin = loginByOAuth(openId, config.providerName);
return templates.getSuccess({
token: ourLogin.token,
id: openId,
name,
provider: config.providerName,
});
} catch (except) {
return templates.logError(except);
}
});

export default OAuth;


2. Google帳號榜定頁面展示


3. 處理使用者確認榜定

在既有帳號中看是否榜定Google登入,再決定是否可以用既有帳號登入。

src/server/user.js

export const loginByOpenId = (openId, provider) => {
let providerColumnIdx = -1;
switch (provider) {
case 'Google':
providerColumnIdx = COLUMN_IDX_OF_BIND_GOOGLE;
break;
default:
}
if (providerColumnIdx < 0) throw new Error(`未知的登入提供者${provider}`);
const sheet = getUserSheet();
const rowIdx = findIndexInColumn(openId, providerColumnIdx, sheet);
if (rowIdx < 0) return null;
const uid = sheet.getRange(1 + rowIdx, 1 + COLUMN_IDX_OF_NAME).getValue();
return {
status: 201,
message: '已成功登入',
token: createToken({ name: uid }),
};
};

如果沒有就會建立一個榜定用的token,裡頭新增了openId的屬性,所以createToken()也有調整加入openId。

src/server/user.js

export const createGoogleBinding = (email, openId) => {
const sheet = getUserSheet();
const rowIdx = findIndexInColumn(email, COLUMN_IDX_OF_NAME, sheet);
if (rowIdx < 0) {
return null; // 沒找到既有Email帳號
}
const bindRange = sheet.getRange(1 + rowIdx, 1 + COLUMN_IDX_OF_BIND_GOOGLE);
const bindId = bindRange.getValue();
if (bindId && !(typeof bindId === 'string' && bindId.startsWith('ey'))) {
throw new Error('已榜定');
}
const token = createToken({
name: email,
openId: { id: openId, provider: 'Google' },
});
bindRange.setValue(token);
return token;
};



處理使用者同意榜定部份,是透過跟註冊確認相同得方式,所以新增了處理函數handleOpenIdConfirm()和confirmOpenIdBinding(),之後還要加上doGet的handler。

src/server/user.js


function handleOpenIdConfirm(token) {
let json;
try {
json = decodeJwt(token);
} catch (error) {
Logger.log(`轉換失敗${error.stack}`);
throw new Error('榜定資訊錯誤');
}
if (!json.openId) {
Logger.log(`not found Open ID ${JSON.stringify(json)}`);
throw new Error('無法榜定未知的使用者ID');
}
const sheet = getUserSheet();
const rowIdx = findIndexInColumn(json.iss, COLUMN_IDX_OF_NAME, sheet);
if (rowIdx < 0) {
Logger.log(`${json.iss} '尚未註冊`);
throw new Error('尚未註冊');
}
let providerColumnIdx = -1;
switch (json.openId.provider) {
case 'Google':
providerColumnIdx = COLUMN_IDX_OF_BIND_GOOGLE;
break;
default:
}
if (providerColumnIdx < 0) {
throw new Error(`未知的榜定提供者${json.openId.provider}`);
}
const tokenRange = sheet.getRange(1 + rowIdx, 1 + providerColumnIdx);
const accessToken = tokenRange.getValue();
if (token !== accessToken) {
Logger.log('bind-token!=', token, accessToken);
throw new Error('榜定失敗');
}
tokenRange.setValue(json.openId.id);
SpreadsheetApp.flush();
return json.openId;
}

export function confirmOpenIdBinding(token) {
try {
const openId = handleOpenIdConfirm(token);
const openIdLogin = loginByOpenId(openId.id, openId.provider);
if (!openIdLogin) throw new Error('登入失敗');
return templates.getSuccess({
token: openIdLogin.token,
id: openId.id,
provider: openId.provider,
});
} catch (error) {
Logger.log(error.stack);
return templates.getFailure({
error: '榜定失敗',
desc: `${error.message}`,
});
}
}

在Handler部份就接上前面新增的處理函數,這樣只要使用者點擊確認就會進行完成榜定作業。

src/server/handlers.js
'bind-account': {
func: confirmOpenIdBinding,
},



留言