秘匿情報を扱うkintoneプラグインを作成してみよう

著者名:akika( サイボウズ株式会社 (External link)

目次

はじめに

前回までプラグインの作り方の基本を学んできましたが、今回はプラグインのメリットのひとつ「プラグインで秘匿情報を扱う方法」を紹介します。
具体的には、次の3点を学習します。

  • 秘匿情報をプラグインの設定情報として保存する方法
  • 保存した秘匿情報を取得する方法
  • 保存した秘匿情報を使って外部APIのリクエストを送信する方法

プラグインで秘匿情報を扱うメリットとそのしくみは、次のページを参考してください。
kintoneプラグインのメリット:秘匿情報を隠蔽できるためセキュリティが向上する

サンプルプラグインのイメージ

今回は、レコードの編集画面で翻訳ボタンをクリックすると「原文」フィールドの内容を英訳して「訳文」フィールドに書き込むプラグインを作ります。
プラグインでは、翻訳サービスが提供するWeb APIを実行します。
このAPIの実行にはサービスの認証情報が必要ですが、プラグインで認証情報を隠すことで、アプリの利用者に認証情報を漏洩させることなく安全にWeb APIを実行します。

翻訳サービスは、DeepL社が提供している翻訳サービスの無料版を使います。
詳細はDeepLの公式サイトを参考してください。
DeepL API Free (External link)

準備するもの

DeepLアカウント作成

次の手順にしたがってDeepLアカウントを作成し、DeepL APIを呼び出すには必要なAPIキーを発行しておきます。

  1. 次のページからアカウントを登録します。
    DeepLアカウント登録 (External link)
  2. DeepLにログインします。
  3. 画面右上からプロフィールのアイコンをクリックし、「アカウント」をクリックします。
  4. 「APIキー」タブを開き、APIキー右側のアイコンをクリックしてAPIキーをコピーします。
    APIキーは後で使うので、メモしておきます。

kintoneアプリ作成

プラグインを動作確認するためのアプリを作成します。
以下のフィールドをアプリのフォームに配置します。
他のフィールドの配置は任意です。

フィールドタイプ フィールドコード フィールド名
文字列(複数行) source 原文
文字列(複数行) target 訳文
スペース なし
要素IDに「button-space」を設定します。
なし

ベースとなるプラグインの作成

これまで習ったことを復習しながら、ベースとなるプラグインを作成します。
まずは、「原文」「訳文」フィールドや「翻訳」ボタンを表示するスペースフィールドを設定画面から指定できるようにします。

  1. プラグインの設定画面のHTMLファイル(config.html)を作成します。
    コードは、後述の 設定画面のHTMLサンプルソースコード を参考してください。
    設定項目のスタイルには、51-modern-defaultを適用しています。
    51-modern-default

    項目名 タイプ 説明
    スペースフィールド ドロップダウン 「翻訳」ボタンを配置するためのスペースフィールドを指定します。
    スペースフィールドのみ選択可能です。
    原文フィールド ドロップダウン 原文フィールドを指定します。
    文字列(複数行)のみ選択可能です。
    訳文フィールド ドロップダウン 翻訳結果を保存するための訳文フィールドを指定します。
    DeepLキー テキストボックス DeepLアカウント作成でメモしたAPIキーを入力します。
    文字列(複数行)のみ選択可能です。
  2. 設定項目を制御するJavaScriptを記述し、config.jsとして保存します。
    ドロップダウンは、前回までに学習したkintone-config-helperでアプリのフィールド情報を取得する方法を使って、実際のアプリのフィールド情報を選択できるように実装しています。
    コードは、後述の 設定画面のJSサンプルソースコード を参考してください。

  3. desktop.jsという名前で中身が空白のカスタマイズファイルを作成します。

  4. プラグインのアイコンファイルを用意し、icon.pngとして保存します。
    お手軽にプラグインを作ってみたい方向けに、今回使ったアイコンファイルを貼っておきます。

  5. 最後に、マニフェストファイルを作成します。
    コードは後述の manifestのサンプルソースコード を参考してください。
    マニフェストファイルの書き方は次の記事を参考してください。
    プラグインを作成してみよう

最終的なファイル構造は次のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
kintoneProxySamplePlugin
├── css
│   └── 51-modern-default.css
├── js
│   ├── config.js -> プラグイン設定画面のJS
│   ├── desktop.js -> カスタマイズファイルJS
│   └── kintone-config-helper.js
├── html
│   └── config.html -> プラグイン設定画面のHTML
├── image
│   └── icon.png
└── manifest.json
設定画面のHTMLサンプルソースコード
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<section class="settings">
  <form id="submit-settings">
    <div class="term-setting">
      <label class="kintoneplugin-label" for="space-field">
        <span>スペースフィールド</span>
        <span class="kintoneplugin-require">*</span>
      </label>
      <div class="kintoneplugin-row">
        [翻訳]ボタンを配置するためのフィールドを選択してください。
      </div>
      <div class="kintoneplugin-select-outer">
        <div class="kintoneplugin-select">
          <select id="space-field">
            <option value="">-----</option>
          </select>
        </div>
      </div>
    </div>
    <div class="term-setting">
      <label class="kintoneplugin-label" for="source-field">
        <span>原文フィールド</span>
        <span class="kintoneplugin-require">*</span>
      </label>
      <div class="kintoneplugin-row">
        翻訳対象フィールドを選択してください。
      </div>
      <div class="kintoneplugin-select-outer">
        <div class="kintoneplugin-select">
          <select id="source-field">
            <option value="">-----</option>
          </select>
        </div>
      </div>
    </div>
    <div class="term-setting">
      <label class="kintoneplugin-label" for="target-field">
        <span>訳文フィールド</span>
      <span class="kintoneplugin-require">*</span>
      </label>
      <div class="kintoneplugin-row">
        翻訳結果を保存するフィールドを選択してください。
      </div>
      <div class="kintoneplugin-select-outer">
        <div class="kintoneplugin-select">
          <select id="target-field">
            <option value="">-----</option>
          </select>
        </div>
      </div>
    </div>
    <div class="term-setting">
      <label class="kintoneplugin-label" for="api-key">
        <span>DeepL キー</span>
        <span class="kintoneplugin-require">*</span>
      </label>
      <div class="kintoneplugin-row">
        API キーを入力してください。
      </div>
      <input type="text" id="token" class="api-key kintoneplugin-input-text" />
    </div>
    <div class="kintoneplugin-row">
      <button type="button" id="cancel-button" class="kintoneplugin-button-dialog-cancel">キャンセル</button>
      <button id="save-button" class="kintoneplugin-button-dialog-ok">保存</button>
    </div>
  </form>
</section>
設定画面のJSサンプルソースコード
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
(async (PLUGIN_ID) => {
  'use strict';

  // XSSを防ぐためのエスケープ処理
  const escapeHtml = (str) => {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
      .replace(/\n/g, '&#xA;');
  };

  // 「翻訳ボタン」を配置するためのスペース要素のセレクタボックスの項目を作成
  const createOptinesForSpace = async () => {
    let options = [];

    // kintoneフォームからスペースフィールドの要素を取得
    const spaceFields = await KintoneConfigHelper.getFields('SPACER');
    if (spaceFields) {
      spaceFields.forEach(field => {
        const option = document.createElement('option');
        option.value = field.elementId;
        option.textContent = field.elementId;
        options = options.concat(option);
      });
    }
    return options;
  };

  // スペースフィールドのセレクタボックスを作成
  const spaceField = document.getElementById('space-field');
  const spaceOptins = await createOptinesForSpace();
  spaceOptins.forEach(option => {
    spaceField.appendChild(option);
  });

  // 翻訳リソースフィールドとターゲットフィールドのセレクタボックスの項目を作成
  const getKintoneFiled = async () => {
    let options = [];

    // kintoneフォームからテキスト複数行フィールドの要素を取得し、セレクタオプションにセット
    const textFields = await KintoneConfigHelper.getFields('MULTI_LINE_TEXT');
    if (textFields) {
      textFields.forEach(field => {
        const option = document.createElement('option');
        option.value = field.code;
        option.textContent = field.label;
        options = options.concat(option);
      });
    }
    return options;
  };

  // 翻訳リソースフィールドとターゲットフィールドのセレクタボックスを作成
  const textOptions = await getKintoneFiled();
  const sourceField = document.getElementById('source-field');
  const targetField = document.getElementById('target-field');
  textOptions.forEach(option => {
    const sourceFieldOption = option.cloneNode(true);
    const targetFieldOptine = option.cloneNode(true);
    sourceField.appendChild(sourceFieldOption);
    targetField.appendChild(targetFieldOptine);
  });

  // 前回保存した設定情報を初期値として設定項目にセットする
  const config = kintone.plugin.app.getConfig(PLUGIN_ID);
  const setConfigValue = (field, options, element) => {
    const selectedOption = options.find(
      (option) => option.value === config[field]
    );
    if (selectedOption) {
      element.value = config[field];
    }
  };
  setConfigValue('sourceFieldValue', textOptions, sourceField);
  setConfigValue('targetFieldValue', textOptions, targetField);
  setConfigValue('spaceFieldID', spaceOptins, spaceField);

  // 「保存」ボタンと「キャンセル」ボタンをクリックした時の処理
  const appId = kintone.app.getId();
  const form = document.getElementById('submit-settings');
  const cancelButton = document.getElementById('cancel-button');
  // 設定情報を保存
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    const newConfig = {
      sourceFieldValue: escapeHtml(sourceField.value),
      targetFieldValue: escapeHtml(targetField.value),
      spaceFieldID: escapeHtml(spaceField.value)
    };

    if (spaceField.value === '' || sourceField.value === '' || targetField.value === '') {
      alert('未入力項目があります。');
    } else {
      kintone.plugin.app.setConfig(newConfig, () => {
        window.location.href = `/k/admin/app/flow?app=${appId}`;
      });
    }
  });
  cancelButton.addEventListener('click', () => {
    window.location.href = `../../${appId}/plugin/`;
  });
})(kintone.$PLUGIN_ID);
manifestのサンプルソースコード
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "manifest_version": 1,
  "version": 1,
  "type": "APP",
  "desktop": {
    "js": [
      "js/desktop.js"
    ]
  },
  "icon": "image/icon.png",
  "config": {
    "html": "html/config.html",
    "js": [
      "js/kintone-config-helper.js",
      "js/config.js"
    ],
    "css": [
      "css/51-modern-default.css"
    ]
  },
  "name": {
    "en": "machine translation plugin",
    "ja": "機械翻訳プラグイン"
  },
  "description": {
    "en": "machine translation plugin",
    "ja": "機械翻訳プラグイン"
  }
}

秘匿情報をプラグインの設定情報として保存する方法

利用するkintone JavaScript API

これまで、プラグインの設定情報を保存するにはkintone.plugin.app.setConfig()を使っていました。
このAPIで保存した情報は、kintone.plugin.app.getConfig()を使って取り出すことができます。
kintone.plugin.app.getConfig()はレコードの画面でも実行できるため、アプリの利用者にも保存した設定情報が見えてしまいます。
そのため秘匿情報の保存には使えません。

その代わり、kintone.plugin.app.setProxyConfig()というkintone JavaScript APIを利用します。
外部APIの実行に必要な情報をプラグインへ保存する kintone.plugin.app.setProxyConfig()

このAPIで保存した情報は、kintone.plugin.app.getProxyConfig()というAPIで取得できます。
kintone.plugin.app.getProxyConfig()はプラグインの設定画面でだけ実行できるAPIなので、プラグインの設定画面にアクセス権がないアプリの利用者は、保存した情報を見ることができません。

APIドキュメントによると、kintone.plugin.app.setProxyConfig()に指定する引数は次のとおりです。

kintone.plugin.app.setProxyConfig(url, method, headers, data, successCallback)

  • 第1引数:Web APIのURL(文字列)
  • 第2引数:HTTPメソッド(文字列)
  • 第3引数:リクエストヘッダー(オブジェクト)
  • 第4引数:リクエストボディ(オブジェクト)
  • 第5引数:kintone.plugin.app.setProxyConfig()の実行が終わった後の処理

DeepL APIのリクエスト情報

リクエストの内容や、認証情報の指定方法はWeb APIによって異なります。
今回使うDeepL APIに必要な情報を調べてみましょう。

DeepL API (External link) のドキュメントによると、このAPIを実行するには、次の情報が必要です。

項目
URL https://api-free.deepl.com/v2/translate
メソッド POST
リクエストヘッダー
  • Authorization: DeepL-Auth-Key YOUR_AUTH_KEY
  • Content-Type: application/json
リクエストボディ
  • text:配列形式の翻訳ソース
  • target_lang:翻訳ターゲット言語

kintone.plugin.app.setProxyConfig()の引数の指定方法に合わせると、コードは次のようになります。
実際のリクエストはカスタマイズファイルから行うので、リクエストボディには空のオブジェクトを指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 外部APIの情報を定義
const apiUrl = 'https://api-free.deepl.com/v2/translate';
const method = 'POST';

// テキストボックス要素の取得
const apiKeyField = document.getElementById('token');

const auth = escapeHtml(apiKeyField.value);
const apiHeader = {
  Authorization: `DeepL-Auth-Key ${auth}`,
  'Content-Type': 'application/json'
};
const apiBody = {};
const successCallback = () => {};
if (apiKeyField.value === '') {
  alert('未入力項目があります。');
  return;
}
kintone.plugin.app.setProxyConfig(apiUrl, method, apiHeader, apiBody, successCallback);

では、ベースとなるプラグインにkintone.plugin.app.setProxyConfig()の処理を追加しましょう。
config.jsを開いて、81行目〜85行目を追記します。

また、秘匿情報ではない設定情報を保存処理は、kintone.plugin.app.setProxyConfig()の実行が終わって行います。
そのため、successCallback()の中でkintone.plugin.app.setConfig()を実行するように、92行目〜121行目のように変更します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
(async (PLUGIN_ID) => {
  'use strict';

  // XSSを防ぐためのエスケープ処理
  const escapeHtml = (str) => {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
      .replace(/\n/g, '&#xA;');
  };

  // 「翻訳ボタン」を配置するためのスペース要素のセレクタボックスの項目を作成
  const createOptinesForSpace = async () => {
    let options = [];

    // kintoneフォームからスペースフィールドの要素を取得
    const spaceFields = await KintoneConfigHelper.getFields('SPACER');
    if (spaceFields) {
      spaceFields.forEach(field => {
        const option = document.createElement('option');
        option.value = field.elementId;
        option.textContent = field.elementId;
        options = options.concat(option);
      });
    }
    return options;
  };

  // スペースフィールドのセレクタボックスを作成
  const spaceField = document.getElementById('space-field');
  const spaceOptins = await createOptinesForSpace();
  spaceOptins.forEach(option => {
    spaceField.appendChild(option);
  });

  // 翻訳リソースフィールドとターゲットフィールドのセレクタボックスの項目を作成
  const getKintoneFiled = async () => {
    let options = [];

    // kintoneフォームからテキスト複数行フィールドの要素を取得し、セレクタオプションにセット
    const textFields = await KintoneConfigHelper.getFields('MULTI_LINE_TEXT');
    if (textFields) {
      textFields.forEach(field => {
        const option = document.createElement('option');
        option.value = field.code;
        option.textContent = field.label;
        options = options.concat(option);
      });
    }
    return options;
  };

  // 翻訳リソースフィールドとターゲットフィールドのセレクタボックスを作成
  const textOptions = await getKintoneFiled();
  const sourceField = document.getElementById('source-field');
  const targetField = document.getElementById('target-field');
  textOptions.forEach(option => {
    const sourceFieldOption = option.cloneNode(true);
    const targetFieldOptine = option.cloneNode(true);
    sourceField.appendChild(sourceFieldOption);
    targetField.appendChild(targetFieldOptine);
  });

  // 前回保存した設定情報を初期値として設定項目にセットする
  const config = kintone.plugin.app.getConfig(PLUGIN_ID);
  const setConfigValue = (field, options, element) => {
    const selectedOption = options.find(
      (option) => option.value === config[field]
    );
    if (selectedOption) {
      element.value = config[field];
    }
  };
  setConfigValue('sourceFieldValue', textOptions, sourceField);
  setConfigValue('targetFieldValue', textOptions, targetField);
  setConfigValue('spaceFieldID', spaceOptins, spaceField);

  // テキストボックス要素の取得
  const apiKeyField = document.getElementById('token');
  // 外部APIの情報を定義
  const apiUrl = 'https://api-free.deepl.com/v2/translate';
  const method = 'POST';

  // 「保存」ボタンと「キャンセル」ボタンをクリックした時の処理
  const appId = kintone.app.getId();
  const form = document.getElementById('submit-settings');
  const cancelButton = document.getElementById('cancel-button');
  // 設定情報を保存
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    const auth = escapeHtml(apiKeyField.value);
    const apiHeader = {
      Authorization: `DeepL-Auth-Key ${auth}`,
      'Content-Type': 'application/json'
    };
    const apiBody = {};
    const successCallback = () => {
      const newConfig = {
        sourceFieldValue: escapeHtml(sourceField.value),
        targetFieldValue: escapeHtml(targetField.value),
        spaceFieldID: escapeHtml(spaceField.value)
      };

      if (spaceField.value === '' || sourceField.value === '' || targetField.value === '') {
        alert('未入力項目があります。');

      } else {
        kintone.plugin.app.setConfig(newConfig, () => {
          window.location.href = `/k/admin/app/flow?app=${appId}`;
        });
      }
    };
    if (apiKeyField.value === '') {
      alert('未入力項目があります。');
      return;
    }
    kintone.plugin.app.setProxyConfig(apiUrl, method, apiHeader, apiBody, successCallback);
  });
  cancelButton.addEventListener('click', () => {
    window.location.href = `../../${appId}/plugin/`;
  });
})(kintone.$PLUGIN_ID);

保存した秘匿情報を取得する方法

設定画面を表示したときにkintone.plugin.app.setProxyConfig()で保存した内容が表示されるよう、保存した設定情報を取得し設定項目の初期値としてセットしましょう。

保存した秘匿情報を取得するには、次のAPIを利用します。
外部APIの実行に必要な情報を取得する kintone.plugin.app.getProxyConfig()

上記APIドキュメントによると、引数に次の2つを指定します。

したがって、コードは次のようになります。

1
const proxyConfig = kintone.plugin.app.getProxyConfig(apiUrl, method);

続いて、取得した設定情報からDeepL APIキーを取り出します。
ヘッダー情報はproxyConfigheadersプロパティに保存されています。
データ構造は次のとおりです。

1
2
3
4
{
  "Authorization": 'DeepL-Auth-Key YOUR_AUTH_KEY',
  "Content-Type": 'application/json'
}

ここでは、DeepL-Auth-Key YOUR_AUTH_KEYに注目してください。
Authorizationの値には、APIキーだけではなく「DeepL-Auth-Key」という文字列が先頭についています。
そのため、次のようにAPIキーだけを取り出す処理が必要になります。

1
const deepLApiToken = proxyConfig ? proxyConfig.headers.Authorization.split(' ')[1] : '';

最後に、テキストボックスのHTML要素にAPIキーをセットします。

1
2
3
if (deepLApiToken) {
  apiKeyField.value = deepLApiToken;
}

最終的なコードは次のようになります。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
 * handle sensitive data sample code
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
*/
(async (PLUGIN_ID) => {
  'use strict';

  // XSSを防ぐためのエスケープ処理
  const escapeHtml = (str) => {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
      .replace(/\n/g, '&#xA;');
  };

  // 「翻訳ボタン」を配置するためのスペース要素のセレクタボックスの項目を作成
  const createOptinesForSpace = async () => {
    let options = [];

    // kintoneフォームからスペースフィールドの要素を取得
    const spaceFields = await KintoneConfigHelper.getFields('SPACER');
    if (spaceFields) {
      spaceFields.forEach(field => {
        const option = document.createElement('option');
        option.value = field.elementId;
        option.textContent = field.elementId;
        options = options.concat(option);
      });
    }
    return options;
  };

  // スペースフィールドのセレクタボックスを作成
  const spaceField = document.getElementById('space-field');
  const spaceOptins = await createOptinesForSpace();
  spaceOptins.forEach(option => {
    spaceField.appendChild(option);
  });

  // 翻訳リソースフィールドとターゲットフィールドのセレクタボックスの項目を作成
  const getKintoneFiled = async () => {
    let options = [];

    // kintoneフォームからテキスト複数行フィールドの要素を取得し、セレクタオプションにセット
    const textFields = await KintoneConfigHelper.getFields('MULTI_LINE_TEXT');
    if (textFields) {
      textFields.forEach(field => {
        const option = document.createElement('option');
        option.value = field.code;
        option.textContent = field.label;
        options = options.concat(option);
      });
    }
    return options;
  };

  // 翻訳リソースフィールドとターゲットフィールドのセレクタボックスを作成
  const textOptions = await getKintoneFiled();
  const sourceField = document.getElementById('source-field');
  const targetField = document.getElementById('target-field');
  textOptions.forEach(option => {
    const sourceFieldOption = option.cloneNode(true);
    const targetFieldOptine = option.cloneNode(true);
    sourceField.appendChild(sourceFieldOption);
    targetField.appendChild(targetFieldOptine);
  });

  // 前回保存した設定情報を初期値として設定項目にセットする
  const config = kintone.plugin.app.getConfig(PLUGIN_ID);
  const setConfigValue = (field, options, element) => {
    const selectedOption = options.find(
      (option) => option.value === config[field]
    );
    if (selectedOption) {
      element.value = config[field];
    }
  };
  setConfigValue('sourceFieldValue', textOptions, sourceField);
  setConfigValue('targetFieldValue', textOptions, targetField);
  setConfigValue('spaceFieldID', spaceOptins, spaceField);

  // テキストボックス要素の取得
  const apiKeyField = document.getElementById('token');
  // 外部APIの情報を定義
  const apiUrl = 'https://api-free.deepl.com/v2/translate';
  const method = 'POST';

  // APIリクエストの設定情報を取得し、初期値としてセット
  const proxyConfig = kintone.plugin.app.getProxyConfig(apiUrl, method);
  const deepLApiToken = proxyConfig ? proxyConfig.headers.Authorization.split(' ')[1] : '';
  if (deepLApiToken) {
    apiKeyField.value = deepLApiToken;
  }

  // 「保存」ボタンと「キャンセル」ボタンをクリックした時の処理
  const appId = kintone.app.getId();
  const form = document.getElementById('submit-settings');
  const cancelButton = document.getElementById('cancel-button');
  // 設定情報を保存
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    const auth = escapeHtml(apiKeyField.value);
    const apiHeader = {
      Authorization: `DeepL-Auth-Key ${auth}`,
      'Content-Type': 'application/json'
    };
    const apiBody = {};
    const successCallback = () => {
      const newConfig = {
        sourceFieldValue: escapeHtml(sourceField.value),
        targetFieldValue: escapeHtml(targetField.value),
        spaceFieldID: escapeHtml(spaceField.value)
      };

      if (spaceField.value === '' || sourceField.value === '' || targetField.value === '') {
        alert('未入力項目があります。');

      } else {
        kintone.plugin.app.setConfig(newConfig, () => {
          window.location.href = `/k/admin/app/flow?app=${appId}`;
        });
      }
    };
    if (apiKeyField.value === '') {
      alert('未入力項目があります。');
      return;
    }
    kintone.plugin.app.setProxyConfig(apiUrl, method, apiHeader, apiBody, successCallback);
  });
  cancelButton.addEventListener('click', () => {
    window.location.href = `../../${appId}/plugin/`;
  });
})(kintone.$PLUGIN_ID);

保存した秘匿情報を使って外部APIのリクエストを送信する方法

次はカスタマイズファイルのdesktop.jsを編集していきます。
カスタマイズファイルでは、次の機能を実装します。

  • プラグインの設定情報を使って、カスタマイズを適用するフィールド情報を取得します。
  • 「翻訳」ボタンをクリックしたときに、DeepL APIを実行し、「原文」フィールドの内容を翻訳して「訳文」フィールドにセットします。

プラグインの設定情報を使ってカスタマイズを適用するフィールド情報を取得する

これまでのチュートリアルで学習した内容の復習です。
プラグインの設定情報を取得して、「原文」「訳文」フィールドのフィールドコードや「翻訳」ボタンを表示するスペースフィールドの情報を取得しましょう。
「翻訳」ボタンを表示するスペースフィールドには、「翻訳」ボタンを表示し、クリックイベントのイベントハンドラーを登録しておきます。
忘れてしまった人は、次のページで復習しましょう。
プラグインを作成してみよう

また、ボタンの作り方は次のページを参考してください。
レコード詳細画面にボタンを配置してみよう

コードは次のとおりになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
((PLUGIN_ID) => {
  'use strict';

  // プラグインの設定情報を取得
  const config = kintone.plugin.app.getConfig(PLUGIN_ID);
  if (!config) {
    return;
  }
  const {sourceFieldValue, targetFieldValue, spaceFieldID} = config;

  kintone.events.on('app.record.edit.show', (event) => {
    const source = event.record[sourceFieldValue];

    // [翻訳]ボタンを用意
    const button = document.createElement('button');
    button.id = 'button-space';
    button.textContent = '翻訳';
    // ボタンのクリックイベント
    button.onclick = async () => {
      // 外部APIのリクエストを送信する処理はここに記載する
    };
    kintone.app.record
      .getSpaceElement(spaceFieldID)
      .appendChild(button);
    return event;
  });
})(kintone.$PLUGIN_ID);

DeepLのAPIを実行して「原文」の内容を翻訳する

最後に、DeepLのAPIを実行して「原文」の内容を翻訳する機能を実装します。
DeepLのAPIを実行、すなわち外部のWeb APIを実行するには、次のkintone JavaScript APIを利用します。
プラグインから外部APIを実行する kintone.plugin.app.proxy()

引数として、今回は次の情報を指定します。

  • pluginId:当プラグインのID
  • url:Web APIのURL
    今回は、”https://api-free.deepl.com/v2/translate”です。
  • method:HTTPメソッド
    今回は”POST”です。
  • data:リクエストボディ
    今回は、 DeepL APIリクエスト (External link) dataです。

上記の引数を指定してAPIを実行すると、プロキシサーバー上で、DeepLのAPIが実行されます。
このとき、kintone.plugin.app.setProxyConfig()を使って保存した内容と、kintone.plugin.app.proxy()で指定した内容が比較されます。
次の3つの情報が一致すると、リクエストヘッダーとリクエストボディが付与されます。

  • プラグインID
  • URL
  • HTTPメソッド

レスポンスとして返ってきた翻訳結果を「訳文」フィールドにセットします。

最終的なコードは次のとおりです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
 * handle sensitive data sample code
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
*/
((PLUGIN_ID) => {
  'use strict';

  // プラグインの設定情報を取得
  const config = kintone.plugin.app.getConfig(PLUGIN_ID);
  if (!config) {
    return;
  }
  const {sourceFieldValue, targetFieldValue, spaceFieldID} = config;

  kintone.events.on('app.record.edit.show', (event) => {
    const source = event.record[sourceFieldValue];
    // 外部APIパラメータを定義
    const apiUrl = 'https://api-free.deepl.com/v2/translate';
    const method = 'POST';
    const headers = {};
    const data = {
      text: [source.value],
      target_lang: 'EN',
      source_lang: 'JA'
    };

    // [翻訳]ボタンを用意
    const button = document.createElement('button');
    button.id = 'button-space';
    button.textContent = '翻訳';
    // ボタンのクリックイベント
    button.onclick = async () => {
      // kintone.plguin.app.proxyで外部APIを実行
      const [result, statusCode] = await kintone.plugin.app
        .proxy(PLUGIN_ID, apiUrl, method, headers, data);
      // API実行が失敗した場合
      if (statusCode !== 200) {
        console.error(JSON.parse(result));
      }
      // API実行が成功した場合
      const objResult = JSON.parse(result);
      const translation = objResult.translations[0].text;
      const response = kintone.app.record.get();
      response.record[targetFieldValue].value = translation;
      kintone.app.record.set(response);
    };
    kintone.app.record
      .getSpaceElement(spaceFieldID)
      .appendChild(button);
    return event;
  });
})(kintone.$PLUGIN_ID);

動作確認

プラグインの動作確認

作ったプラグインが正しく動作するか確認しましょう。
作ったプラグインファイルをパッケージングし、「kintoneシステム管理」からプラグインを読み込み、 kintoneアプリ作成 で作ったアプリに追加します。
次の2点を確認します。

  1. 設定画面では次のことを確認します。
    • 複数選択フィールドの一覧が表示され、選択できること
    • 保存したプラグインの情報が初期値としてセットされること

  2. レコード編集画面で「翻訳」ボタンをクリックすると、原文フィールドの内容の英訳が訳文フィールドに書き込まれること

秘密情報が見えないことを確認

アプリの利用者には、プラグインの設定画面で保存したAPIキーが見えないことを確認してみましょう。

kintone.plugin.app.proxy.getConfig() で秘匿情報が見えないこと

レコードの編集画面を開いて、ブラウザーの「コンソール」タブを開きます。
次のコードを貼り付けてkintone.plugin.app.proxy.getConfig()を実行してみましょう。
プラグインIDは後述の 「ネットワーク」タブのリクエスト情報 から確認できます。

1
2
3
4
5
const pluginId = '今回のプラグインのID';
const apiUrl = 'https://api-free.deepl.com/v2/translate';
const method = 'POST';
const proxyConfig = kintone.plugin.app.getProxyConfig(pluginId, apiUrl, method);
console.log(proxyConfig);

次のように、実行結果がnullになり、アプリの利用者には秘匿情報が見えないことを確認できました。

ネットワークタブのリクエストでも見えないこと

レコードの編集画面でブラウザーの「ネットワーク」タブを開きます。
「翻訳」ボタンをクリックして表示されたcall.json?文字列...をクリックします。
すると、DeepL APIを実行したときのリクエスト内容が表示されます。
リクエスト内容から、ヘッダーに付与されるAPIキーが表示されていないことを確認できます。

詳細な検証方法は、 kintoneプラグインで秘匿情報を隠す〜実践編〜 を参考してください。

まとめ

今回は、プラグインで認証情報などの秘匿情報を隠す方法を紹介しました。
より安全に外部サービスと連携したい方、ぜひプラグインを検討してみてください。

information

このTipsは、2024年4月版kintoneで動作を確認しています。