Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google Calendar Integration #183

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
17 changes: 16 additions & 1 deletion documentation/EnvironmentSetup.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,26 @@ The above is just a placeholder, you'll need to fill in each entry with the appr

10. On the next screen, select `Web application`. Name the OAuth client ID something like `SSEquel Dev`. Under `Authorized JavaScript origins`, add `http://localhost:3000`. Under `Authorized redirect URIs`, add `http://localhost:3000/api/auth/callback/google`. Click `Create`. You should be presented with a modal titled `OAuth client created`.

11. Congratulations, you've created a Google OAuth client ID! You can now fill in the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` entries in the `.env` file.
11. Congratulations, you've created a Google OAuth client ID! You can now fill in the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` entries in the `.env` file.

The `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` can be found again later by going to the [Credentials tab](https://console.cloud.google.com/apis/credentials) and clicking on the client ID under `OAuth 2.0 Client IDs`.

## Setting up Google Calendar

1. Go to the [Google Service Accounts](https://console.developers.google.com/iam-admin/serviceaccounts) page and select/create a project.

2. Click `+ Create Service Account`. Enter any name, id, and description.

3. Click `Create and Continue`, then `Continue`, then `Done`.

4. Click the email address of the account you just created and click the `Keys` tab.

5. In the `Add Key` drop-down list, select `Create new Key` and click `Create`. Your browser will download a JSON file. Keep this somewhere safe.

6. Copy the `client_email` from this file to the `GCAL_CLIENT_EMAIL` entry in the `.env` file. Copy the `private_key` to the `GCAL_PRIVATE_KEY` entry.

## Building the Local Database

If you run the project now, you'll encounter schema errors. This is because the local database hasn't been built. We use Prisma for managing the Postgres database, so we'll use [Prisma's migrate command](https://www.prisma.io/docs/concepts/components/prisma-migrate/migrate-development-production) to build the db tables using the schema defined in the [schema.prisma](../next/prisma/schema.prisma) file.

In the /next/ directory, run `npx prisma migrate dev`. Then run `npx prisma db seed` to populate the database with test data.
Expand Down
24 changes: 24 additions & 0 deletions next/app/api/calendar/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextRequest } from "next/server";
import { getToken } from "../../../../lib/calendar";

/**
* HTTP GET to /api/calendar/[id]
*
* List all of the events in the calendar
*
* @param request
* @returns
*/
export async function GET(
request: NextRequest,
{ params: { id } }: { params: { id: string } }
) {
const gcal_token = await getToken();

return await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${
process.env.GCAL_CAL_ID || "primary"
}/events/${id}`,
{ headers: { Authorization: `Bearer ${gcal_token}` } }
);
}
126 changes: 126 additions & 0 deletions next/app/api/calendar/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { NextRequest } from "next/server";
import { getToken } from "../../../lib/calendar";

/**
* HTTP GET to /api/calendar
*
* List all of the events in the calendar
*
* @param request
* @returns
*/
export async function GET(request: NextRequest) {
const gcal_token = await getToken();

return await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${
process.env.GCAL_CAL_ID || "primary"
}/events`,
{
headers: { Authorization: `Bearer ${gcal_token}` },
}
);
}

/**
* HTTP POST to /api/calendar
*
* Creates a new event
*
* @param request
* @returns
*/
export async function POST(request: NextRequest) {
let body;
try {
body = await request.json();
} catch {
return new Response("Invalid JSON", { status: 422 });
}

// verify the id is included
if (
!(
"summary" in body &&
"description" in body &&
"location" in body &&
"start" in body &&
"end" in body
)
) {
return new Response("ID must be included", { status: 422 });
}

const gcal_token = await getToken();

return await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${
process.env.GCAL_CAL_ID || "primary"
}/events`,
{
method: "POST",
headers: { Authorization: `Bearer ${gcal_token}` },
body: JSON.stringify({
summary: body.summary,
description: body.description,
location: body.location,
start: body.start,
end: body.end,
}),
}
);
}

/**
* HTTP PUT to /api/calendar
*
* Edits an event
*
* Internally, this is mapped go gcal's PATCH method because it supports partial changes
*
* @param request
* @returns
*/
export async function PUT(request: NextRequest) {
const gcal_token = await getToken();

// TODO: Request Body
return await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${
process.env.GCAL_CAL_ID || "primary"
}/events`,
{ method: "PATCH", headers: { Authorization: `Bearer ${gcal_token}` } }
);
}

/**
* HTTP DELETE to /api/calendar
*
* Deletes an event
*
* @param request
* @returns
*/
export async function DELETE(request: NextRequest) {
let body;
try {
body = await request.json();
} catch {
return new Response("Invalid JSON", { status: 422 });
}

// verify the id is included
if (!("id" in body)) {
return new Response("ID must be included", { status: 422 });
}
const id = body.id;

const gcal_token = await getToken();

return await fetch(
`https://www.googleapis.com/calendar/v3/calendars/${
process.env.GCAL_CAL_ID || "primary"
}/events/${id}`,
{ method: "DELETE", headers: { Authorization: `Bearer ${gcal_token}` } }
);
}
50 changes: 50 additions & 0 deletions next/lib/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import jwt from "jsonwebtoken";
import fs from "fs";

let accessToken = new Promise<string>((resolve, reject) => {
resolve("");
});
let expiry = 0;

export const getToken = async () => {
if (expiry > Math.floor(Date.now() / 1000) + 3300) {
return await accessToken;
}

accessToken = new Promise<string>(async (resolve, reject) => {
const token = jwt.sign(
{
iss: process.env.GCAL_CLIENT_EMAIL,
scope:
"https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events",
aud: "https://oauth2.googleapis.com/token",
exp: Math.floor(Date.now() / 1000) + 3600,
iat: Math.floor(Date.now() / 1000),
},
process.env.GCAL_PRIVATE_KEY as string,
{
algorithm: "RS256",
}
);

const res = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
body: JSON.stringify({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: token,
}),
headers: {
"Content-Type": "application/json",
},
});

const data = (await res.json()) as any;
accessToken = data.access_token;
expiry = Math.floor(Date.now() / 1000) + data.expires_in;
resolve(accessToken);
});

// set the expiry so that anyone else calling this function awaits our promise
expiry = Math.floor(Date.now() / 1000) + 3600;
return await accessToken;
};
1 change: 1 addition & 0 deletions next/lib/middlewares/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const nonGetMentorVerifier = nonGetVerifier(mentorVerifier);
* correspond to the key "golinks"
*/
const ROUTES: { [key: string]: AuthVerifier } = {
calendar: nonGetOfficerVerifier,
course: nonGetOfficerVerifier,
courseTaken: nonGetMentorVerifier,
departments: nonGetOfficerVerifier,
Expand Down
Loading