Merge pull request #87 from emrysal/main

Adds Office 365 / Outlook.com Calendar Integration
This commit is contained in:
Bailey Pumfleet authored and GitHub committed 2021-04-22 15:04:56 +01:00
commit 6091f7ba86
12 files changed
+365 -110

No files matched your search

+3 -31
View File
@@ -1,8 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import prisma from '../../../lib/prisma';
const {google} = require('googleapis');
const credentials = process.env.GOOGLE_API_CREDENTIALS;
import { getBusyTimes } from '../../../lib/calendarClient';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { user } = req.query
@@ -17,32 +15,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
});
let availability = [];
authorise(getAvailability)
// Set up Google API credentials
function authorise(callback) {
const {client_secret, client_id, redirect_uris} = JSON.parse(credentials).web;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
oAuth2Client.setCredentials(currentUser.credentials[0].key);
callback(oAuth2Client)
}
function getAvailability(auth) {
const calendar = google.calendar({version: 'v3', auth});
calendar.freebusy.query({
requestBody: {
timeMin: req.query.dateFrom,
timeMax: req.query.dateTo,
items: [{
"id": "primary"
}]
}
}, (err, apires) => {
if (err) return console.log('The API returned an error: ' + err);
availability = apires.data.calendars;
res.status(200).json(availability);
});
}
const availability = await getBusyTimes(currentUser.credentials, req.query.dateFrom, req.query.dateTo);
res.status(200).json(availability);
}
+14 -48
View File
@@ -1,8 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import prisma from '../../../lib/prisma';
const {google} = require('googleapis');
const credentials = process.env.GOOGLE_API_CREDENTIALS;
import { createEvent, CalendarEvent } from '../../../lib/calendarClient';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { user } = req.query;
@@ -17,50 +15,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
});
authorise(bookEvent);
const evt: CalendarEvent = {
title: 'Meeting with ' + req.body.name,
description: req.body.notes,
startTime: req.body.start,
endTime: req.body.end,
timeZone: currentUser.timeZone,
attendees: [
{ email: req.body.email, name: req.body.name }
]
};
// Set up Google API credentials
function authorise(callback) {
const {client_secret, client_id, redirect_uris} = JSON.parse(credentials).web;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
oAuth2Client.setCredentials(currentUser.credentials[0].key);
callback(oAuth2Client);
}
function bookEvent(auth) {
var event = {
'summary': 'Meeting with ' + req.body.name,
'description': req.body.notes,
'start': {
'dateTime': req.body.start,
'timeZone': currentUser.timeZone,
},
'end': {
'dateTime': req.body.end,
'timeZone': currentUser.timeZone,
},
'attendees': [
{'email': req.body.email},
],
'reminders': {
'useDefault': false,
'overrides': [
{'method': 'email', 'minutes': 60}
],
},
};
const calendar = google.calendar({version: 'v3', auth});
calendar.events.insert({
auth: auth,
calendarId: 'primary',
resource: event,
}, function(err, event) {
if (err) {
console.log('There was an error contacting the Calendar service: ' + err);
return;
}
res.status(200).json({message: 'Event created'});
});
}
// TODO: for now, first integration created; primary = obvious todo; ability to change primary.
const result = await createEvent(currentUser.credentials[0], evt);
res.status(200).json(result);
}
+6 -1
View File
@@ -30,8 +30,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
// A refresh token is only returned the first time the user
// consents to providing access. For illustration purposes,
// setting the prompt to 'consent' will force this consent
// every time, forcing a refresh_token to be returned.
prompt: 'consent',
});
res.status(200).json({url: authUrl});
}
}
}
@@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getSession } from 'next-auth/client';
import prisma from '../../../../lib/prisma';
const scopes = ['User.Read', 'Calendars.Read', 'Calendars.ReadWrite'];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
// Check that user is authenticated
const session = await getSession({req: req});
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; }
// TODO: Add user ID to user session object
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true
}
});
const hostname = 'x-forwarded-host' in req.headers ? 'https://' + req.headers['x-forwarded-host'] : 'host' in req.headers ? (req.secure ? 'https://' : 'http://') + req.headers['host'] : '';
if ( ! hostname || ! req.headers.referer.startsWith(hostname)) {
throw new Error('Unable to determine external url, check server settings');
}
function generateAuthUrl() {
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=' + scopes.join(' ') + '&client_id=' + process.env.MS_GRAPH_CLIENT_ID + '&redirect_uri=' + hostname + '/api/integrations/office365calendar/callback';
}
res.status(200).json({url: generateAuthUrl() });
}
}
@@ -0,0 +1,54 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getSession } from 'next-auth/client';
import prisma from '../../../../lib/prisma';
const scopes = ['offline_access', 'Calendars.Read', 'Calendars.ReadWrite'];
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
// Check that user is authenticated
const session = await getSession({req: req});
if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; }
// TODO: Add user ID to user session object
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true
}
});
const toUrlEncoded = payload => Object.keys(payload).map( (key) => key + '=' + encodeURIComponent(payload[ key ]) ).join('&');
const hostname = 'x-forwarded-host' in req.headers ? 'https://' + req.headers['x-forwarded-host'] : 'host' in req.headers ? (req.secure ? 'https://' : 'http://') + req.headers['host'] : '';
const body = toUrlEncoded({ client_id: process.env.MS_GRAPH_CLIENT_ID, grant_type: 'authorization_code', code, scope: scopes.join(' '), redirect_uri: hostname + '/api/integrations/office365calendar/callback', client_secret: process.env.MS_GRAPH_CLIENT_SECRET });
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { method: 'POST', headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
}, body });
const responseBody = await response.json();
if (!response.ok) {
return res.redirect('/integrations?error=' + JSON.stringify(responseBody));
}
const whoami = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { 'Authorization': 'Bearer ' + responseBody.access_token } });
const graphUser = await whoami.json();
responseBody.email = graphUser.mail;
responseBody.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in); // set expiry date in seconds
delete responseBody.expires_in;
const credential = await prisma.credential.create({
data: {
type: 'office365_calendar',
key: responseBody,
userId: user.id
}
});
return res.redirect('/integrations');
}