Accessing Drive thumbnailLink with gapi.client.request gives 404 - reactjs

I'm currently working on a React app that should load a set of (non-public) files from a user's Google Drive and display thumbnails using the CSS background-image: url(...) property. I can load all of the file metadata using the Gapi Files:get method, and an OAuth token is set ahead of time using gapi.client.setToken. I'm hoping to load the thumbnail as follows:
function CollectionArtifact(props){
const [thumbnail, setThumbnail] = useState(null);
async function loadThumbnail(){
try {
const res = await window.gapi.client.request(props.artifact.thumbnailLink);
const data = await res.blob();
const localURL = URL.createObjectUrl(data);
setThumbnail(localURL);
catch(e){ console.log('Failed to load thumbnail', e); }
}
useEffect(() => {
if(props.apisLoaded){
loadThumbnail();
}
}, [props.apisLoaded]);
return (
...
<div style={{backgroundImage: `url(${thumbnail})`}}>
...
</div>
...
);
}
The client is initialized in a wrapper component like so:
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
apiKey: developerKey,
clientId: clientId,
scope: 'https://www.googleapis.com/auth/drive'
}).then(() => {
window.gapi.client.load('drive', 'v3', () => {
if(props.onGapisLoad) props.onGapisLoad();
setInitialized(true);
});
});
});
However, the window.gapi.client.request call gives Failed to load resource: the server responded with a status of 404, and the URL that's actually giving this error is the proxy request https://docs.google.com/static/proxy.html?usegapi=1&.... Using fetch also doesn't work, as it gives a CORS error. Meanwhile, if I try to access the thumbnailLink in the browser while signed in to the correct Google account (and no other accounts), I can see the image without a problem.
What would be the proper way to load thumbnails through an authorized request? I've seen other answers that recommend generating a PDF and using it to create a public thumbnail, but I'd like to avoid creating extra files in the user's drive or making file data publicly accessible. And since the image can be loaded directly from the browser, I would assume there's a way to access it with an authorized client.

Related

Sveltekit call API from server and pass the data to the front-end

I'm learning Sveltekit and i've been stuck on passing data from backend to frontend. I have a 3rd party API that i want to call and send back the results that gives me the bearer needed for all the other API calls.
The problem is that the CORS policys of this server (that i have no control over) isn't configured well and i can't call the API from the browser.
So i wanted to call it from the backend and send the result to the front, what I did is a "+page.server.ts" file with the API call in the "load" function and puts the results in a store that i created in the stores.ts file :
import { writable } from 'svelte/store';
export const token = writable('');
So in the front (+page.svelte) I just imported the token from the store and showed it :
<script>
import { token } from "../stores";
</script>
<h1>{$token}</h1>
And it works for a second before the token is overriden with an empty string and i don't get why.
Do you have any idea of what could cause this ?
Feel free to ask for more informations if needed, because i'm lost right now
You cannot access a svelte store from within a server file.
To call a api from +page.server.js. Create a folder in your routes and add a +page.server.js file and add the following:
/** #type {import('./$types').PageServerLoad} */
export async function load({ fetch }) {
const res = await fetch('https://api.agify.io/?name=bella', {
method: 'GET'
});
const data = await res.json();
console.log(data);
return {
props: {
data: data
}
};
}
Then create a +page.svelte with the following code:
<script>
/** #type {import('./$types').PageData} */
export let data;
console.log(data);
let name = data.props.data.name;
let age = data.props.data.age;
</script>
<h1>
{name} <!-- bella -->
{age} <!-- the age -->
</h1>
SvelteKit docs have info that could be of use. SvelteKit docs loading data

Uppy/Shrine: How to retrieve presigned url for video after successful upload (using AWS S3)

I'm using Uppy for file uploads in React, with a Rails API using Shrine.
I'm trying to show a preview for an uploaded video before submitting a form. It's important to emphasize that this is specifically for a video upload, not an image. So the 'thumbnail:generated' event will not apply here.
I can't seem to find any events that uppy provides that returns a cached video preview (like thumbnail:generated does) or anything that passes back a presigned url for the uploaded file (less expected, obviously), so the only option I see is constructing the url manually. Here's what I'm currently trying for that (irrelevant code removed for brevity):
import React, { useEffect, useState } from 'react'
import AwsS3 from '#uppy/aws-s3'
import Uppy from '#uppy/core'
import axios from 'axios'
import { DragDrop } from '#uppy/react'
import { API_BASE } from '../../../api'
const constructParams = (metadata) => ([
`?X-Amz-Algorithm=${metadata['x-amz-algorithm']}`,
`&X-Amz-Credential=${metadata['x-amz-credential']}`,
`&X-Amz-Date=${metadata['x-amz-date']}`,
'&X-Amz-Expires=900',
'&X-Amz-SignedHeaders=host',
`&X-Amz-Signature=${metadata['x-amz-signature']}`,
].join('').replaceAll('/', '%2F'))
const MediaUploader = () => {
const [videoSrc, setVideoSrc] = useState('')
const uppy = new Uppy({
meta: { type: 'content' },
restrictions: {
maxNumberOfFiles: 1
},
autoProceed: true,
})
const getPresigned = async (id, type) => {
const response = await axios.get(`${API_BASE}/s3/params?filename=${id}&type=${type}`)
const { fields, url } = response.data
const params = constructParams(fields)
const presignedUrl = `${url}/${fields.key}${params}`
console.log('presignedUrl from Shrine request data: ', presignedUrl)
setVideoSrc(presignedUrl)
}
useEffect(() => {
uppy
.use(AwsS3, {
id: `AwsS3:${Math.random()}`,
companionUrl: API_BASE,
})
uppy.on('upload-success', (file, _response) => {
const { type, meta } = file
// First attempt to construct presigned URL here
const url = 'https://my-s3-bucket.s3.us-west-1.amazonaws.com'
const params = constructParams(meta)
const presignedUrl = `${url}/${meta.key}${params}`
console.log('presignedUrl from upload-success data: ', presignedUrl)
// Second attempt to construct presigned URL here
const id = meta.key.split(`${process.env.REACT_APP_ENV}/cache/`)[1]
getPresigned(id, type)
})
}, [uppy])
return (
<div className="MediaUploader">
<div className="Uppy__preview__wrapper">
<video
src={videoSrc || ''}
className="Uppy__preview"
controls
/>
</div>
{(!videoSrc || videoSrc === '') && (
<DragDrop
uppy={uppy}
className="UploadForm"
locale={{
strings: {
dropHereOr: 'Drop here or %{browse}',
browse: 'browse',
},
}}
/>
)}
</div>
)
}
export default MediaUploader
Both urls here come back with a SignatureDoesNotMatch error from AWS.
The manual construction of the url comes mainly from constructParams. I have two different implementations of this, the first of which takes the metadata directly from the uploaded file data in the 'upload-success' event, and then just concatenates a string to build the url. The second one uses getPresigned, which makes a request to my API, which points to a generated Shrine path that should return data for a presigned URL. API_BASE simply points to my Rails API. More info on the generated Shrine route here.
It's worth noting that everything works perfectly with the upload process that passes through Shrine, and after submitting the form, I'm able to get a presigned url for the video and play it without issue on the site. So I have no reason to believe Shrine is returning incorrectly signed urls.
I've compared the two presigned urls I'm manually generating in the form, with the url returned from Shrine after uploading. All 3 are identical in structure, but have different signatures. Here are those three urls:
presignedUrl from upload-success data:
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/41b229fb17cbf21925d2cd907a59be25.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132613Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=97aefd1ac7f3d42abd2c48fe3ad50b542742ad0717a51528c35f1159bfb15609
presignedUrl from Shrine request data:
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/023592fb14c63a45f02c1ad89a49e5fd.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132619Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=7171ac72f7db2b8871668f76d96d275aa6c53f71b683bcb6766ac972e549c2b3
presigned url displayed on site after form submission:
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/41b229fb17cbf21925d2cd907a59be25.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132734Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=9ecc98501866f9c5bd460369a7c2ce93901f94c19afa28144e0f99137cdc2aaf
The first two urls come back with SignatureDoesNotMatch, while the third url properly plays the video.
I'm aware the first and third urls have the same file name, while the second url does not. I'm not sure what to make of that, though, but the relevance of this is secondary to me, since that solution was more of a last ditch effort anyway.
I'm not at all attached to the current way I'm doing things. It's just the only solution I could come up with, due to lack of options. If there's a better way of going about this, I'm very open to suggestions.

Cannot get Cypress test to work in React with Okta

I am trying to test my React (v16.10.2) application with Cypress (v4.5.0). Our application is using Okta for authentication (with the client #okta/okta-react 1.3.1).
I can use my app from the browser without any issues. The first time I login, I get the Okta login screen. I enter my user ID and password, and the react client calls the authn endpoint with my creds, and then calls the authorization endpoint to get the token. I am then taken to the first screen of our application.
When I try to login in my Cypress test, my user ID and password are entered into the login screen, the authn endpoint is called successfully, but the authorization endpoint returns a 403 error. Unfortunately, there is no other info about why I am getting the 403.
I have compared the authorization requests between the one that works in the browser, and the one that doesn't work from Cypress. The only real difference I see is that the working browser request has an origin header, whereas the failing one does not.
Question #1: Could the missing origin header be the cause of my problem?
In order to avoid a bunch of CORS and cross-site issues, I had to install a couple Chrome extensions (ignore-x-frame-headers and Access-Control-Allow-Origin-master). I am implementing them in the following code in cypress/plugins/index.js:
module.exports = (on, config) => {
on('before:browser:launch', (browser = {}, launchOptions) => {
// The following code comes from https://medium.com/#you54f/configuring-cypress-to-work-with-iframes-cross-origin-sites-afff5efcf61f
// We were getting cross-origin errors when trying to run the tests.
if (browser.name === 'chrome') {
const ignoreXFrameHeadersExtension = path.join(__dirname, '../extensions/ignore-x-frame-headers');
launchOptions.args.push(`--load-extension=${ignoreXFrameHeadersExtension}`);
const accessControlAllowOriginMasterExtension = path.join(__dirname, '../extensions/Access-Control-Allow-Origin-master');
launchOptions.args.push(`--load-extension=${accessControlAllowOriginMasterExtension}`);
launchOptions.args.push("--disable-features=CrossSiteDocumentBlockingIfIsolating,CrossSiteDocumentBlockingAlways,IsolateOrigins,site-per-process");
launchOptions.args.push('--disable-site-isolation-trials');
launchOptions.args.push('--reduce-security-for-testing');
launchOptions.args.push('--out-of-blink-cors');
}
if (browser.name === 'electron') {
launchOptions.preferences.webPreferences.webSecurity = false;
}
return launchOptions;
});
I also added the following to cypress.json:
{
"chromeWebSecurity": false
}
Here is my cypress test:
describe('Order Lookup Test', () => {
const UI_URL: string = 'http://localhost:3000/';
const ORDER_NUMBER: string = '10307906234';
beforeEach(() => {
Cypress.config('requestTimeout', 50000);
cy.visit(UI_URL);
cy.get('#okta-signin-username', {timeout: 10000}).type('xxxxxxxx');
cy.get('#okta-signin-password', {timeout: 10000}).type('xxxxxxxx');
cy.get('#okta-signin-submit', {timeout: 10000}).click();
})
it('should return an order', () => {
cy.get('.number-input', {timeout: 10000}).type(ORDER_NUMBER);
cy.get('.order-lookup-buttons-search-valid').should('be.visible').click();
})
})
Does anyone have any idea what might be going on? What other information should I be including in order to help narrow this down?

Getting “TypeError: failed to fetch” when sending an email with SendGrid on ReactJS project

I am trying to send email with SendGrid in ReactJS project.
This is my componnet:
//Email.js
import React from 'react'
const sgMail = require('#sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: 'aaaaa#gmail.com',
from: 'bbbb#gmail.com',
subject: 'This is a test mail',
text: 'and easy to do anywhere, even with Node.js',
html: '<strong>and easy to do anywhere, even with Node.js</strong>',
};
sgMail.send(msg).catch(error => {alert(error.toString()); });
export const Email= () => (
<h1>Email Sending Page</h1>
)
When I am trying to run the app with "npm start" on localhost, the email is not sent and I got the error message "TypeError: Failed to fetch".
But, if I am using this code:
//Email.js
const sgMail = require('#sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: 'aaaaa#gmail.com',
from: 'bbbb#gmail.com',
subject: 'This is a test mail',
text: 'and easy to do anywhere, even with Node.js',
html: '<strong>and easy to do anywhere, even with Node.js</strong>',
};
sgMail.send(msg)
and do this command: "node Email.js" the mail is sent. It works only this way and I cannot understand why.
I tried any solution that I could find but nothing works.
(I tried even to put the api_key hardcoded in the code just for the test and I got the same result).
EDIT
After looking around a bit I found out that you can't use Sendgrid to send email directly from the browser.
Sendgrid won't let you send an email directly using Javascript in the
browser.
You will need to have a server set-up and use the server to send the
email instead (using your favourite back-end framework/language,
Node.js, php, Java, etc.).
The steps for sending a mail will be similar to this:
Write email details in the React application
Send a POST request to
your server endpoint (for example, /sendemail) with the email data
(recipient, title, content, etc.) Receive Email data in the server and
send it to Sendgrid api Here is the official Sendgrid documentation
regarding their CORS policy:
https://sendgrid.com/docs/for-developers/sending-email/cors/
Source: React unable to send email with SendGrid
EDIT 2
If you want to implement Sendgrid without actually building and deploying a server, you can use a simple Firebase function which is free to host.
I know this may look intimidating but in reality its pretty easy. Also I just put this together real quick so if anything doesn't work out for you, shoot me a comment.
Follow steps 1-3 on the getting started page for firebase functions. It is pretty straightforward and you end up with the firebase tools CLI installed.
Navigate to the functions/ folder inside your project on the command line/terminal.
Install the Sendgrid and cors libraries in the functions folder
npm i #sendgrid/mail cors
Add your Sendgrid API key to your firebase environment with the following command in your project:
firebase functions:config:set sendgrid.key="THE API KEY"
Copy this into your functions/index.js file:
const functions = require("firebase-functions");
const cors = require("cors")({ origin: true });
const sgMail = require("#sendgrid/mail");
exports.sendEmail = functions.https.onRequest((req, res) => {
sgMail.setApiKey(functions.config().sendgrid.api);
return cors(req, res, () => {
const { msg } = req.body;
sgMail.send(msg).catch(error => {
alert(error.toString());
});
res.status(200).send(msg);
});
});
Save it and run firebase deploy --only functions on the command line. Your function should now be live at https://us-central1-<project-id>.cloudfunctions.net/sendEmail
Now change your React file to:
//Email.js
import React, { useEffect } from 'react'
export const Email= () => {
useEffect(() => {
const sendEmail = async() => {
const msg = {
to: 'aaaaa#gmail.com',
from: 'bbbb#gmail.com',
subject: 'This is a test mail',
text: 'and easy to do anywhere, even with Node.js',
html: '<strong>and easy to do anywhere, even with Node.js</strong>',
};
const response = await fetch(
'https://us-central1-FIREBASE-PROJECT-ID-HERE.cloudfunctions.net/sendEmail', {
method: 'POST',
body: JSON.stringify(msg),
headers: {
'Content-Type': 'application/json'
}
});
console.log("response", response);
}
sendEmail();
}, []);
return <h1>Email Sending Page</h1>
}
And thats it! You basically have a server side function without making a server and its free!
Feel free to ignore this if you don't feel like putting in the work but if you need any help, let me know.

Error: User credentials required in Google Cloud Print API

I'm trying to use Google Cloud Print(GCP) API, but I can't make it works.
Maybe I've understood bad the workflow because is the first time I'm using the google api, please help me to understand how to make it works.
Initial considerations:
I'm trying to implement it in reactJS, but It is indifferent because the logic to make GCP works is independent of the technology. Then you also can help me understand the workflow.
What exactly I want:
To make my first test, I am looking to get all information about my printer.
What I did:
I created a project in: https://console.developers.google.com
Inside the project created, I created a credential:
create credentials -> OAuth client ID
And I chose Application type: Web, and also configure the restrictions to source and redirection to my localhost.
Manually in https://www.google.com/cloudprint, I added my printer, I made a test printing a PDF and was OK.
I created a project in reactJS to get the information of my printer I've added.
Component:
Explanation:
I'm using a component react-google-login to obtain easily the user accessToken: https://github.com/anthonyjgrove/react-google-login
This component only obtains the access token and save it in localStorage, in a variable called googleToken and it draws a button to call a function to obtain the information about the printer.
code:
import React, { Component } from 'react'
import GoogleLogin from 'react-google-login';
import { connect } from 'react-redux'
import { getPrinters } from '../actions/settings'
class Setting extends Component {
responseGoogle(response) {
const accessToken = response.accessToken
localStorage.setItem('googleToken', accessToken)
}
render() {
return (
<div>
<GoogleLogin
clientId="CLIENT_ID_REMOVED_INTENTIONALLY.apps.googleusercontent.com"
buttonText="Login"
onSuccess={this.responseGoogle}
onFailure={this.responseGoogle}
/>
<button
onClick = {() => {
this.props.getPrinters()
}}
>test printer</button>
</div>
)
}
}
const mapStateToProps = state => {
return {
state: state
}
}
const mapDispatchToProps = dispatch => {
return {
getPrinters() {
dispatch(getPrinters())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Setting)
Action or Function to get information printer:
Explanation:
I'm passing the parameter printerid to get information about that printer.
In authorization, I'm using OAuth ... because in the documentation says that(second paragraph).: https://developers.google.com/cloud-print/docs/appInterfaces
The next two headers I wrote it because I tried solutions as:
Google Cloud Print API: User credentials required
Google Cloud Print User credentials required
code:
import axios from 'axios'
axios.defaults.headers.common['Authorization'] = 'OAuth ' + localStorage.getItem('googleToken')
axios.defaults.headers.common['scope'] = 'https://www.googleapis.com/auth/cloudprint'
axios.defaults.headers.common['X-CloudPrint-Proxy'] = 'printingTest'
const getPrinters = () => {
return () => {
return axios.get('https://www.google.com/cloudprint/printer'
, {
params: {
printeid: 'PRINTER_ID_REMOVED_INTENTIONALLY'
}
}
)
.then(response => {
console.log('response of google cloud print')
console.log(response)
})
}
}
export { getPrinters }
Error:
After all explained before, I got the next error:
User credentials required
Error 403
Note:
I'm using CORS plugin by recommendation of:
Chrome extensions for silent print?
because initially, I had cors error.
Any suggestion or recommendation would be very useful, thanks.
I've resolved my problem, my main problem about User Credential required were because I was using the incorrect access token and It was because I was getting the access token incorrectly.
I'm going to explain my whole solution because there are few examples of codes with this API.
Solutions:
The steps described were Ok until the fourth step where I used the external component react-google-login to trying to get the access token, instead I used googleapis module: Link Github googleapis
Also to avoid CORS problem(and not use CORS chrome plugin) I wrote the requests to Google API in server side.(NODEJS)
I had also a problem in the frontend when I tried to generate a popup to give permission for printer(problems about CORS), my solution was to use this very simple module for authentication: Link Github oauth-open
General scheme:
Explanation:
Knowing I have all data described in my question post(until the third step).
Authentication:
The next step in getting a URL and use it to the user can authenticate.
As I said before I used the module oauth-open in the frontend to generate the popup and only this module need the URL. To get the URL in the backend I used the endpoint /googleurl, where here I used the method generateAuthUrl of the module googleapis to generate the URL.
After that In the frontend, I got the authentication_code(that returned the module oauth-open), I send It to my endpoint /googletoken and here I process the authentication_code to generate access token, refresh token and expiration date with the method getToken of the module googleapis. Finally, these data are stored in the database.
Print:
For print, since the frontend, I send what data I need send to the printer. I used my endpoint /print
In the backend endpoint, my logic was the next:
Recover tokens and expiration date from database, with the expiration date check if the token has expired, and if It has already expired then gets another token and replace the old access token with the new one, replacing also with the new expiration date, to obtain this new data only is necessary call to method refreshAccessToken of module googleapis.Note: the refresh token never expires.
After having the access token updated, use it to send data to the printer with Google route(.../submit)
Code:
All the next codes are in only 1 file
Some data as validation, static variables, error handler, etc, has been removed to better understanding.
Route get URL authentication.
const express = require('express');
const google = require('googleapis');
const router = express.Router();
var OAuth2 = google.auth.OAuth2;
const redirect_url = 'http://localhost:3001/setting'; //Your redirect URL
var oauth2Client = new OAuth2(
'CLIENT ID', //Replace it with your client id
'CLIEND SECRET', //Replace it with your client secret
redirect_url
);
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: 'https://www.googleapis.com/auth/cloudprint'
});
router.get('/googleurl', (req, res) => {
return res.status(200).send({
result: { googleURLToken: url }
});
});
To get tokens using the authentication code and save these in the database.
const Setting = require('../models/setting'); // My model(Mongoose)
router.post('/googletoken', (req, res) => {
oauth2Client.getToken(req.body.code, function (err, tokens) {
oauth2Client.credentials = tokens;
// If refresh token exits save it
// because the refresh token it returned only 1 time! IMPORTANT
if (tokens.hasOwnProperty('refresh_token')) {
let setting = new Setting();
setting.refreshTokenGoogle = tokens.refresh_token;
setting.expirationTokenGoogle = tokens.expiry_date;
setting.tokenGoogle = tokens.access_token;
setting.save()
.then((settingCreated) => {
return res.status(200).send({
message: 'OK'
});
})
}
});
});
To print
const axios = require('axios');
const moment = require('moment');
router.post('/print',async (req, res) => {
const tickeProperties = {
'version': '1.0',
'print': {
'vendor_ticket_item': [],
'color': { 'type': 'STANDARD_MONOCHROME' },
'copies': { 'copies': 1 }
}
};
const accessToken = await getTokenGoogleUpdated();
axios.get(
'https://www.google.com/cloudprint/submit',
{
params: {
printerid : printerID, // Replace by your printer ID
title: 'title printer',
ticket: tickeProperties,
content : 'print this text of example!!!',
contentType: 'text/plain'
},
headers: {
'Authorization': 'Bearer ' + accessToken
}
}
)
.then(response => {
return res.status(200).send({
result: response.data
});
})
}
);
async function getTokenGoogleUpdated() {
return await Setting.find({})
.then(async setting => {
const refreshTokenGoogle = setting[0].refreshTokenGoogle;
const expirationTokenGoogle = setting[0].expirationTokenGoogle;
const tokenGoogle = setting[0].tokenGoogle;
const dateToday = new Date();
// 1 minute forward to avoid exact time
const dateTodayPlus1Minute = moment(dateToday).add(1, 'm').toDate();
const dateExpiration = new Date(expirationTokenGoogle);
// Case date expiration, get new token
if (dateExpiration < dateTodayPlus1Minute) {
console.log('Updating access token');
oauth2Client.credentials['refresh_token'] = refreshTokenGoogle;
return await oauth2Client.refreshAccessToken( async function(err, tokens) {
// Save new token and new expiration
setting[0].expirationTokenGoogle = tokens.expiry_date;
setting[0].tokenGoogle = tokens.access_token;
await setting[0].save();
return tokens.access_token;
});
} else {
console.log('Using old access token');
return tokenGoogle;
}
})
.catch(err => {
console.log(err);
});
}
I hope It helps you if you want to use Google Cloud Print to not waste a lot of time as I did.
The important part there is a scope https://www.googleapis.com/auth/cloudprint which is not obvious and took one day for me to figure out.

Resources