Google Calendar API with Service User - Reminder overrides are not set - node.js

Problem:
When using the service user auth and inserting / updating a calendar event, the reminders are not overridden
The event is inserted / updated correct APART from the reminders are always at the default (email > 10m, popup > 30m).
Context:
Node.js using standard libraries below
Valid service account with downloaded credentials.json
Service account (service-user-account#myapp.iam.gserviceaccount.com) has write access to myuser#gmail.com calendar
Code:
const {google} = require('googleapis')
const {auth} = require('google-auth-library')
const credentials = require('./credentials.json')
const addEvent = async (auth) => {
const calendar = google.calendar({version: 'v3', auth})
const insertRes = await calendar.events.insert({
calendarId: 'myuser#gmail.com',
resource: {
summary: 'Test API',
start: {
dateTime: '2020-06-02T12:55:00.000',
timeZone: 'Europe/London'
},
end: {
dateTime: '2020-06-02T12:56:00.000',
timeZone: 'Europe/London'
},
reminders: {
useDefault: false,
overrides: [
{method: 'popup', 'minutes': 5}
]
}
}
})
console.log('insertRes', insertRes.data)
}
const getAuth = async () => {
let client = auth.fromJSON(credentials)
client.scopes = ['https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events']
return client
}
const init = async () => {
const auth = await getAuth()
await addEvent(auth)
}
init()
Response: from console.log(insertRes)
{ kind: 'calendar#event',
etag: '"3182200547452000"',
id: '6063phndufgppo8rfev1XXXXXX',
status: 'confirmed',
htmlLink:
'https://www.google.com/calendar/event?eid=NjA2M3BobmR1ZmdwcG84cmZldjFjdWh2YzQgZGFuZ2FyZmllbGR1a0Bnb29nbGVtYWlsXXXXXX',
created: '2020-06-02T12:17:53.000Z',
updated: '2020-06-02T12:17:53.768Z',
summary: 'Test API',
creator:
{ email: 'service-user-account#myapp.iam.gserviceaccount.com' },
organizer: { email: 'myuser#googlemail.com', self: true },
start:
{ dateTime: '2020-06-02T12:55:00+01:00',
timeZone: 'Europe/London' },
end:
{ dateTime: '2020-06-02T12:56:00+01:00',
timeZone: 'Europe/London' },
iCalUID: '6063phndufgppo8rfev1XXXXXX#google.com',
sequence: 0,
reminders: { useDefault: false, overrides: [{"method":"popup","minutes":5}] }
}
Hopefully someone can shed a light on the issue for me.
Thanks

It seems to be a bug, already reported on Google's Public Issue Tracker
Give it a "star" to show that more people are affected and to receive updates on the issue.

The service account user is a user more or less just like you. One can only modify reminders for the current user, not for other users.
The reminder is set, but for the "wrong" user. Try to get the event through api with the service account user, you will see the reminder there. Add a reminder with your own user through UI and do the api request again, you will not be able to see the new reminder.
If you want to set reminders on events for yourself, be sure to use your own account to modify the event, for example with OAuth 2.0.

Related

How to edit event in google calendar created by service account

I am emailing an HTML link of a Google calendar event generated by Google API to users but they are unable to edit the event, they can only view it when they click the link. I am creating this event with a service account and sharing with other users.
How can I ensure these events are editable in the user's calendar?
This is a link to the code I am using:
const { google } = require('googleapis');
// Provide the required configuration
const CREDENTIALS = JSON.parse(process.env.CREDENTIALS);
const calendarId = process.env.CALENDAR_ID;
// Google calendar API settings
const SCOPES = ['https://www.googleapis.com/auth/calendar.events'];
const calendar = google.calendar({version : "v3"});
const auth = new google.auth.JWT(
CREDENTIALS.client_email,
null,
CREDENTIALS.private_key,
SCOPES,
'email_used_to_configure_the_service_account#gmail.com'
);
auth.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("Successfully connected!");
}
});
//fetching the even object from the db to get evnt.name and co
const saveEvent = {
summary: event.name,
location: event.room.host.location,
description: event.extra,
colorId: 3,
start: {
dateTime: event.startDate,
timeZone: 'Africa/Lagos',
},
end: {
dateTime: event.endDate,
timeZone: 'Africa/Lagos',
},
organizer: {
email: 'email_used_to_configure_the_service_account#gmail.com',
displayName: 'display name',
self: true
},
attendees: [{ email: 'email of recepient of event' }]
//visibility: 'public'
}
async function generateLink(){
try{
const val = await calendar.events.insert({ auth: auth, calendarId: calendarId, resource: saveEvent, sendNotifications: true });
if(val.status === 200 && val.statusText === 'OK'){
console.log('CREATED', val);
return val.data.htmlLink;
}
return console.log('NOT CREATED')
} catch(error){
console.log(`Error ${error}`);
return;
}
}
const link = await generateLink();
let mailData = {
name: user.name ? user.name : user.firstname,
token: `${config.get('platform.url')}/event-accepted/${invite.token}`,
coverImage: event.gallery.link,
eventName: event.name,
hostName: host.name? host.name : host.firstname,
venue: event.venue,
date: event.startDate,
time: event.startDate,
attendees: '',
ticketNo: '',
cost: event.amount,
action: link // htmlLink that takes you to the calendar where user can edit event.
}
mail.sendTemplate({
template: 'acceptEventEmail',
to: u.email.value,
context: mailData
});
Current Behaviour
Expected behaviour
P.S: Code has been added to the question
YOu created the event using a Service account, think of a service account as a dummy user. When it created the event it became the owner / organizer of the event and there for only the service account can make changes to it.
You either need the service account to update it and set someone else as organizer
"organizer": {
"email": "me#gmail.com",
"displayName": "L P",
"self": true
},
Service accounts cannot invite attendees without Domain-Wide Delegation of Authority
In enterprise applications you may want to programmatically access users data without any manual authorization on their part. In Google Workspace domains, the domain administrator can grant to third party applications domain-wide access to its users' data—this is referred as domain-wide delegation of authority. To delegate authority this way, domain administrators can use service accounts with OAuth 2.0.

Stripe processed, now what?

I'm trying to build a serverless NuxtJS app, utilizing firebase for authentication, netlify for deployment (and functions) and stripe for payment.
This whole payment-process and serverless functions on netlify is all new to me, so this might be a nooby question.
I've followed serveral docs and guides, and accomplished an app with firebase authentication, netlify deployment and serverless functions making me able to process a stripe payment - now I just cant figure out the next step. My stripe success_url leads to a /pages/succes/index.js route containing a success message -> though here I'd need some data response from Stripe, making me able to present the purchased item and also attaching the product id as a "bought-product" entry on the user object in firebase (the products will essentially be an upgrade to the user profile).
Click "buy product" function
async buyProduct(sku, qty) {
const data = {
sku: sku,
quantity: qty,
};
const response = await fetch('/.netlify/functions/create-checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((res) => res.json());
console.log(response);
const stripe = await loadStripe(response.publishableKey);
const { error } = await stripe.redirectToCheckout({
sessionId: response.sessionId,
});
if (error) {
console.error(error);
}
}
create-checkout Netlify function
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const inventory = require('./data/products.json');
exports.handler = async (event) => {
const { sku, quantity } = JSON.parse(event.body);
const product = inventory.find((p) => p.sku === sku);
const validatedQuantity = quantity > 0 && quantity < 11 ? quantity : 1;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
billing_address_collection: 'auto',
shipping_address_collection: {
allowed_countries: ['US', 'CA'],
},
success_url: `${process.env.URL}/success`,
cancel_url: process.env.URL,
line_items: [
{
name: product.name,
description: product.description,
images: [product.image],
amount: product.amount,
currency: product.currency,
quantity: validatedQuantity,
},
],
});
return {
statusCode: 200,
body: JSON.stringify({
sessionId: session.id,
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
}),
};
};
Please let me know if you need more information, or something doesnt make sense!
tldr; I've processed a Stripe payment in a serverless app using Netlify function, and would like on the success-page to be able to access the bought item and user information.
When you create your Checkout Session and define your success_url, you can append session_id={CHECKOUT_SESSION_ID} as a template where Stripe will auto-fill in the session ID. See https://stripe.com/docs/payments/checkout/accept-a-payment#create-checkout-session
In your case you'd do:
success_url: `${process.env.URL}/success?session_id={CHECKOUT_SESION_ID}`,
Then when your user is redirected to your success URL you can retrieve the session with the session ID and do any purchase fulfilment.

Google Calendar API Node JS, can't add attendees with service account

I'm currently trying to develop a simple Node JS program deployed on a GCP Cloud Function, to use Google Calendar API with a Service Account.
The program is simple, i just want to create an event and add attendees.
My code works well, however i can't add attendees with the service account i have this error :
There was an error contacting the Calendar service: Error: Service accounts cannot invite attendees without Domain-Wide Delegation of Authority.
The API are activated and i allowed the calendar scope on the GSuite Admin Security, my service account have the delegation authority checked on GCP.
I search a lot a solution but nothing solve my problem..
This is the code of my module :
const { google } = require('googleapis');
const sheets = google.sheets('v4');
const SCOPES = ['https://www.googleapis.com/auth/spreadsheets','https://www.googleapis.com/auth/calendar'];
let privatekey = require("../creds.json");
async function getAuthToken() {
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
SCOPES);
//authenticate request
jwtClient.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("Successfully connected!");
}
});
return jwtClient;
}
async function insertEvents(auth,items) {
const calendar = google.calendar({ version: 'v3', auth });
var startDate = new Date();
var endDate = new Date();
startDate.setHours(14, 0, 0, 0);
endDate.setHours(15, 0, 0, 0);
var attendees = [];
items.forEach(function(item){
attendees.push({email: item[2]});
});
var event = {
summary: 'Stack Roulette : It\'s Coffee Time =)',
location: 'Partout, mais avec un café',
description: "",
start: {
dateTime: startDate.toISOString(),
timeZone: 'Europe/London'
},
end: {
dateTime: endDate.toISOString(),
timeZone: 'Europe/London'
},
attendees: attendees,
reminders: {
useDefault: false,
overrides: [
{ method: 'email', minutes: 24 * 60 },
{ method: 'popup', minutes: 10 }
]
},
conferenceData: {
createRequest: {requestId: Math.random().toString(36).substring(2, 10),
conferenceSolution: {
key: {
type: "hangoutsMeet"
}
},}
}
};
calendar.events.insert(
{
auth: auth,
calendarId: 'primary',
resource: event,
conferenceDataVersion: 1
},
function(err, event) {
if (err) {
console.log(
'There was an error contacting the Calendar service: ' + err
);
return;
}
console.log('Event created: %s', event.data.htmlLink);
}
);
}
module.exports = {
getAuthToken,
getSpreadSheetValues,
insertEvents
}
I precise, there is no Frontend for my application, the code run with a cloud Function like an API Endpoint.
PS : it's not a Firebase Cloud Function, but GCP Cloud Function
If i remove attendees from event creation, it's work well but can't see the event.
Thx for your help
To set up Domain-Wide Delegation of Authority
Visit your cloud console
Go to IAM & Admin -> Service Accounts
Chose the service account you using in your script
Check Enable G Suite Domain-wide Delegation
If you are sure that you already enabled the domain-wide delegation for the correct service account and you encounter issues only when creating events with attendees:
Your issue is likely related to this issue reported on Google's Issue Tracker:
Currently there are restrictions for creation of events with attendees with a service account, especially when the attendees are outside of your domain.
When you create jwtClient, I think you have to put user primary email to JWT method.
Like:
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
SCOPES,
"yourUserPrimaryEmail"); // here

Not able to watch Admin Users Directory using `google-admin-sdk`

I am trying to connect to the G-Suite's User directory using the google-admin-sdk. I am using an API Key for authorization and I am not able to reach a successful execution.
Here is the code snippet that I'm using:
import { google } from 'googleapis';
import uuid from 'uuid/v4';
const API_KEY = 'my api key goes here';
google.admin({
version: 'directory_v1',
auth: API_KEY
}).users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res: any) => {
if (err) { return console.error('The API returned an error:', err.message); }
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user: any) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Output:
Login Required
Can someone tell me what I am doing wrong here?
Also, how do I proceed further for listening to the events emitted by the Google API?
---UPDATE---
Here is the snippet that works for me now:
import { JWT } from 'google-auth-library';
import { google } from 'googleapis';
// Importing the serivce account credentials
import { credentials } from './credentials';
const scopes = ['https://www.googleapis.com/auth/admin.directory.user'];
const adminEmail = 'admin_account_email_address_goes_here';
const myDomain = 'domain_name_goes_here';
async function main () {
const client = new JWT(
credentials.client_email,
undefined,
credentials.private_key,
scopes,
adminEmail
);
await client.authorize();
const service = google.admin('directory_v1');
const res = await service.users.list({
domain: myDomain,
auth: client
});
console.log(res);
}
main().catch(console.error);
--- Bonus Tip ---
If you face any Parse Errors while using other methods of the directory, remember to JSON.stringify the request body. For example, on the admin.users.watch method:
// Watch Request
const channelID = 'channel_id_goes_here';
const address = 'https://your-domain.goes/here/notifications';
const ttl = 3600; // Or any other TTL that you can think of
const domain = 'https://your-domain.goes';
const body = {
id: channelID,
type: 'web_hook',
address,
params: {
ttl,
},
};
// Remember to put this in an async function
const res = await service.users.watch({
domain,
customer: 'my_customer',
auth: client, // get the auth-client from above
event: 'add'
}, {
headers: {
'Content-Type': 'application/json'
},
// This is the important part
body: JSON.stringify(body),
});
As you can see in the official documentation, every request sent "to the Directory API must include an authorization token". In order to authorize your request, you have to use OAuth 2.0.
You are providing an API key instead, which is not appropriate for this process. API keys are usually used for accessing public data, not users' private data as in your current situation.
You should follow the steps provided in the Node.js Quickstart instead:
First, obtain client credentials from the Google API Console.
Second, authorize the client: obtain an access token after setting the user credentials and the appropriate scopes (a process accomplish in functions authorize and getNewToken in the Quickstart).
Finally, once the client is authorized, call the API (function listUsers).
Update:
If you want to use a Service Account for this, you will have to follow these steps:
Grant domain-wide delegation to the Service Account by following the steps specified here.
In the Cloud console, create a private key for the Service Account and download the corresponding JSON file. Copy it to your directory.
Use the Service Account to impersonate a user who has access to this resource (an Admin account). This is achieved by indicating the user's email address when creating the JWT auth client, as indicated in the sample below.
The code could be something along the following lines:
const {google} = require('googleapis');
const key = require('./credentials.json'); // The name of the JSON you downloaded
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/admin.directory.user'],
"admin#domain" // Please change this accordingly
);
// Create the Directory service.
const service = google.admin({version: 'directory_v1', auth: jwtClient});
service.users.list({
customer: 'my_customer',
maxResults: 10,
orderBy: 'email',
}, (err, res) => {
if (err) return console.error('The API returned an error:', err.message);
const users = res.data.users;
if (users.length) {
console.log('Users:');
users.forEach((user) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log('No users found.');
}
});
Reference:
Directory API: Authorize Requests
Directory API: Node.js Quickstart
Delegate domain-wide authority to your service account
Google Auth Library for Node.js
I hope this is of any help.

Why are the Stripe Payments Incomplete?

My Stripe payments show up on my dashboard, but they have an 'Incomplete' status, and hovering over shows the tip, "The customer has not entered their payment method." I thought I accounted for payment method in my sessions.create() method.
My Angular component creates a StripeCheckout and sends the session data to my API. API then includes it in the response to the browser (my first attempt was to use this sessionId to redirect to checkout, but I chose this because it was smoother).
Angular/TS StripeCheckout and handler:
let headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append(
"authentication",
`Bearer ${this.authServ.getAuthenticatedUserToken()}`
);
this.handler = StripeCheckout.configure({
key: environment.stripe_public_key,
locale: "auto",
source: async source => {
this.isLoading = true;
this.amount = this.amount * 100;
this.sessionURL = `${this.stringValServ.getCheckoutSessionStripeURL}/${this.activeUser.id}/${this.amount}`;
const session = this.http
.get(this.sessionURL, {
headers: new HttpHeaders({
"Content-Type": "application/json",
authentication: `Bearer ${this.authServ.getAuthenticatedUserToken()}`
})
})
.subscribe(res => {
console.log(res);
});
}
});
//so that the charge is depicted correctly on the front end
this.amount = this.amount / 100;
}
async checkout(e) {
this.stripeAmountEvent.emit(this.amount);
this.handler.open({
name: "[REDACTED]",
amount: this.amount * 100,
description: this.description,
email: this.activeUser.email
});
e.preventDefault();
}
NodeJS API picks it up
exports.getCheckoutSession = catchAsync(async (req, res, next) => {
const currentUserId = req.params.userId;
const paymentAmount = req.params.amount;
const user = await User.findById(currentUserId);
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
success_url: `${process.env.CLIENT_URL}`,
cancel_url: `${process.env.CLIENT_URL}`,
customer_email: user.email,
client_reference_id: user.userId,
line_items: [
{
name: `Donation from ${user.userName}`,
description: '[REDACTED]',
amount: paymentAmount,
currency: 'usd',
quantity: 1,
customer: user.userId
}
]
});
const newPayment = await Payment.create({
amount: paymentAmount,
createdAt: Date.now(),
createdById: user._id
});
res.status(200).send({
status: 'success',
session
});
});
The payment gets created in my db, and the payment shows up on my Stripe dashboard. The payments show as 'Incomplete' when I expected it to charge the card.
Thanks in advance.
The latest version of Checkout lets you accept payments directly on a Stripe-hosted payment page. This includes collecting card details, showing what's in your cart and ensuring the customer pays before being redirected to your website.
At the moment, though, your code is mixing multiple products in one place incorrectly. The code you have client-side uses Legacy Checkout. This is an older version of Stripe's product that you could use to collect card details securely. This is not something you should use anymore.
Then server-side, you are using the newer version of Checkout by creating a Session. This part is correct but you never seem to be using it.
Instead, you need to create the Session server-side, and then client-side you only need to redirect to Stripe using redirectToCheckout as documented here.

Resources