トークン必要なAPIをGAS(Google Apps Script)で作ってみた【コピペ可】

# 背景
公開したソースコードに1箇所だけワンチャン悪用からの責任逃れで見せたくない部分があったからAPIにした。
更に誰にでも実行できるのもワンチャン悪用責任追及怠いから簡単なトークンを実装した。

# プロジェクトの作成

下記の記事を参照しました。
プロジェクトの作成まで行けばOKです。


https://qiita.com/riversun/items/c924cfe70e16ee3fe3ba

## トラブルシューティング
GASが開けない場合は、chromeのユーザープロファイルに問題がないか確認してください。

## GASの利用制限について
一応紹介しておきます。

https://qiita.com/kakakaori830/items/84a7dcba2d6119ed320a

# Tokenの実装
さくっと実装したかったのでコーディングはAI君に一任しました。

## Tokenを作成する
GASプロジェクト > プロジェクト設定 > スクリプトプロパティ
ここに文字列置いとけばTokenになりますね。
ですが、手動でやるのは怠いので機械君にやってもらいましょう。
下記の関数を新しく作成した.gsファイルにぶちこんでください。

``` js
function saveNewToken() {
  var scriptProperties = PropertiesService.getScriptProperties();
  
  // 既存のトークンを取得
  var tokens = JSON.parse(scriptProperties.getProperty('API_TOKENS') || '{}');
  
  // 新しいトークンを生成
  var newToken = generateRandomToken(256);
  
  // 新しいトークンのインデックス番号を決める（次の番号を自動生成）
  var tokenIndex = 'token' + (Object.keys(tokens).length + 1);
  
  // トークンをインデックス番号で保存
  tokens[tokenIndex] = newToken;
  
  // 保存したトークンを再度プロパティサービスに保存
  scriptProperties.setProperty('API_TOKENS', JSON.stringify(tokens));
  
  Logger.log('New Token: ' + newToken);
}

function generateRandomToken(length) {
  var charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@$%';
  var token = '';
  
  // 指定されたlengthに基づいてランダムな文字列を生成
  for (var i = 0; i < length; i++) {
    var randomIndex = Math.floor(Math.random() * charset.length);
    token += charset[randomIndex];
  }
  
  // UUIDを生成し、トークンの末尾に追加
  var uuid = Utilities.getUuid().replace(/-/g, '');  // UUIDから'-'を取り除く
  token += uuid;
  
  return token;
}
```

## トークンを共有する
作成したトークンは共有しないと意味がないです。
が、GASプロジェクト内で確認するにはUIが終わっています。
なので下記の関数を共有時に実行しましょう。

``` js
function getTokens() {
  var scriptProperties = PropertiesService.getScriptProperties();
  
  // プロパティからトークンの情報を取得
  var tokens = JSON.parse(scriptProperties.getProperty('API_TOKENS') || '{}');
  
  // トークンをキーと値のペアで整形
  var tokensList = [];
  for (var key in tokens) {
    tokensList.push(key + ": " + tokens[key]);
  }
  
  // トークンリストを表示
  Logger.log(tokensList.join("\n"));
  
  // トークンを返す（共有しやすい形式で返却）
  return tokensList.join("\n");
}
```

## パラメータにTokenが含まれているか認証する
doGet / doPost関数の中に下記の処理をコピペしてください。
受信したパラメータに正しいTokenがなければ404行きです。

``` js
  var scriptProperties = PropertiesService.getScriptProperties();
  // プロパティからトークンの情報を取得
  var tokens = JSON.parse(scriptProperties.getProperty('API_TOKENS') || '{}');
  
  var token = e.parameter.token; // パラメータからトークンを取得
  
  // トークンが存在しない、または一致しない場合、アクセスを拒否
  if (!token || !Object.values(tokens).includes(token)) {
    return ContentService.createTextOutput(
      JSON.stringify({ "error": "Unauthorized access. Invalid or missing token." })
    ).setMimeType(ContentService.MimeType.JSON);
  }
```

## トークンを削除する
共有したTokenは何らかの理由で削除したい日が来ると思います。
そういうときは下記の関数を利用してください。

``` js
function deleteToken(tokenKey='token1') {
  var scriptProperties = PropertiesService.getScriptProperties();
  
  // プロパティからトークンの情報を取得
  var tokens = JSON.parse(scriptProperties.getProperty('API_TOKENS') || '{}');
  
  // 指定されたトークンキーが存在するか確認
  if (tokens[tokenKey]) {
    // トークンを削除
    delete tokens[tokenKey];
    
    // インデックスを1から始まる連番に修正
    var updatedTokens = {};
    var index = 1; // インデックスは1から始める
    
    // トークンを新しいインデックス順で再構築
    for (var key in tokens) {
      updatedTokens['token' + index] = tokens[key];
      index++;
    }

    // 修正後のトークン情報をプロパティに保存
    scriptProperties.setProperty('API_TOKENS', JSON.stringify(updatedTokens));
    
    Logger.log('トークン "' + tokenKey + '" を削除しました。');
  } else {
    Logger.log('指定されたトークンキーは存在しません: ' + tokenKey);
  }
}
```

# APIのデプロイ

下記の記事を参照してください。

https://qiita.com/riversun/items/c924cfe70e16ee3fe3ba#web-api%E3%81%AE%E5%85%AC%E9%96%8B

# 作ったAPIの呼び出し方
getリクエストの例 (python)
``` python
import requests

# GASプロジェクトのデプロイURL
url = 'https://script.google.com/macros/s/dummy'

params = {
    'token': 'dummy', 
}

headers = {
}

response = requests.get(url, headers=headers, params=params)
```

# おわりに
(今思えばスプレッドシートで管理した方が良かったかも。。まあ単純明快だから良いよね)