- Goa should be instructed to store the token infrastructure in the userPropertyStore rather that the scriptPropertyStore.
- The project credentials are stored in the scriptPropertyStore and Goa will clone them to the userPropertyStore as required (if they change .. for example a new scope, or on the first time a particular user is encountered).
- The webapp will be published as follows.
All the demo code is available on github. Note that I’m using the same code for all these examples, so the final source on github will have evolved in some details from the writeup here.
The example
where the ‘Change folder’ buttons launches the googlePicker, from which the user can select the folder to process,as below.
After that, the app works in exactly the same was as Using OAUTH2 with a webapp published ‘as me’, and will pass the images in the user’s folder to the cloud vision API for processing according to the type of detection selected, producing a result like this;
MZx5DzNPsYjVyZaR67xXJQai_d-phDA33
Creating credentials
Storing the credentials.
cGoa.GoaApp.setPackage (propertyStore , cGoa.GoaApp.createPackageFromFile (DriveApp , { packageName: 'cloudvision', fileId:'0B92xxxxxxFreTVPdjQ', scopes : cGoa.GoaApp.scopesGoogleExpand (['cloud-platform','drive']), service:'google', apiKey:'AIzxxxxxxGvZKbdg' // you can store arbirary properties in goa too. }));
I’ve added the drive scope, and also a new property called apiKey. Picker doesn’t have any scope of its own, but will need Drive scope to be able to access the user’s drive. The picker needs an API key in addition to Oauth2 authorization. An API key can be created in the developers console in the cloud vision project, as described below. Goa is able to retrieve any arbitrary properties in addition to the ones it needs for working with tokens, so I’ll store the API key here too and get it later when needed.
Creating an API key
Some APIs need an API key to measure usage. An API key can also be assigned a domain from which to expect requests using this key- which means that if someone gets hold of your key he would need to issue requests from a registered domain to be able to use it. Of course, in the case of Apps Script, all requests come from Google, so it doesn’t help too much. In any case, here’s how to create an API Key.
In the credentials area of your developer console entry for the cloud vision project (the one you enabled the cloudvision API in).
Store the API key in the goa one off script and execute it to write the whole thing away for later.
cGoa.GoaApp.setPackage (propertyStore , cGoa.GoaApp.createPackageFromFile (DriveApp , { packageName: 'cloudvision', fileId:'0B92xxxxxxFreTVPdjQ', scopes : cGoa.GoaApp.scopesGoogleExpand (['cloud-platform','drive']), service:'google', apiKey:'AIzxxxxxxGvZKbdg' // you can store arbirary properties in goa too. }));
Enabling the API
Since I want to enhance this project by including the Picker, I also need to enable the Drive API and the Picker API in the developers console in the project created for this cloudvision project.
The webApp
/** * this is how to do a webapp which needs authentication * @param {*} e - parameters passed to doGet * @return {HtmlOurput} for rendering */ // since Im using the same webapp for multiplt purposes // these describe what each do // change TYPE to the run required var PARAMS = { TYPE:'asuser', // or asme PACKAGE_NAME:'cloudvision', // always this asuser: { props:PropertiesService.getUserProperties(), clone:true, html:'asuser' }, asme: { props:PropertiesService.getScriptProperties(), clone:false, html:'asme' } }; function doGet (e) { // this is pattern for a WebApp. // passing the doGet parameters (or anything else) // will ensure they are preservered during the multiple oauth2 processes // im using this same for each, so need a way to swicth between var demo = PARAMS[PARAMS.TYPE]; if (!demo) { throw 'set PARAMS.TYPE to match one of the properties of PARAMS'; } // it may need cloning if user props required if (demo.clone) { cGoa.GoaApp.userClone( PARAMS.PACKAGE_NAME, PropertiesService.getScriptProperties() , demo.props ); } // get the goa var goa = cGoa.GoaApp.createGoa( PARAMS.PACKAGE_NAME, demo.props ).execute(e); // it's possible that we need consent - this will cause a consent dialog if (goa.needsConsent()) { return goa.getConsent(); } // if we get here its time for your webapp to run // and we should have a token, or thrown an error somewhere if (!goa.hasToken()) { throw 'something went wrong with goa - did you check if consent was needed?'; } // now return the evaluated page return HtmlService .createTemplateFromFile(demo.html) .evaluate() .setSandboxMode(HtmlService.SandboxMode.IFRAME) .setTitle('Web app Oauth2 ' + demo.html) }
Webapp walkthrough
// since Im using the same webapp for multiplt purposes // these describe what each do // change TYPE to the run required var PARAMS = { TYPE:'asuser', // or asme PACKAGE_NAME:'cloudvision', // always this asuser: { props:PropertiesService.getUserProperties(), clone:true, html:'asuser' }, asme: { props:PropertiesService.getScriptProperties(), clone:false, html:'asme' } };
Aside from the parametrization, the only difference between this pattern and the one used in Using OAUTH2 with a webapp published ‘as me’ is that I am using the userPropertyStore to keep the token infrastructure in. That means I need to handle cloning the credentials as new users come along. Goa takes care of all of that by cloning the package from the scriptProperties to the userProperties as needed. This code needs to be at the start of the pattern to make that happen. From then on, all interaction is with the UserProperties.
// it may need cloning if user props required if (demo.clone) { cGoa.GoaApp.userClone( PARAMS.PACKAGE_NAME, PropertiesService.getScriptProperties() , demo.props ); }
The Picker
<!-- javascript. --> <script src="https://www.google.com/jsapi"></script> <?!= requireJs(['mainasuser']); ?> <?!= requireGs(['Client','Server','App','Render','Picker']); ?>
The mainasuser.js html file is also slightly different as it loads the picker code from the Google API as follows, then initializes the App when it is completely loaded.
// this version uses the picker to get at the users drive google.load('picker', '1'); google.setOnLoadCallback(function () { App.init(); });
The picker code itself is actually quite simple, although it could be improved by playing around with the (very complicated) extensive options that the picker API offers.
var Picker = (function myFunction(ns) { /** * get a picker * @param {string} accessToken * @param {string} developerKey * @param {function} callback when picked */ ns.getFolderPicker = function (accessToken, developerKey , callback) { var docsView = new google.picker.DocsView() .setIncludeFolders(true) .setMimeTypes('application/vnd.google-apps.folder') .setSelectFolderEnabled(true); var picker = new google.picker.PickerBuilder() .addView(docsView) .setOAuthToken(accessToken) .setCallback(callback) .setDeveloperKey(developerKey) .setOrigin(google.script.host.origin) .setTitle('Pick a folder with images to analyze') .build(); picker.setVisible(true); }; return ns; })(Picker || {});
Since the picker is invoked client side, it needs a way to get the api key and oauth token from the server and ensure they are fresh. This method is in the Client namespace.
/** * for the picker we need to get the access token * @param {function} func what to do after we have it */ ns.getPickerKeys = function (func) { google.script.run .withFailureHandler ( function (err) { App.reportMessage (err); }) /** * called back from server */ .withSuccessHandler ( function (result) { return func (result); }) .expose('Server','pickerKeys'); };
Which as usual, asks the Server namespace to do something on its behalf – namely to get a token and an apikey that can be used with the picker.
/** * return s tokens and keys needed for pcier * @return {object} */ ns.pickerKeys = function () { return { token:Goth.getToken(PARAMS.PACKAGE_NAME, PARAMS[PARAMS.TYPE].props), key:Goth.getGoa (PARAMS.PACKAGE_NAME, PARAMS[PARAMS.TYPE].props).getProperty ('apiKey') } };
All this is kicked off by listening for a click on the Change folder button, and the selected folder details are displayed and stored in the folder controls.
// handle a picker to get a different folder listen (ns.globals.divs.getFolder , "click" , function (e) { // first get the token and the developer key from the server Client.getPickerKeys (function (keys) { // then do a picker, passing them Picker.getFolderPicker (keys.token, keys.key , function (pickerData) { // couple of shortcuts var p = google.picker; var g = ns.globals.divs; // see if we got something picked; if (pickerData[p.Response.ACTION] === p.Action.PICKED) { // the first one var folder = pickerData[p.Response.DOCUMENTS][0]; // show what was picked g.folderId.value = folder[p.Document.ID]; g.folderLabel.innerHTML = folder[p.Document.NAME] + ' (' + folder[p.Document.ID] + ')'; } }); }); });
- 2 ways to create and preserve formulas with fiddler for Google Apps Script
- A fourth way to preserve and create formulas with Fiddler for Sheets, plus some more new methods
- A functional approach to updating master sheet with Fiddler
- A recursive extend function for Apps Script
- A third way to preserve formulas with fiddler, plus 2 new methods
- A webapp to share copies of the contents of folders
- Abstracting services with closures
- Add-on spinner
- Addressing namespace and library methods from google.script.run
- Anonymous user registration with the Apps Script PropertiesService
- Apps for Office – binding example comparison
- Apps Script as a proxy
- Apps Script const scoping problems
- Apps Script server side to client side htmlservice progress reporting using CacheService
- Calculating image dimensions in server side apps script
- Calculating the last day of a given weekday in the month
- Canvasser
- Chaining JavaScript
- Changing class properties dynamically
- Checking the argument types in Apps Script
- Cleaning up a document format
- Cleaning up heading levels
- Column numbers to characters
- Composing functions and functional programming
- Configurable canvas meter
- Convert JSON to XML
- Converting SVG to PNG with JavaScript
- Converting timestamps to dates formula
- Copying canvas and svg images from your Add-on
- Copying to new host location
- Copying to new host location
- Counting script and library usage
- Create sha1 signatures with apps script
- Creating a key digest to use for a cache key or to compare content
- Creating a pile of files list from Google Drive
- Creating and working with transposed sheet data arrays
- Cross Origin Resource sharing (CORS)
- CryptoJS libraries for Google Apps Script
- Custom checking for exponential backoff
- Data wrangling with named columns in Google Spreadsheet
- Dealing with objects that are too large for the property or cache store
- Detecting Spreadsheet tables automatically with Google Apps Script
- Direction minimizer – other usages
- Do something useful with GAS in 5 minutes
- Dynamically creating tables with clusterize.js
- EasyCron Library
- ES6 JavaScript features
- Exponential backoff
- Exponential backoff for promises
- Fiddler – A functional approach to fiddling with sheet data
- Fiddler and rangeLists
- Fiddler now supports joins to merge matching columns from multiple sheets
- Fiddling with text fields that look like dates
- Filling ranges in Google Sheets
- Finding a Drive App folder by path
- Finding where Drive hosting is being used in Sites
- Flattening an object with dot syntax
- Flattening and unflattening objects to spreadsheets
- Formatting GraphQL queries
- Formatting sheet column data with fiddler
- From notes to frequencies and back again
- From Xml to JSON
- Generating and managing random lists with JavaScript and Apps Script
- Generating coupon codes with expiry dates
- Generating test data for sheets and tables
- Get GAS library info
- Getting an htmlservice template from a library
- Getting insights into Sheets performance
- Google Drive as cache
- Header formatting with fiddler
- Highlight duplicate rows in a sheet – map and reduce
- Highlight duplicate rows in a sheet – map, filter and every
- How to determine what kind of object something is in Apps Script
- How to get stats about youtube videos in your channel with apps script
- How to pass non stringifyable objects to html service
- How to transpose spreadsheet data with apps script
- Identify duplicates on Drive
- Identifying hosted files
- Implementing a client side progress bar reporting on server side work for htmlService
- Importing Predictwise data
- Improved namespace pattern for Apps Script
- Including the stack in custom errors
- Iterator magic – Splitting an array into chunks
- JavaScript closures – how, where and why
- JavaScript currying and functional programming
- JavaScript currying and functional programming – even more
- JavaScript recursion primer
- JavaScript snippet to create a default method for an object
- JSONP and JSON and Google Apps Script webapps
- Loading large JSON datasets into BigQuery with Apps Script
- Logging differences in strings in Apps Script
- Measuring library load speed
- Migrating user and script properties
- Minimizing maps directionfinder api calls
- More client server code sharing
- More recursion – parents and children
- More sheet data fiddling
- Multiple inserts in Fusion Tables
- Namespaces in libraries and scripts
- Normalizing the header level of blank paragraphs
- Optimizing sheet formatting with Apps Script
- Optimizing showing and hiding rows and columns
- Organizing asynchronous calls to google.script.run
- Organizing parallel streams of server calls with google.script.run promises
- Parallel process orchestration with HtmlService
- Passing data to html service
- Patching site html
- Populating sheets with API data using a Fiddler
- Proxy jsonp
- Pseudo binding in HTML service
- Queuing asynchronous tasks with rate limit and concurrency constraints
- Random and fake test data in Sheets with Google Apps Script
- Recursive async functions
- Removing duplicate paragraphs
- Reporting file, function and line number in Apps Script
- Resumable uploads – writing large files to Drive with Apps Script
- Reusing html stuff between Apps Script projects
- Roughly matching text
- Serving apps script to JavaScript app
- Sharing code between client and server
- Shortcut for adding nested properties to a JavaScript object
- Simple but powerful Apps Script Unit Test library
- Simple server side polling
- Sorting Google Sheet DisplayValues
- Squeezing more into (and getting more out of) Cache services
- Styling Gmail html tables
- Summarizing emails to a sheet
- SunCalc
- Super simple cipher library for Apps Script encryption and decryption
- TimeSimmer : An adjustable timer for apps that need to speed up or slow down time
- Transform dates for add-on transfer
- Transposing sheet data
- Traversing a tree
- Unique values with data fiddler
- Unnesting data to sheet values
- Untangling with promises
- Use Drive properties to find app files
- Use promise instead of callback for settimeout
- Using Advanced Drive service to convert files
- Using Apps Script for xml json conversion
- Using array formulas to improve performance
- Using crossfilter with Google Apps Script
- Using D3 in server side Gas
- Using es6 promises server side in Apps Script
- Using Es6 with Apps Script
- Using exponential backoff with github api – dealing with data “in preparation”
- Using Google sheets via Bigquery from Apps Script
- Using named locks with Google Apps Scripts
- Using promises to orchestrate Html service polling
- Using promises with apps script
- Using the Itunes API with Apps Script
- Using the slideshare API from Apps Script
- Using timing functions to get insight into Sheets
- Watching docs for changes
- Watching for server side changes from the client html service
- What JavaScript engine is Apps Script running on?
- Why Base64
- Zipping to make stuff fit in cache or properties service.
Why not join our forum, follow the blog or follow me on Twitter to ensure you get updates when they are available.