javascript - 在 Google AppS 脚本用户属性中存储 API key 和 secret

标签 javascript google-apps-script properties user-input api-key

我对 Google AppScript 还很陌生,正在尝试编写自定义 REST API 的连接器。对于该 API,我需要一个 API key (或 secret ),即每个用户的 key 。由于在脚本中以纯文本形式存储 secret 并不是最好的主意,因此我想将其存储在 Google PropertyService 中并从那里检索它。像这样:

var userProperties = PropertiesService.getUserProperties();
var apiKey = userProperties.getProperty('MY_SECRET')

但我不明白的是,用户如何才能首先存储 key ?我没有找到用户(在本例中为我)可以查看或编辑属性的任何位置。然后我发现这个不错introduction to user properties在脚本容器中创建一个菜单,允许用户手动输入密码。

const API_KEY = 'API_KEY';

var ui = SpreadsheetApp.getUi();
var userProperties = PropertiesService.getUserProperties();


function onOpen(){
  ui.createMenu('API Keys')
    .addItem('Set API Key', 'userPromptApiKey')
    .addItem('Delete API Key', 'deleteApiKey')
  .addToUi();
}


function userPromptApiKey(){
  var userValue = ui.prompt('API Key ', ui.ButtonSet.OK);
  // ToDo: add current key to the prompt
  userProperties.setProperty(API_KEY, userValue.getResponseText());
}


function deleteApiKey(){
  userProperties.deleteProperty(API_KEY)
}

问题是,我的脚本没有绑定(bind)到任何容器(没有电子表格,没有文档)。相反,我想稍后在 Google DataStudio 中使用它。这就是为什么

SpreadsheetApp.getUi();

不起作用。关于如何处理这个问题有什么想法或建议吗?还有其他推荐的方法来处理这些 secret 吗?

最佳答案

现在,几周后我学到了很多东西。首先,您需要区分 UI 和逻辑脚本。其次,是容器绑定(bind)的脚本还是独立的脚本。

容器绑定(bind)脚本绑定(bind)到 Google 电子表格、Google 文档或允许用户交互的任何其他 UI。在这种情况下,您可以访问代码中的 UI 并向 UI 添加自定义菜单,一旦用户单击该菜单,该菜单将调用脚本中的方法。缺点是您需要知道它是电子表格还是文档,因为 UI 类不同。您还需要指示用户使用自定义菜单输入他或她的凭据。有一个very nice instruction在线的。下面的代码是受指令启发而截取的。确保为 onOpen 创建触发器。

var ui = SpreadsheetApp.getUi();
var userProperties = PropertiesService.getUserProperties();

const API_KEY = 'api.key';

function onOpen(){
  ui.createMenu('Credentials & Authentication')
    .addItem('Set API key', 'setKey')
    .addItem('Delete API key', 'resetKey')
    .addItem('Delete all credentials', 'deleteAll')
  .addToUi();
}

function setKey(){
  var scriptValue = ui.prompt('Please provide your API key.' , ui.ButtonSet.OK);
  userProperties.setProperty(API_KEY, scriptValue.getResponseText());
}

function resetKey(){
  userProperties.deleteProperty(API_KEY);
}

function deleteAll(){
  userProperties.deleteAllProperties();
}

对于独立脚本,您需要找到任何其他方式来连接到 UI。在我的情况下,我正在实现 custom connector for Google Data Studio其中有 a very nice example也在线。有一个相当详细的instruction on authentication和一个API reference on authentication以及。这个custom connector for Kaggle也非常有帮助。它在 Google Data Studio GitHub 上开源。 。以下演示代码的灵感来自于这些示例。查看 getCredentialsvalidateCredentialsgetAuthTyperesetAuthisAuthValidsetCredentials

var cc = DataStudioApp.createCommunityConnector();

const URL_DATA = 'https://www.myverysecretdomain.com/api';
const URL_PING = 'https://www.myverysecretdomain.com/ping';
const AUTH_USER = 'auth.user'
const AUTH_KEY = 'auth.key';
const JSON_TAG = 'user';

String.prototype.format = function() {
  // https://coderwall.com/p/flonoa/simple-string-format-in-javascript
  a = this;
  for (k in arguments) {
    a = a.replace("{" + k + "}", arguments[k])
  }
  return a
}

function httpGet(user, token, url, params) {
  try {
    // this depends on the URL you are connecting to
    var headers = {
      'ApiUser': user,
      'ApiToken': token,
      'User-Agent': 'my super freaky Google Data Studio connector'
    };

    var options = {
      headers: headers
    };

    if (params && Object.keys(params).length > 0) {
      var params_ = [];
      for (const [key, value] of Object.entries(params)) {
        var value_ = value;
        if (Array.isArray(value))
          value_ = value.join(',');

        params_.push('{0}={1}'.format(key, encodeURIComponent(value_)))
      }

      var query = params_.join('&');
      url = '{0}?{1}'.format(url, query);
    }

    var response = UrlFetchApp.fetch(url, options);

    return {
      code: response.getResponseCode(),
      json: JSON.parse(response.getContentText())
    }  
  } catch (e) {
    throwConnectorError(e);
  }
}

function getCredentials() {
  var userProperties = PropertiesService.getUserProperties();
  return {
    username: userProperties.getProperty(AUTH_USER),
    token: userProperties.getProperty(AUTH_KEY)
  }
}

function validateCredentials(user, token) {
  if (!user || !token) 
    return false;

  var response = httpGet(user, token, URL_PING);

  if (response.code == 200)
    console.log('API key for the user %s successfully validated', user);
  else
    console.error('API key for the user %s is invalid. Code: %s', user, response.code);

  return response;
}  

function getAuthType() {
  var cc = DataStudioApp.createCommunityConnector();
  return cc.newAuthTypeResponse()
    .setAuthType(cc.AuthType.USER_TOKEN)
    .setHelpUrl('https://www.myverysecretdomain.com/index.html#authentication')
    .build();
}

function resetAuth() {
  var userProperties = PropertiesService.getUserProperties();
  userProperties.deleteProperty(AUTH_USER);
  userProperties.deleteProperty(AUTH_KEY);

  console.info('Credentials have been reset.');
}

function isAuthValid() {
  var credentials = getCredentials()
  if (credentials == null) {
    console.info('No credentials found.');
    return false;
  }

  var response = validateCredentials(credentials.username, credentials.token);
  return (response != null && response.code == 200);
}

function setCredentials(request) {
  var credentials = request.userToken;
  var response = validateCredentials(credentials.username, credentials.token);

  if (response == null || response.code != 200) return { errorCode: 'INVALID_CREDENTIALS' };

  var userProperties = PropertiesService.getUserProperties();
  userProperties.setProperty(AUTH_USER, credentials.username);
  userProperties.setProperty(AUTH_KEY, credentials.token);

  console.info('Credentials have been stored');

  return {
    errorCode: 'NONE'
  };
}

function throwConnectorError(text) {
  DataStudioApp.createCommunityConnector()
    .newUserError()
    .setDebugText(text)
    .setText(text)
    .throwException();
}

function getConfig(request) {
  // ToDo: handle request.languageCode for different languages being displayed
  console.log(request)

  var params = request.configParams;
  var config = cc.getConfig();

  // ToDo: add your config if necessary

  config.setDateRangeRequired(true);
  return config.build();
}

function getDimensions() {
  var types = cc.FieldType;

  return [
    {
      id:'id',
      name:'ID',
      type:types.NUMBER
    },
    {
      id:'name',
      name:'Name',
      isDefault:true,
      type:types.TEXT
    },
    {
      id:'email',
      name:'Email',
      type:types.TEXT
    }
  ];
}

function getMetrics() {
  return [];
}

function getFields(request) {
  Logger.log(request)

  var fields = cc.getFields();

  var dimensions = this.getDimensions();
  var metrics = this.getMetrics();
  dimensions.forEach(dimension => fields.newDimension().setId(dimension.id).setName(dimension.name).setType(dimension.type));  
  metrics.forEach(metric => fields.newMetric().setId(metric.id).setName(metric.name).setType(metric.type).setAggregation(metric.aggregations));

  var defaultDimension = dimensions.find(field => field.hasOwnProperty('isDefault') && field.isDefault == true);
  var defaultMetric = metrics.find(field => field.hasOwnProperty('isDefault') && field.isDefault == true);

  if (defaultDimension)
    fields.setDefaultDimension(defaultDimension.id);
  if (defaultMetric)
    fields.setDefaultMetric(defaultMetric.id);

  return fields;
}

function getSchema(request) {
  var fields = getFields(request).build();
  return { schema: fields };
}

function convertValue(value, id) {  
  // ToDo: add special conversion if necessary
  switch(id) {      
    default:
      // value will be converted automatically
      return value[id];
  }
}

function entriesToDicts(schema, data, converter, tag) {

  return data.map(function(element) {

    var entry = element[tag];
    var row = {};    
    schema.forEach(function(field) {

      // field has same name in connector and original data source
      var id = field.id;
      var value = converter(entry, id);

      // use UI field ID
      row[field.id] = value;
    });

    return row;
  });
}

function dictsToRows(requestedFields, rows) {
  return rows.reduce((result, row) => ([...result, {'values': requestedFields.reduce((values, field) => ([...values, row[field]]), [])}]), []);
}

function getParams (request) { 
  var schema = this.getSchema();
  var params;

  if (request) {
    params = {};

    // ToDo: handle pagination={startRow=1.0, rowCount=100.0}
  } else {
    // preview only
    params = {
      limit: 20
    }
  }

  return params;
}

function getData(request) {
  Logger.log(request)

  var credentials = getCredentials()
  var schema = getSchema();
  var params = getParams(request);

  var requestedFields;  // fields structured as I want them (see above)
  var requestedSchema;  // fields structured as Google expects them
  if (request) {
    // make sure the ordering of the requested fields is kept correct in the resulting data
    requestedFields = request.fields.filter(field => !field.forFilterOnly).map(field => field.name);
    requestedSchema = getFields(request).forIds(requestedFields);
  } else {
    // use all fields from schema
    requestedFields = schema.map(field => field.id);
    requestedSchema = api.getFields(request);
  }

  var filterPresent = request && request.dimensionsFilters;
  //var filter = ...
  if (filterPresent) {
    // ToDo: apply request filters on API level (before the API call) to minimize data retrieval from API (number of rows) and increase speed
    // see https://developers.google.com/datastudio/connector/filters

    // filter = ...   // initialize filter
    // filter.preFilter(params);  // low-level API filtering if possible
  }

  // get HTTP response; e.g. check for HTTT RETURN CODE on response.code if necessary
  var response = httpGet(credentials.username, credentials.token, URL_DATA, params);  

  // get JSON data from HTTP response
  var data = response.json;

  // convert the full dataset including all fields (the full schema). non-requested fields will be filtered later on  
  var rows = entriesToDicts(schema, data, convertValue, JSON_TAG);

  // match rows against filter (high-level filtering)
  //if (filter)
  //  rows = rows.filter(row => filter.match(row) == true);

  // remove non-requested fields
  var result = dictsToRows(requestedFields, rows);

  console.log('{0} rows received'.format(result.length));
  //console.log(result);

  return {
    schema: requestedSchema.build(),
    rows: result,
    filtersApplied: filter ? true : false
  };
}

如果这些都不符合您的要求,请选择 WebApp正如@kessy 在另一个答案中所建议的。

关于javascript - 在 Google AppS 脚本用户属性中存储 API key 和 secret ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61540618/

相关文章:

javascript - 如何使用 php 打印到包含 ESC/P 的通用纯文本文件?

javascript - 在异步 google.script.run 成功时递增 while 循环

google-apps-script - 返回超链接或现有公式的自定义 Google 电子表格函数

swift - Swift 中的计算只读属性与函数

javascript - 为什么这些 JavaScript 对象列表属性返回错误值?

javascript - lerna 独立版本控制 + Github Actions 的问题

javascript - Safari img 元素不会使用 blob URL 将从服务(例如 Dropbox)检索到的图像呈现为 ArrayBuffer

javascript - 什么是密集阵列?

google-apps-script - 查找一列中的值,如果匹配则添加来自不同列的值 Google Script Google Sheets

properties - 不同类型的属性 setter