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,
  },

留言