diff --git a/.env.example b/.env.example index 6b4f0c43..19383ec6 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,7 @@ MAILCHIMP_API_KEY= MAILCHIMP_SERVER= MAILCHIMP_LIST_ID= # if null, it will be set to https://devedmonton.com -NUXT_PUBLIC_SITE_URL= \ No newline at end of file +NUXT_PUBLIC_SITE_URL= +# these are needed for the google calendar api +GOOGLE_CALENDAR_ID= +GOOGLE_SERVICE_ACCOUNT_CREDENTIALS_JSON= diff --git a/nuxt.config.ts b/nuxt.config.ts index be5e70a1..76977778 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -17,6 +17,10 @@ export default defineNuxtConfig({ server: process.env.MAILCHIMP_SERVER ?? 'us20', listId: process.env.MAILCHIMP_LIST_ID, }, + googleCalendarAPI: { + googleCalendarId: process.env.GOOGLE_CALENDAR_ID, + serviceAccountCredentialsJSON: process.env.GOOGLE_SERVICE_ACCOUNT_CREDENTIALS_JSON, + }, }, app: { diff --git a/package-lock.json b/package-lock.json index 0bfd8d27..d646fa5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,8 @@ "@vueuse/core": "^10.9.0", "@vueuse/nuxt": "^10.9.0", "eslint": "^9.0.0", + "google-auth-library": "^9.9.0", + "googleapis": "^136.0.0", "happy-dom": "^13.10.1", "nuxt": "^3.11.1", "nuxt-icon": "^0.6.10", @@ -7681,6 +7683,15 @@ } ] }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -7855,6 +7866,12 @@ "node": ">=8.0.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -9514,6 +9531,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", @@ -10859,6 +10885,72 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/gaxios": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.5.0.tgz", + "integrity": "sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dev": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11092,6 +11184,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.9.0.tgz", + "integrity": "sha512-9l+zO07h1tDJdIHN74SpnWIlNR+OuOemXlWJlLP9pXy6vFtizgpEzMuwJa4lqY9UAdiAv5DVd5ql0Am916I+aA==", + "dev": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "136.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-136.0.0.tgz", + "integrity": "sha512-W2Z1NtFS8ie5SzN74+lnTzLS9d0tS01dybibqXjWKSfdvTkQr9DYsKYNMRpzMAcBva1ZWCAgTiX4fgGz2Cwbaw==", + "dev": true, + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -11116,6 +11255,19 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dev": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -12338,6 +12490,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -12432,6 +12593,27 @@ "jsonrepair": "bin/cli.js" } }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dev": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -20562,6 +20744,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "dev": true + }, "node_modules/urlpattern-polyfill": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", @@ -20583,6 +20771,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v-lazy-show": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/v-lazy-show/-/v-lazy-show-0.2.4.tgz", diff --git a/package.json b/package.json index 02cb41b1..0eeca9c7 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "@vueuse/core": "^10.9.0", "@vueuse/nuxt": "^10.9.0", "eslint": "^9.0.0", + "google-auth-library": "^9.9.0", + "googleapis": "^136.0.0", "happy-dom": "^13.10.1", "nuxt": "^3.11.1", "nuxt-icon": "^0.6.10", diff --git a/server/api/events.js b/server/api/events.js new file mode 100644 index 00000000..2e9c92e4 --- /dev/null +++ b/server/api/events.js @@ -0,0 +1,121 @@ +import { google } from 'googleapis' +import { JWT } from 'google-auth-library' + +/* +Reference for folks who maybe curious. +- The google calendar id of the user you want to follow the events. + - Refer to here https://it.umn.edu/services-technologies/how-tos/google-calendar-find-your-google#:~:text=Click%20on%20the%20three%20vertical,will%20find%20your%20Calendar%20ID. +- A service account + - Refer to here https://dev.to/pedrohase/create-google-calender-events-using-the-google-api-and-service-accounts-in-nodejs-22m8 +- The google calendar user needs to give the "Make Changes to Events" permissions to the service account + - Refer to here https://dev.to/pedrohase/create-google-calender-events-using-the-google-api-and-service-accounts-in-nodejs-22m8 +*/ + +/** + * Retrieves events from Google Calendar. + * @async + * @param {Object} options - The options object. + * @param {string} options.googleCalendarId - The ID of the Google Calendar should be an email. + * @param {Object} options.serviceAccountCredentials - The service account credentials for google authentication. + * @param {Date} options.startDate - The start date from which to retrieve events. + * @param {number} options.limitEvents - The maximum number of events to retrieve. + * @returns {Promise} An array of events. + */ +const getEvents = async ({ + googleCalendarId, + serviceAccountCredentials, + startDate, + limitEvents, +}) => { + if (!startDate) { + startDate = new Date().toISOString() + } + else { + startDate = new Date(startDate).toISOString() + } + + if (!limitEvents) { + limitEvents = 10 + } + + // get the credentials from the service account + const client = new JWT({ + email: serviceAccountCredentials.client_email, + key: serviceAccountCredentials.private_key, + scopes: [ // set the right scope + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/calendar.events', + ], + }) + + // get the calendar api using google + const calendar = google.calendar({ version: 'v3' }) + // get the events from the calendar + const resposnse = await calendar.events.list({ + auth: client, + calendarId: googleCalendarId, + timeMin: startDate, + maxResults: limitEvents, + singleEvents: true, + orderBy: 'startTime', + }) + + // return the events. + return resposnse.data.items +} +/** + * API endpoint for fetching events for DES. + * @param {Object} event - The event object. + * @param {string} startDate - Start date of the events you want to fetch (query param). + * @param {number} limitEvents - Limit the number of events to fetch (query param) + * @example + * // Example usage: + * // GET /api/events?startDate=2024-05-01&limitEvents=25 + * // GET /api/events + */ +// eslint-disable-next-line +export default defineEventHandler(async (event) => { + // get the query params + // eslint-disable-next-line + const { startDate, limitEvents } = getQuery(event) + + // get the credentials from the service account + const { googleCalendarId, serviceAccountCredentialsJSON } = useRuntimeConfig(event).googleCalendarAPI + + let serviceAccountCredentials + try { + serviceAccountCredentials = JSON.parse(serviceAccountCredentialsJSON) + } + catch { + throw createError({ + statusCode: 400, + statusMessage: 'Bad Request', + message: 'No Service Account Credentials Provided', + error: 'Service Account Credentials are not provided', + }) + } + + try { + const events = await getEvents({ + googleCalendarId, + serviceAccountCredentials, + startDate, + limitEvents, + }) + + // working version. + return { + statusCode: 200, + statusMessage: 'Success', + message: 'Events fetched successfully', + events, + } + } + catch { + throw createError({ + statusCode: 400, + statusMessage: 'Bad Request', + message: 'Invalid Request to the Calendar API', + }) + } +})