|
| 1 | +import { MCDocument, CacheKeys, URLS, TimeInSeconds, MCProject } from './Common'; |
| 2 | + |
| 3 | +export const baseURL = 'https://whole-tires-fix.loca.lt'; |
| 4 | + |
| 5 | +/** |
| 6 | + * Attempts to access a non-Google API using a constructed service |
| 7 | + * object. |
| 8 | + * |
| 9 | + * If your add-on needs access to non-Google APIs that require OAuth, |
| 10 | + * you need to implement this method. You can use the OAuth1 and |
| 11 | + * OAuth2 Apps Script libraries to help implement it. |
| 12 | + * |
| 13 | + * @param url The URL to access. |
| 14 | + * @param method_opt The HTTP method. Defaults to GET. |
| 15 | + * @param headers_opt The HTTP headers. Defaults to an empty |
| 16 | + * object. The Authorization field is added |
| 17 | + * to the headers in this method. |
| 18 | + * @return {HttpResponse} the result from the UrlFetchApp.fetch() call. |
| 19 | + */ |
| 20 | +export function accessProtectedResource( |
| 21 | + url: string, |
| 22 | + method: GoogleAppsScript.URL_Fetch.HttpMethod = 'get', |
| 23 | + headers: GoogleAppsScript.URL_Fetch.HttpHeaders = {} |
| 24 | +) { |
| 25 | + if (!url.startsWith('http')) { |
| 26 | + url = baseURL + url; |
| 27 | + } |
| 28 | + const service = getOAuthService(); |
| 29 | + let maybeAuthorized = service.hasAccess(); |
| 30 | + if (maybeAuthorized) { |
| 31 | + // A token is present, but it may be expired or invalid. Make a |
| 32 | + // request and check the response code to be sure. |
| 33 | + |
| 34 | + // Make the UrlFetch request and return the result. |
| 35 | + const accessToken = service.getAccessToken(); |
| 36 | + headers['Authorization'] = Utilities.formatString('Bearer %s', accessToken); |
| 37 | + const resp = UrlFetchApp.fetch(url, { |
| 38 | + headers: headers, |
| 39 | + method: method, |
| 40 | + muteHttpExceptions: true // Prevents thrown HTTP exceptions. |
| 41 | + }); |
| 42 | + |
| 43 | + const code = resp.getResponseCode(); |
| 44 | + if (code >= 200 && code < 300) { |
| 45 | + return resp; |
| 46 | + } else if (code == 401 || code == 403) { |
| 47 | + // Not fully authorized for this action. |
| 48 | + maybeAuthorized = false; |
| 49 | + } else { |
| 50 | + // Handle other response codes by logging them and throwing an exception. |
| 51 | + console.error('Backend server error (%s): %s', code.toString(), resp.getContentText('utf-8')); |
| 52 | + throw 'Backend server error: ' + code; |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + if (!maybeAuthorized) { |
| 57 | + // Invoke the authorization flow using the default authorization prompt card. |
| 58 | + CardService.newAuthorizationException() |
| 59 | + .setAuthorizationUrl(service.getAuthorizationUrl()) |
| 60 | + .setResourceDisplayName('Display name to show to the user') |
| 61 | + .throwException(); |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Create a new OAuth service to facilitate accessing an API. |
| 67 | + * This example assumes there is a single service that the add-on needs to |
| 68 | + * access. Its name is used when persisting the authorized token, so ensure |
| 69 | + * it is unique within the scope of the property store. You must set the |
| 70 | + * client secret and client ID, which are obtained when registering your |
| 71 | + * add-on with the API. |
| 72 | + * |
| 73 | + * See the Apps Script OAuth2 Library documentation for more |
| 74 | + * information: |
| 75 | + * https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service |
| 76 | + * |
| 77 | + * @return A configured OAuth2 service object. |
| 78 | + */ |
| 79 | +function getOAuthService() { |
| 80 | + return ( |
| 81 | + OAuth2.createService('Mermaid Chart') |
| 82 | + .setAuthorizationBaseUrl('https://whole-tires-fix.loca.lt/oauth/authorize') |
| 83 | + .setTokenUrl('https://whole-tires-fix.loca.lt/oauth/token') |
| 84 | + .setClientId('f88f1365-dea8-466e-8880-e22211e145bd') |
| 85 | + .setParam('code_challenge_method', 'S256') |
| 86 | + .setScope('email') |
| 87 | + .setCallbackFunction('authCallback') |
| 88 | + // @ts-expect-error |
| 89 | + .generateCodeVerifier() |
| 90 | + .setCache(CacheService.getUserCache()) |
| 91 | + .setPropertyStore(PropertiesService.getUserProperties()) |
| 92 | + ); |
| 93 | +} |
| 94 | + |
| 95 | +/** |
| 96 | + * Boilerplate code to determine if a request is authorized and returns |
| 97 | + * a corresponding HTML message. When the user completes the OAuth2 flow |
| 98 | + * on the service provider's website, this function is invoked from the |
| 99 | + * service. In order for authorization to succeed you must make sure that |
| 100 | + * the service knows how to call this function by setting the correct |
| 101 | + * redirect URL. |
| 102 | + * |
| 103 | + * The redirect URL to enter is: |
| 104 | + * https://script.google.com/macros/d/<Apps Script ID>/usercallback |
| 105 | + * |
| 106 | + * See the Apps Script OAuth2 Library documentation for more |
| 107 | + * information: |
| 108 | + * https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service |
| 109 | + * |
| 110 | + * @param {Object} callbackRequest The request data received from the |
| 111 | + * callback function. Pass it to the service's |
| 112 | + * handleCallback() method to complete the |
| 113 | + * authorization process. |
| 114 | + * @return {HtmlOutput} a success or denied HTML message to display to |
| 115 | + * the user. Also sets a timer to close the window |
| 116 | + * automatically. |
| 117 | + */ |
| 118 | +export function authCallback(callbackRequest: any) { |
| 119 | + const authorized = getOAuthService().handleCallback(callbackRequest); |
| 120 | + if (authorized) { |
| 121 | + return HtmlService.createHtmlOutput( |
| 122 | + 'Success! You can close this tab now. <script>setTimeout(function() { top.window.close() }, 1);</script>' |
| 123 | + ); |
| 124 | + } else { |
| 125 | + return HtmlService.createHtmlOutput('Denied'); |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +/** |
| 130 | + * Unauthorizes the non-Google service. This is useful for OAuth |
| 131 | + * development/testing. Run this method (Run > resetOAuth in the script |
| 132 | + * editor) to reset OAuth to re-prompt the user for OAuth. |
| 133 | + */ |
| 134 | +export function resetOAuth() { |
| 135 | + getOAuthService().reset(); |
| 136 | +} |
| 137 | + |
| 138 | +export function cachedFetch<T>(key: string, url: string, ttl: number, fallback: string = '[]'): T { |
| 139 | + const cache = CacheService.getUserCache(); |
| 140 | + const shouldUseCache = hasAccess() && true; // Set to false when testing to bypass cache. |
| 141 | + let value = shouldUseCache ? cache.get(key) : undefined; |
| 142 | + if (!value) { |
| 143 | + value = accessProtectedResource(url)?.getContentText() ?? fallback; |
| 144 | + cache.put(key, value, ttl); |
| 145 | + } |
| 146 | + return JSON.parse(value) as T; |
| 147 | +} |
| 148 | + |
| 149 | +export function getDocuments(projectID: string): MCDocument[] { |
| 150 | + return cachedFetch( |
| 151 | + CacheKeys.documents(projectID), |
| 152 | + URLS.rest.projects.get(projectID).documents, |
| 153 | + TimeInSeconds.minutes(5) |
| 154 | + ); |
| 155 | +} |
| 156 | + |
| 157 | +export function getProjects(): MCProject[] { |
| 158 | + return cachedFetch(CacheKeys.projects, URLS.rest.projects.list, TimeInSeconds.day); |
| 159 | +} |
| 160 | + |
| 161 | +function hasAccess() { |
| 162 | + const service = getOAuthService(); |
| 163 | + const maybeAuthorized = service.hasAccess(); |
| 164 | + return maybeAuthorized; |
| 165 | +} |
0 commit comments