Kon's DX Lab - Case Study

Day 60|Logiless API × GASでOAuth認証を完全構築!

Published on 2025-05-31

🔬 Case Study Summary
Problem

(ここに課題を記述)

Result

(ここに具体的な成果を記述)


Tech & Process

(ここに採用技術とプロセスを記述) コードを詳しく見る »

こんにちは、こんです🦊

今日は、Logiless APIをGAS(Google Apps Script)で扱うためのOAuth2認証環境を整えました!

「配送データを自動取得したい」「返品ステータスを定期チェックしたい」など、Logiless APIを活用した自動化は非常に便利ですが、最初のハードルがOAuth2認証の構築
今回はそれをゼロからGASで実装し、誰でも社内で使える形に仕上げました。


実験のきっかけ:「認証って面倒そう…」

  • Logiless APIを使いたいけど「OAuthって難しそう」

  • トークンの有効期限切れでエラーになる

  • 複数メンバーに使ってもらいたいが、再認証が面倒

「だったら一度しっかり整えて、誰でも再利用できるようにしよう」と思い、取り組んでみました。


作ってみたもの(システムの概要)

今回は以下のような認証基盤を構築しました:

  • ✅ 認証用URLの自動生成&UI表示

  • ✅ 初回認証時のトークン保存(access_token / refresh_token / 有効期限)

  • ✅ トリガーやUIからの自動リフレッシュ

  • ✅ リフレッシュトークンの再取得にも対応

  • ✅ 有効期限の残り時間をユーザーに表示

  • ✅ エラー発生時のSlack通知やログ出力(※発展編)

すべてGAS(Google Apps Script)だけで完結しています。


実感したメリット

使ってみて感じたことを3つにまとめてみました。

1. トークン管理が“見える化”された

「あと何時間で切れるか?」がUIで確認できるようになり、エラー前の更新判断が可能になりました。

2. 一度認証すれば“自動で更新”

初回だけブラウザ認証をすれば、その後はバックグラウンドで自動リフレッシュ。定期実行トリガーとも親和性◎。

3. エラー時の対応が楽になった

リフレッシュ失敗時やJSONパース失敗などもログに出力し、UIで通知するようにしているので、運用時の安心感が段違いです。


実際の画面イメージ(構成)

  • 【認証メニュー】:スプレッドシート上のメニューから「認証開始」「状況確認」「手動更新」などを選択できるように

  • 【認証URLダイアログ】:初回認証時にログインURLを表示して、認証完了後にトークン保存

  • 【認証チェック表示】:トークンの有効期限や残り時間をダイアログで可視化


🔧 Logiless API × GAS 完全認証スクリプト(全文)

🔐 注意:client_id と client_secret は Logiless開発者ページ で取得したものを自身でセットしてください。

// === 認証用URLを生成し、ユーザーに表示 ===
function authorize() {
  const service = getOAuthService();
  const authorizationUrl = service.getAuthorizationUrl();
  const message = service.hasAccess()
    ? "LOGILESS APIの認証は既に完了しています。"
    : `<p>LOGILESS APIの認証が必要です。<br>以下のURLを開いて認証してください:</p>
       <a href="${authorizationUrl}" target="_blank">${authorizationUrl}</a>`;
  showDialog(message, "認証状況");
}

// === クライアントID/シークレットを保存 ===
function setClientCredentials() {
  const props = PropertiesService.getScriptProperties();
  props.setProperty('client_id', 'YOUR_CLIENT_ID'); // ← 自分のIDに差し替えてください
  props.setProperty('client_secret', 'YOUR_CLIENT_SECRET'); // ← 自分のシークレットに差し替えてください
}

// === OAuth2認証情報を取得 ===
function getOAuthCredentials() {
  const props = PropertiesService.getScriptProperties();
  return {
    clientId: props.getProperty('client_id'),
    clientSecret: props.getProperty('client_secret'),
  };
}

// === OAuth2サービス定義 ===
function getOAuthService() {
  const { clientId, clientSecret } = getOAuthCredentials();
  return OAuth2.createService('logiless')
    .setAuthorizationBaseUrl('https://app2.logiless.com/oauth/v2/auth')
    .setTokenUrl('https://app2.logiless.com/oauth2/token')
    .setClientId(clientId)
    .setClientSecret(clientSecret)
    .setRedirectUri(ScriptApp.getService().getUrl())
    .setCallbackFunction('authCallback')
    .setPropertyStore(PropertiesService.getUserProperties())
    .setCache(CacheService.getUserCache());
}

// === コールバック処理(認証成功・失敗) ===
function authCallback(request) {
  const service = getOAuthService();
  const isAuthorized = service.handleCallback(request);
  const message = isAuthorized
    ? "LOGILESS APIの認証に成功しました。"
    : "LOGILESS APIの認証に失敗しました。<br>もう一度お試しください。";
  if (isAuthorized) {
    const token = service.getToken();
    Logger.log(`🟢 初回認証トークン: ${JSON.stringify(token)}`);
    saveTokenExpiry(token);
    if (token.refresh_token) {
      PropertiesService.getScriptProperties().setProperty('refresh_token', token.refresh_token);
    }
  }
  showDialog(message, isAuthorized ? "認証成功" : "認証失敗");
  return HtmlService.createHtmlOutput(`<p>${message}</p>`);
}

// === アクセストークンの有効期限を保存 ===
function saveTokenExpiry(token) {
  if (token?.expires_in) {
    const expiryTime = Date.now() + token.expires_in * 1000;
    PropertiesService.getScriptProperties().setProperty('token_expiry', expiryTime.toString());
  }
}

// === UIメニューからの更新実行用 ===
function refreshAccessTokenFromMenu() {
  try {
    refreshAccessTokenIfNeeded(true, false);
  } catch (e) {
    Logger.log(`⚠️ メニュー実行中のエラー: ${e.message}`);
    showDialog(`アクセストークン更新に失敗しました。<br>エラー: ${e.message}`, "認証状況");
  }
}

// === トリガーからの更新用 ===
function refreshAccessTokenOnTrigger() {
  refreshAccessTokenIfNeeded(false, true);
}

// === 有効期限チェック&自動リフレッシュ処理 ===
function refreshAccessTokenIfNeeded(forceRefresh = false, isTriggered = false) {
  const service = getOAuthService();
  const props = PropertiesService.getScriptProperties();
  const expiryTime = Number(props.getProperty('token_expiry')) || 0;
  const remainingTime = expiryTime - Date.now();

  if (!service.hasAccess()) {
    return isTriggered ? null : showDialog("LOGILESS APIの認証が必要です。", "認証状況");
  }

  if (!expiryTime) {
    Logger.log("⚠️ 有効期限が保存されていません。");
    return isTriggered ? null : showDialog("トークンの有効期限が保存されていません。<br>初回認証をやり直してください。", "認証状況");
  }

  if (forceRefresh || remainingTime <= 24 * 60 * 60 * 1000) {
    refreshAccessToken(isTriggered);
  } else if (!isTriggered) {
    showDialog(`残り時間: ${formatExpiryTime(expiryTime)}<br>24時間未満になったら更新します。`, "認証状況");
  }
}

// === アクセストークンのリフレッシュ実行 ===
function refreshAccessToken(isTriggered = false) {
  const props = PropertiesService.getScriptProperties();
  const refreshToken = props.getProperty('refresh_token');
  const { clientId, clientSecret } = getOAuthCredentials();

  if (!refreshToken || !clientId || !clientSecret) {
    return isTriggered ? null : showDialog("認証情報が不足しています。<br>初回認証をやり直してください。", "認証状況");
  }

  const query = `client_id=${encodeURIComponent(clientId)}&` +
                `client_secret=${encodeURIComponent(clientSecret)}&` +
                `grant_type=refresh_token&` +
                `refresh_token=${encodeURIComponent(refreshToken)}`;

  const url = `https://app2.logiless.com/oauth2/token?${query}`;

  let responseText = '';
  try {
    const response = UrlFetchApp.fetch(url, {
      method: 'get',
      muteHttpExceptions: true,
    });

    responseText = response.getContentText();
    Logger.log(`📥 APIレスポンス: ${responseText}`);

    if (!responseText) {
      return isTriggered ? null : showDialog("レスポンスが空です。更新失敗。", "認証状況");
    }

    const result = JSON.parse(responseText);
    if (result.access_token) {
      const newExpiryTime = Date.now() + result.expires_in * 1000;
      props.setProperty('token_expiry', newExpiryTime.toString());
      if (result.refresh_token) props.setProperty('refresh_token', result.refresh_token);
      return isTriggered ? null : showDialog(`トークンを更新しました。<br>${formatExpiryTime(newExpiryTime)}`, "認証状況");
    } else {
      return isTriggered ? null : showDialog(`更新に失敗しました。<br>${responseText}`, "認証状況");
    }
  } catch (error) {
    Logger.log(`⚠️ JSONパース失敗: ${error.message}`);
    return isTriggered ? null : showDialog(`JSONエラー: ${error.message}`, "認証状況");
  }
}

// === 有効期限のフォーマット表示 ===
function formatExpiryTime(expiryTime) {
  const expiryDate = new Date(expiryTime);
  const remainingTime = expiryTime - Date.now();
  return `アクセストークンの有効期限:${expiryDate.toLocaleString()}<br>
          残り ${Math.floor(remainingTime / 86400000)}日 
          ${Math.floor((remainingTime % 86400000) / 3600000)}時間 
          ${Math.floor((remainingTime % 3600000) / 60000)}分`;
}

// === 現在の認証状況を確認 ===
function checkAccess() {
  const service = getOAuthService();
  const props = PropertiesService.getScriptProperties();
  const expiryTime = Number(props.getProperty('token_expiry')) || 0;

  if (service.hasAccess()) {
    showDialog(`認証済みです。<br>${formatExpiryTime(expiryTime)}`, "認証状況");
  } else {
    showDialog("LOGILESS APIの認証が必要です。", "認証状況");
  }
}

// === 認証情報のリセット ===
function reset() {
  getOAuthService().reset();
  showDialog("認証をリセットしました。<br>初回認証からやり直してください。", "認証状況");
}

// === ダイアログ表示共通関数 ===
function showDialog(message, title = "通知") {
  SpreadsheetApp.getUi().showModalDialog(
    HtmlService.createHtmlOutput(`<p>${message}</p>`), title
  );
}

✅ 補足:最初に呼び出す関数

  • 認証を開始:authorize()

  • クライアントIDの登録:setClientCredentials()(一度だけ手動実行)

  • トークン有効期限チェック&更新:refreshAccessTokenFromMenu() またはトリガーで refreshAccessTokenOnTrigger()


次回予告:実際にLogiless APIを叩いてみる!

今回は「認証部分」までを紹介しましたが、次回は:

  • ステップ1:出荷実績 or 受注情報の取得

  • ステップ2:結果をスプレッドシートに書き出す

  • ステップ3:業務データとの突合や自動通知

など、「Logiless APIで何ができるか」具体的な活用例を深掘りしていきます。


まとめ:GAS × Logilessは社内DXの強力な武器

今回の認証構築で、「使ってみたいけど面倒そう…」という状態から一歩踏み出す準備が整いました。

次はこの認証基盤を活かして、

  • 日次の出荷レポートの自動生成

  • 返品状況のAPIチェック

  • ECサイトとの在庫連携

などなど、業務効率化・自動化の展開に進んでいきます。

それでは、また次の実験で!🦊


#ノーコード #GAS #業務自動化 #Logiless #100日チャレンジ #ChatGPTで業務効率化