From 5004b37547eeea89cee03e9a09f2b8e437d6d094 Mon Sep 17 00:00:00 2001 From: Harsha Srikara Date: Tue, 2 Feb 2021 20:24:17 -0600 Subject: [PATCH 1/9] 2-way document storage + documentation --- functions/src/admin/readme.md | 20 +++++++ functions/src/application/portal.ts | 12 +++- functions/src/application/readme.md | 6 ++ functions/src/application/typeform.ts | 16 ++++- functions/src/express_configs/readme.md | 49 +++++++++++++++ readme.md | 79 +++---------------------- 6 files changed, 107 insertions(+), 75 deletions(-) create mode 100644 functions/src/admin/readme.md create mode 100644 functions/src/application/readme.md create mode 100644 functions/src/express_configs/readme.md diff --git a/functions/src/admin/readme.md b/functions/src/admin/readme.md new file mode 100644 index 0000000..43eb73c --- /dev/null +++ b/functions/src/admin/readme.md @@ -0,0 +1,20 @@ +# Firebase Admin + +`admin.ts` handles centrally initializing the Firebase Admin to access various different services across different files / functions. + +### Usage + +When needing to use additional services from firebase instantiate them in `admin.ts` and import them where needed. Also use this place to pass in custom arguments or different configuration parameters. + +### Notes + + - The `service-account.json` file is not passed in as a paramter to `admin.initializeApp()` because the backend is deployed on Firebase Functions and those variables are automatically accessible as environment variables. If the backend is shifted elsewhere then those variables will need to be passed in. + - If you choose write any standalone scripts store them in this folder. + +### Questions + +Sometimes you may have additional questions. If the answer was not found in this readme please feel free to reach out to the [Director of Development](mailto:development@acmutd.co) for _ACM_ + +We request that you be as detailed as possible in your questions, doubts or concerns to ensure that we can be of maximum assistance. Thank you! + +![ACM Development](https://www.acmutd.co/brand/Development/Banners/light_dark_background.png) \ No newline at end of file diff --git a/functions/src/application/portal.ts b/functions/src/application/portal.ts index 27bf1a6..dbe483a 100644 --- a/functions/src/application/portal.ts +++ b/functions/src/application/portal.ts @@ -73,11 +73,21 @@ export const record_event = async (request: Request, response: Response): Promis sub: data.sub, past_events: admin.firestore.FieldValue.arrayUnion({ name: result.data()?.name, - submitted_at: new Date().toISOString(), + submitted_at: result.data()?.date, }), }, { merge: true } ); + + firestore + .collection(event_collection) + .doc(pathname as string) + .update({ + attendance: admin.firestore.FieldValue.arrayUnion({ + sub: data.sub, + email: data.email, + }), + }); logger.log({ ...data, ...result.data(), diff --git a/functions/src/application/readme.md b/functions/src/application/readme.md new file mode 100644 index 0000000..8dc5318 --- /dev/null +++ b/functions/src/application/readme.md @@ -0,0 +1,6 @@ +# Portal + +This code drives the majority of the backend responsible for powering the ACM Portal. + +### Portal Functions + diff --git a/functions/src/application/typeform.ts b/functions/src/application/typeform.ts index 3859406..bd47691 100644 --- a/functions/src/application/typeform.ts +++ b/functions/src/application/typeform.ts @@ -6,6 +6,9 @@ import { upsert_contact, send_dynamic_template, user_contact, sendgrid_email } f import admin from "firebase-admin"; // import crypto from "crypto"; +const profile_collection = "profile"; +const typeform_meta_collection = "typeform_meta"; + type definition = { id: string; title: string; @@ -93,7 +96,7 @@ export const send_confirmation = functions.firestore .onCreate(async (snap, context) => { const document = snap.data(); try { - const meta_doc = await firestore.collection("typeform_meta").doc(document.typeform_id).get(); + const meta_doc = await firestore.collection(typeform_meta_collection).doc(document.typeform_id).get(); if (!meta_doc.exists) { logger.log(`No email template found for typeform ${document.typeform_id}`); return; @@ -146,7 +149,7 @@ export const send_confirmation = functions.firestore `sending email to user ${sub} with email ${email} in response to completion of form ${document.typeform_id}` ); await firestore - .collection("profile") + .collection(profile_collection) .doc(sub) .update({ past_applications: admin.firestore.FieldValue.arrayUnion({ @@ -154,6 +157,15 @@ export const send_confirmation = functions.firestore submitted_at: document.submission_time, }), }); + await firestore + .collection(typeform_meta_collection) + .doc(document.typeform_id) + .update({ + submitted: admin.firestore.FieldValue.arrayUnion({ + sub: sub, + email: email, + }), + }); } catch (error) { Sentry.captureException(error); } diff --git a/functions/src/express_configs/readme.md b/functions/src/express_configs/readme.md new file mode 100644 index 0000000..62c2603 --- /dev/null +++ b/functions/src/express_configs/readme.md @@ -0,0 +1,49 @@ +# Express Configurations + +The ACM Core API is broken down into several smaller sections that are designed to receive input from different sources. These different sources may have different forms of Authorization (or none at all). To handle this easily and make the code more readable those different possibilities for the API Requests are broken down into separate Express instances. + +### express_open.ts + +The endpoints served by express_open are unprotected endpoints that anyone can make an API call to without Authorization. The primary purpose of this endpoint is to serve as a place to receive webhooks easily. Webhooks from Typeform are received by this endpoint. In addition to that the ACM Development Challenge 0 is also served through this Express instance. + +``` +https://us-central1-acm-core.cloudfunctions.net/challenge/ +``` + +The endpoints are deployed under `/challenge` + +### express_cf.ts + +Handles applications where the authorization is provided through Cloudflare Access + +``` +https://us-central1-acm-core.cloudfunctions.net/cf/ +``` + +The endpoints are deployed under `/cf` + +### express_secure.ts + +Handles applications where authorization is provided through Auth0 + +``` +https://us-central1-acm-core.cloudfunctions.net/api/ +``` + +The endpoints are deployed under `/api` + +### express_portal.ts + +Handles API Requests from the [portal](https://app.acmutd.co). Part of the configuration for this express configuration is identical to both `express_secure.ts` and `express_cf.ts`. This one express instance can handle receiving tokens that are signed either by Auth0 or by Cloudflare Access. That is accomplished by subrouting the requests under `/portal/auth0` or `/portal/gsuite`. + +Requests that are sent to `/portal/auth0` must come from a single signed in account. Those API calls only have permission to access the profile and documents of their user. One user cannot access or perform any CRUD operations on another user. JWT sub is used as a primary key to ensure that a user cannot tamper with the database by making raw API calls. + +Requests that are sent to `/portal/gsuite` are for officer-exclusive operations. When authenticated through Cloudflare Access it allows for officers to effectively perform any operation on any user. Do not share your personal `access_token` signed by G Suite with anyone. + +Unauthenticated requests can be sent to `/portal`. This is useful for situations where people would like to access public information for their third party applications. This could be fetching a list of events or a list of open applications. + +``` +https://us-central1-acm-core.cloudfunctions.net/portal +``` + +The endpoints are deployed under `/portal` \ No newline at end of file diff --git a/readme.md b/readme.md index 33fc1e6..eae1268 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # ACM API -The official API of ACM UTD. View API [Documentation](https://documenter.getpostman.com/view/6712035/TVKJxEVW) +The official Core API of ACM UTD. View API [Documentation](https://documenter.getpostman.com/view/6712035/TVKJxEVW) ### Quick Start - Clone the repo @@ -12,6 +12,8 @@ $ npm install -g firebase-tools ``` $ firebase login ``` +Note: Make sure to sign in with your ACM G Suite account. If you are already signed in with another account you can use `firebase logout` to sign out first. + #### Testing Locally - Set up admin credentials following the firebase [docs](https://firebase.google.com/docs/functions/local-emulator#set_up_admin_credentials_optional). The key should be saved at `functions/acm-core-service-account.json` and should not be tracked. - Output firebase configuration to local file. This file should also not be tracked. @@ -23,12 +25,13 @@ $ firebase functions:config:get > functions/.runtimeconfig.json $ npm run serve ``` ⚠️ Pay attention to the warnings! Unless you are emulating other firebase services, your code can still affect production data. -- Acquire a testing access_token for auth0 under Applications > Dev Testing API (Test Application) > Quick start. -- Finally, you can invoke a function (`exampleFunction`) locally either with an http client like curl or postman: +- Acquire a testing `access_token` for auth0 under APIs > ACM Core > Test. + +- Finally, you can invoke an endpoint (`/portal/endpoint`) locally either with an http client like curl or postman: ``` curl --request GET \ - --url http://localhost:5001/acm-core/us-central1/api/exampleFunction \ + --url http://localhost:5002/acm-core/us-central1/portal/endpoint \ --header 'Authorization: Bearer token' ``` You can also use the [firebase functions shell](https://firebase.google.com/docs/functions/local-shell) if you prefer that. @@ -37,74 +40,6 @@ You can also use the [firebase functions shell](https://firebase.google.com/docs ``` $ npm run deploy ``` - -### Extended Start Guide - -Project structure -``` -root -├── firebase.json -├── functions -│ ├── acm-core-service-account.json -│ ├── package-lock.json -│ ├── package.json -│ ├── src -│ │ ├── admin -│ │ │ └── admin.ts -│ │ ├── application -│ │ │ ├── rebrand.ts -│ │ │ └── typeform.ts -│ │ ├── auth -│ │ │ └── auth.ts -│ │ ├── authTypes.ts -│ │ ├── challenge -│ │ │ └── challenge.ts -│ │ ├── custom -│ │ │ ├── hacktoberfest.ts -│ │ │ └── vanity.ts -│ │ ├── divisions -│ │ │ ├── GET -│ │ │ ├── POST -│ │ │ └── divisions.ts -│ │ ├── events -│ │ │ └── events.ts -│ │ ├── express_configs -│ │ │ ├── express_open.ts -│ │ │ └── express_secure.ts -│ │ ├── index.ts -│ │ ├── mail -│ │ │ ├── mailchimp.ts -│ │ │ └── sendgrid.ts -│ │ ├── roles -│ │ │ ├── permissions.ts -│ │ │ └── roles.ts -│ │ └── services -│ │ └── ErrorService.ts -│ ├── tsconfig.json -└── readme.md -``` - -### How to Contribute - -When testing deployment on a feature branch rename last line in `index.ts` to be as follows - -`exports.api-YOURNAME = functions.https.onRequest(app);` - -When making a pull request to `dev` ensure that it has been reverted to exports.api. - -##### Pull Requests & Issues - -Follow _ACM Development_ standards and guidelines. 2 approving reviews required for merge. Tag all developers on team as reviewers. Review code within 48hrs of making a pull request. - -### Contributors - - - [Harsha Srikara](https://harshasrikara.com) - - [David Richey](https://darichey.com) - - [Aliah Shaira De Guzman]() - - [Sivam Patel](https://github.com/sivampatel) - - [Kendra Huang](https://github.com/kendra-huang) - - [Jafar Ali](https://github.com/jafrilli) - ### Questions Sometimes you may have additional questions. If the answer was not found in this readme please feel free to reach out to the [Director of Development](mailto:development@acmutd.co) for _ACM_ From c649283096bc2f268e23b8071f609fdf057c5052 Mon Sep 17 00:00:00 2001 From: Harsha Srikara Date: Thu, 4 Feb 2021 00:48:12 -0600 Subject: [PATCH 2/9] added rsvp, refactored firestore triggers, marked old code as deprecated --- functions/src/application/portal.ts | 8 +- functions/src/application/typeform.ts | 19 +++ functions/src/custom/custom_forms.ts | 97 ++++++++++++++ functions/src/custom/deprecated_vanity.ts | 52 ++++++++ functions/src/custom/vanity.ts | 148 ---------------------- functions/src/index.ts | 67 +--------- 6 files changed, 175 insertions(+), 216 deletions(-) create mode 100644 functions/src/custom/custom_forms.ts create mode 100644 functions/src/custom/deprecated_vanity.ts delete mode 100644 functions/src/custom/vanity.ts diff --git a/functions/src/application/portal.ts b/functions/src/application/portal.ts index dbe483a..12a8636 100644 --- a/functions/src/application/portal.ts +++ b/functions/src/application/portal.ts @@ -50,6 +50,7 @@ export const create_blank_profile = async (request: Request, response: Response) export const record_event = async (request: Request, response: Response): Promise => { const pathname = (request.query.checkpath as string).substring((request.query.checkpath as string).lastIndexOf("/")); + const is_checkin = (request.query.checkpath as string).includes("checkin"); const data = request.body; try { const result = await firestore @@ -64,6 +65,8 @@ export const record_event = async (request: Request, response: Response): Promis return; } + const field_name = is_checkin ? "past_events" : "past_rsvps"; + firestore .collection(profile_collection) .doc(data.sub) @@ -71,19 +74,20 @@ export const record_event = async (request: Request, response: Response): Promis { email: data.email, sub: data.sub, - past_events: admin.firestore.FieldValue.arrayUnion({ + [field_name]: admin.firestore.FieldValue.arrayUnion({ name: result.data()?.name, submitted_at: result.data()?.date, }), }, { merge: true } ); + const field = is_checkin ? "attendance" : "rsvp"; firestore .collection(event_collection) .doc(pathname as string) .update({ - attendance: admin.firestore.FieldValue.arrayUnion({ + [field]: admin.firestore.FieldValue.arrayUnion({ sub: data.sub, email: data.email, }), diff --git a/functions/src/application/typeform.ts b/functions/src/application/typeform.ts index bd47691..71a6743 100644 --- a/functions/src/application/typeform.ts +++ b/functions/src/application/typeform.ts @@ -4,6 +4,7 @@ import { firestore } from "../admin/admin"; import logger from "../services/logging"; import { upsert_contact, send_dynamic_template, user_contact, sendgrid_email } from "../mail/sendgrid"; import admin from "firebase-admin"; +import { build_vanity_link } from "../custom/custom_forms"; // import crypto from "crypto"; const profile_collection = "profile"; @@ -171,6 +172,24 @@ export const send_confirmation = functions.firestore } }); +export const custom_form_actions = functions.firestore + .document("typeform/{document_name}") + .onCreate(async (snap, context) => { + const document = snap.data(); + try { + switch (document.typeform_id) { + case "Link Generator": + build_vanity_link(document); + break; + default: + logger.log(`No custom action found for typeform ${document.typeform_id}... exiting`); + return; + } + } catch (error) { + Sentry.captureException(error); + } + }); + // const verify_signature = (expectedSig: any, body: any) => { // const hash = crypto // .createHmac("sha256", functions.config().typeform.secret) diff --git a/functions/src/custom/custom_forms.ts b/functions/src/custom/custom_forms.ts new file mode 100644 index 0000000..be7ee86 --- /dev/null +++ b/functions/src/custom/custom_forms.ts @@ -0,0 +1,97 @@ +import * as functions from "firebase-functions"; +import request from "request"; +import sendgrid from "@sendgrid/mail"; + +export interface Vanity { + destination: string; + primary_domain: string; + subdomain: string; + slashtag: string; +} + +export const build_vanity_link = (document: FirebaseFirestore.DocumentData): void => { + const typeform_results = document.data; + let first_name = ""; + let last_name = ""; + let email = ""; + let destination = ""; + let primary_domain = ""; + let subdomain = ""; + let slashtag = ""; + + const first_name_question = "first_name"; + const last_name_question = "last_name"; + const email_question = "email"; + const destination_question = "vanity link"; + const primary_domain_question = "primary domain"; + const subdomain_question = "vanity domain"; + const slashtag_question = "slashtag"; + + typeform_results.forEach((element: any) => { + if (element.question.includes(first_name_question)) first_name = element.answer; + if (element.question.includes(last_name_question)) last_name = element.answer; + if (element.question.includes(email_question)) email = element.answer; + if (element.question.includes(destination_question)) destination = element.answer; + if (element.question.includes(primary_domain_question)) primary_domain = element.answer.label; + if (element.question.includes(subdomain_question) && element.answer.label !== "") subdomain = element.answer.label; + if (element.question.includes(slashtag_question)) slashtag = element.answer; + }); + + const data: Vanity = { + destination: destination, + primary_domain: primary_domain, + subdomain: subdomain, + slashtag: slashtag, + }; + create_link(data); + send_confirmation(data, email, first_name, last_name); +}; + +const create_link = async (vanity: Vanity): Promise => { + const linkRequest = { + destination: vanity.destination, + domain: { fullName: vanity.subdomain + "." + vanity.primary_domain }, + slashtag: vanity.slashtag, + }; + + let apikey = ""; + if (vanity.primary_domain === "acmutd.co") { + apikey = functions.config().rebrandly.apikey; + } else { + apikey = functions.config().rebrandly.apikey2; + } + + const requestHeaders = { + "Content-Type": "application/json", + apikey: apikey, + }; + + return request({ + uri: "https://api.rebrandly.com/v1/links", + method: "POST", + body: JSON.stringify(linkRequest), + headers: requestHeaders, + }); +}; + +const send_confirmation = ( + vanity: Vanity, + email: string, + first_name: string, + last_name: string +): Promise<[any, any]> => { + sendgrid.setApiKey(functions.config().sendgrid.apikey); + const msg: sendgrid.MailDataRequired = { + from: "development@acmutd.co", + to: email, + dynamicTemplateData: { + preheader: "Successful Generation of Vanity Link", + subject: "Vanity Link Confirmation", + vanity_link: vanity.subdomain + "." + vanity.primary_domain + "/" + vanity.slashtag, + first_name: first_name, + last_name: last_name, + }, + templateId: "d-cd15e958009a43b3b3a8d7352ee12c79", + }; + return sendgrid.send(msg); +}; diff --git a/functions/src/custom/deprecated_vanity.ts b/functions/src/custom/deprecated_vanity.ts new file mode 100644 index 0000000..637f0ea --- /dev/null +++ b/functions/src/custom/deprecated_vanity.ts @@ -0,0 +1,52 @@ +import * as functions from "firebase-functions"; +import * as Sentry from "@sentry/node"; +import request from "request"; +import sendgrid from "@sendgrid/mail"; + +/** + * @deprecated limited functionality + */ +export const create_vanity_link = functions.firestore + .document("vanity_link/{document_name}") + .onCreate((snap, context) => { + const document_value = snap.data(); + try { + const linkRequest = { + destination: document_value.vanity_redirect, + domain: { fullName: document_value.vanity_domain + ".acmutd.co" }, + slashtag: document_value.vanity_slash, + }; + + const requestHeaders = { + "Content-Type": "application/json", + apikey: functions.config().rebrandly.apikey, + }; + + request( + { + uri: "https://api.rebrandly.com/v1/links", + method: "POST", + body: JSON.stringify(linkRequest), + headers: requestHeaders, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: any, res: any, body: string) => { + //const link = JSON.parse(body); + sendgrid.setApiKey(functions.config().sendgrid.apikey); + const msg: sendgrid.MailDataRequired = { + from: "development@acmutd.co", + to: document_value.email, + dynamicTemplateData: { + preheader: "Successful Generation of Vanity Link", + subject: "Vanity Link", + vanity_link: document_value.vanity_domain + ".acmutd.co/" + document_value.vanity_slash, + }, + templateId: "d-cd15e958009a43b3b3a8d7352ee12c79", + }; + sendgrid.send(msg); + } + ); + } catch (error) { + Sentry.captureException(error); + } + }); diff --git a/functions/src/custom/vanity.ts b/functions/src/custom/vanity.ts deleted file mode 100644 index d65dc65..0000000 --- a/functions/src/custom/vanity.ts +++ /dev/null @@ -1,148 +0,0 @@ -import * as functions from "firebase-functions"; -import * as Sentry from "@sentry/node"; -import request from "request"; -import sendgrid from "@sendgrid/mail"; - -interface Vanity { - destination: string; - primary_domain: string; - subdomain: string; - slashtag: string; -} - -/** - * @deprecated limited functionality - */ -export const create_vanity_link = functions.firestore - .document("vanity_link/{document_name}") - .onCreate((snap, context) => { - const document_value = snap.data(); - try { - const linkRequest = { - destination: document_value.vanity_redirect, - domain: { fullName: document_value.vanity_domain + ".acmutd.co" }, - slashtag: document_value.vanity_slash, - }; - - const requestHeaders = { - "Content-Type": "application/json", - apikey: functions.config().rebrandly.apikey, - }; - - request( - { - uri: "https://api.rebrandly.com/v1/links", - method: "POST", - body: JSON.stringify(linkRequest), - headers: requestHeaders, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (err: any, res: any, body: string) => { - //const link = JSON.parse(body); - sendgrid.setApiKey(functions.config().sendgrid.apikey); - const msg: sendgrid.MailDataRequired = { - from: "development@acmutd.co", - to: document_value.email, - dynamicTemplateData: { - preheader: "Successful Generation of Vanity Link", - subject: "Vanity Link", - vanity_link: document_value.vanity_domain + ".acmutd.co/" + document_value.vanity_slash, - }, - templateId: "d-cd15e958009a43b3b3a8d7352ee12c79", - }; - sendgrid.send(msg); - } - ); - } catch (error) { - Sentry.captureException(error); - } - }); - -export const build_vanity_link = functions.firestore - .document("typeform/{document_name}") - .onCreate(async (snap, context) => { - const document = snap.data(); - try { - if (document.typeform_id !== "Link Generator Old") { - return; - } - const typeform_results = document.data; - let full_name = ""; - let email = ""; - let destination = ""; - let primary_domain = ""; - let subdomain = ""; - let slashtag = ""; - - const fullname_question = "name"; - const email_question = "email"; - const destination_question = "vanity link"; - const primary_domain_question = "primary domain"; - const subdomain_question = "vanity domain"; - const slashtag_question = "slashtag"; - - typeform_results.forEach((element: any) => { - if (element.question.includes(fullname_question)) full_name = element.answer; - if (element.question.includes(email_question)) email = element.answer; - if (element.question.includes(destination_question)) destination = element.answer; - if (element.question.includes(primary_domain_question)) primary_domain = element.answer.label; - if (element.question.includes(subdomain_question) && element.answer.label !== "") - subdomain = element.answer.label; - if (element.question.includes(slashtag_question)) slashtag = element.answer; - }); - - const data: Vanity = { - destination: destination, - primary_domain: primary_domain, - subdomain: subdomain, - slashtag: slashtag, - }; - create_link(data); - send_confirmation(data, email, full_name); - } catch (error) { - Sentry.captureException(error); - } - }); - -const create_link = async (vanity: Vanity): Promise => { - const linkRequest = { - destination: vanity.destination, - domain: { fullName: vanity.subdomain + "." + vanity.primary_domain }, - slashtag: vanity.slashtag, - }; - - let apikey = ""; - if (vanity.primary_domain === "acmutd.co") { - apikey = functions.config().rebrandly.apikey; - } else { - apikey = functions.config().rebrandly.apikey2; - } - - const requestHeaders = { - "Content-Type": "application/json", - apikey: apikey, - }; - - return request({ - uri: "https://api.rebrandly.com/v1/links", - method: "POST", - body: JSON.stringify(linkRequest), - headers: requestHeaders, - }); -}; - -const send_confirmation = (vanity: Vanity, email: string, name: string): Promise<[any, any]> => { - sendgrid.setApiKey(functions.config().sendgrid.apikey); - const msg: sendgrid.MailDataRequired = { - from: "development@acmutd.co", - to: email, - dynamicTemplateData: { - preheader: "Successful Generation of Vanity Link", - subject: "Vanity Link Confirmation", - vanity_link: vanity.subdomain + "." + vanity.primary_domain + "/" + vanity.slashtag, - full_name: name, - }, - templateId: "d-cd15e958009a43b3b3a8d7352ee12c79", - }; - return sendgrid.send(msg); -}; diff --git a/functions/src/index.ts b/functions/src/index.ts index 691f33a..148b01a 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,21 +3,12 @@ */ import app_portal from "./express_configs/express_portal"; import app_cf from "./express_configs/express_cf"; -import app_secure from "./express_configs/express_secure"; import app_open from "./express_configs/express_open"; import { Request, Response } from "express"; import * as functions from "firebase-functions"; -import * as authFunctions from "./auth/auth"; -import * as divisionFunctions from "./divisions/divisions"; -import * as roleFunctions from "./roles/roles"; -import * as permissionFunctions from "./roles/permissions"; -import * as applicationFunctions from "./application/rebrand"; import * as challengeFunctions from "./challenge/challenge"; -import * as sendgridFunctions from "./mail/sendgrid"; -import * as eventFunctions from "./events/events"; -import * as vanityFunctions from "./custom/vanity"; import * as hacktoberfestFunctions from "./custom/hacktoberfest"; import * as typeformFunctions from "./application/typeform"; import * as errorFunctions from "./services/ErrorService"; @@ -39,59 +30,6 @@ app_open.all("/", (request: Request, response: Response, next) => { next(); }); -/** - * Sample functions, not actually used - */ -app_secure.get("/getCustomToken", authFunctions.getCustomToken); -app_secure.post("/createTestUser", authFunctions.createTestUser); - -/** - * API will error out if the role is not present. - * For create role it will error if the role is already present - */ -app_secure.post("/role/:role", roleFunctions.createRole); -app_secure.put("/role/:role", roleFunctions.updateRole); -app_secure.delete("/role/:role", roleFunctions.deleteRole); -app_secure.get("/role/:role", roleFunctions.getRole); -app_secure.get("/role", roleFunctions.getAllRoles); - -/** - * Granular permissions management - */ -app_secure.post("/role/:role/addPermission", permissionFunctions.addPermission); -app_secure.post("/role/:role/removePermission", permissionFunctions.removePermission); - -/** - * Operate on divisions - */ -app_secure.post("/division/:division", divisionFunctions.setStaffMember); -app_secure.get("/division/:division", divisionFunctions.getAllStaff); - -/** - * Operate on events - */ -app_secure.post("/event/:event", eventFunctions.createEvent); -app_secure.put("/event/:event", eventFunctions.updateEvent); -app_secure.delete("/event/:event", eventFunctions.deleteEvent); -app_secure.get("/event/:event", eventFunctions.getEvent); - -/** - * Link shortener - */ -app_secure.post("/link", applicationFunctions.createLink); -app_secure.post("/link/:link", applicationFunctions.updateLink); -app_secure.get("/link/:link", applicationFunctions.getLink); -app_secure.delete("/link/:link", applicationFunctions.deleteLink); -app_secure.get("/link", applicationFunctions.getLinks); - -/** - * Sendgrid integration - */ -app_secure.post("/sendgrid/test-email", sendgridFunctions.sendTestEmail); -app_portal.post("/sendgrid/confirmation", sendgridFunctions.send_email); -app_portal.post("/sendgrid/upsert-contact", sendgridFunctions.upsertContact); -app_secure.post("/sendgrid/send-mailing-list", sendgridFunctions.sendMailingList); - /** * Challenges for ACM Development */ @@ -108,7 +46,6 @@ app_open.post("/typeform", typeformFunctions.typeform_webhook); /** * Debugging endpoints */ -app_secure.get("/debug-sentry", errorFunctions.debug_sentry); app_open.get("/debug-sentry", errorFunctions.debug_sentry); app_open.get("/debug-logger", debug_logger); @@ -145,13 +82,11 @@ process.on("uncaughtException", (err) => Sentry.captureException(err)); // http server endpoints export const cf = functions.https.onRequest(app_cf); -export const api = functions.https.onRequest(app_secure); export const portal = functions.https.onRequest(app_portal); export const challenge = functions.https.onRequest(app_open); // firestore triggers -export const build_vanity_link = vanityFunctions.build_vanity_link; -export const create_vanity_link = vanityFunctions.create_vanity_link; +export const custom_form_actions = typeformFunctions.custom_form_actions; export const email_discord_mapper = hacktoberfestFunctions.mapper; export const create_profile = portalFunctions.create_profile; export const typeform_confirmation = typeformFunctions.send_confirmation; From c4944e608a650f370e1722c75ff770547e802adc Mon Sep 17 00:00:00 2001 From: Harsha Srikara Date: Thu, 4 Feb 2021 21:32:32 -0600 Subject: [PATCH 3/9] new acm dev microservice, vanity.acmutd.co/sendgrid --- functions/src/application/typeform.ts | 11 ++- functions/src/custom/sendgrid_map.ts | 86 +++++++++++++++++++ .../src/custom/{custom_forms.ts => vanity.ts} | 5 +- .../src/express_configs/express_portal.ts | 11 ++- functions/src/mail/sendgrid.ts | 30 ++++++- 5 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 functions/src/custom/sendgrid_map.ts rename functions/src/custom/{custom_forms.ts => vanity.ts} (97%) diff --git a/functions/src/application/typeform.ts b/functions/src/application/typeform.ts index 71a6743..78356e5 100644 --- a/functions/src/application/typeform.ts +++ b/functions/src/application/typeform.ts @@ -4,7 +4,8 @@ import { firestore } from "../admin/admin"; import logger from "../services/logging"; import { upsert_contact, send_dynamic_template, user_contact, sendgrid_email } from "../mail/sendgrid"; import admin from "firebase-admin"; -import { build_vanity_link } from "../custom/custom_forms"; +import { build_vanity_link } from "../custom/vanity"; +import { connect_sendgrid } from "../custom/sendgrid_map"; // import crypto from "crypto"; const profile_collection = "profile"; @@ -181,11 +182,19 @@ export const custom_form_actions = functions.firestore case "Link Generator": build_vanity_link(document); break; + case "Connect Sendgrid": + connect_sendgrid(document); + break; default: logger.log(`No custom action found for typeform ${document.typeform_id}... exiting`); return; } } catch (error) { + console.log(error); + logger.log({ + ...error, + message: "Error occured in custom typeform function", + }); Sentry.captureException(error); } }); diff --git a/functions/src/custom/sendgrid_map.ts b/functions/src/custom/sendgrid_map.ts new file mode 100644 index 0000000..28bbedf --- /dev/null +++ b/functions/src/custom/sendgrid_map.ts @@ -0,0 +1,86 @@ +import { firestore } from "../admin/admin"; +import logger from "../services/logging"; +import { get_dynamic_template, create_marketing_list, send_dynamic_template, sendgrid_email } from "../mail/sendgrid"; + +export interface SendgridDoc { + typeform_name: string; + sendgrid_dynamic_id: string; + sendgrid_marketing_list: string; + sender_email: string; + sender_from: string; + dynamic_template_name: string; +} + +const typeform_meta_collection = "typeform_meta"; + +export const connect_sendgrid = async (document: FirebaseFirestore.DocumentData): Promise => { + const typeform_results = document.data; + let first_name = ""; + let last_name = ""; + let email = ""; + let typeform_name = ""; + let sendgrid_dynamic_id = ""; + let sender_email = ""; + let sender_from = ""; + + const first_name_question = "first_name"; + const last_name_question = "last_name"; + const email_question = "email"; + const typeform_question = "Typeform"; + const sendgrid_dynamic_id_question = "Sendgrid"; + const sender_email_question = "sender e-mail"; + const sender_from_question = "sender name"; + + typeform_results.forEach((element: any) => { + if (element.question.includes(first_name_question)) first_name = element.answer; + if (element.question.includes(last_name_question)) last_name = element.answer; + if (element.question.includes(email_question)) email = element.answer; + if (element.question.includes(typeform_question)) typeform_name = element.answer; + if (element.question.includes(sendgrid_dynamic_id_question)) sendgrid_dynamic_id = element.answer; + if (element.question.includes(sender_email_question)) sender_email = element.answer; + if (element.question.includes(sender_from_question)) sender_from = element.answer; + }); + + const sendgrid_id = await create_marketing_list(typeform_name); + const template_id = await get_dynamic_template(sendgrid_dynamic_id); + + const data: SendgridDoc = { + typeform_name: typeform_name, + sendgrid_dynamic_id: sendgrid_dynamic_id, + sendgrid_marketing_list: sendgrid_id, + sender_email: sender_email, + sender_from: sender_from, + dynamic_template_name: template_id, + }; + + const email_options: sendgrid_email = { + from: "development@acmutd.co", + from_name: "ACM Development", + template_id: "d-8d16910adcae4b918ba9c44670d963ac", + to: email, + dynamicSubstitutions: { + first_name: first_name, + last_name: last_name, + typeform_id: typeform_name, + template_id: template_id, + preheader: "Successful Typeform X Sendgrid Connection", + subject: "Sendgrid Connect Confirmation", + }, + }; + create_map(data); + send_dynamic_template(email_options); +}; + +const create_map = (document: SendgridDoc): void => { + firestore.collection(typeform_meta_collection).doc(document.typeform_name).create({ + sendgrid_dynamic_template: document.sendgrid_dynamic_id, + from: document.sender_email, + from_name: document.sender_from, + sendgrid_marketing_list: document.sendgrid_marketing_list, + sendgrid_template_name: document.dynamic_template_name, + }); + logger.log({ + ...document, + message: "Successfully connected Typeform with Sendgrid", + }); +}; diff --git a/functions/src/custom/custom_forms.ts b/functions/src/custom/vanity.ts similarity index 97% rename from functions/src/custom/custom_forms.ts rename to functions/src/custom/vanity.ts index be7ee86..0f168ff 100644 --- a/functions/src/custom/custom_forms.ts +++ b/functions/src/custom/vanity.ts @@ -82,7 +82,10 @@ const send_confirmation = ( ): Promise<[any, any]> => { sendgrid.setApiKey(functions.config().sendgrid.apikey); const msg: sendgrid.MailDataRequired = { - from: "development@acmutd.co", + from: { + email: "development@acmutd.co", + name: "ACM Development", + }, to: email, dynamicTemplateData: { preheader: "Successful Generation of Vanity Link", diff --git a/functions/src/express_configs/express_portal.ts b/functions/src/express_configs/express_portal.ts index 7601ff4..a6eda1b 100644 --- a/functions/src/express_configs/express_portal.ts +++ b/functions/src/express_configs/express_portal.ts @@ -12,7 +12,6 @@ import * as Tracing from "@sentry/tracing"; import cors from "cors"; import * as bodyParser from "body-parser"; import { Response, Request } from "express"; -import logger from "../services/logging"; const app = express(); @@ -112,11 +111,11 @@ app.use(extractAuth0Fields); /** * Log entire request */ -function logRequest(request: Request, response: Response, next: () => void) { - logger.log(request); - next(); -} -app.use(logRequest); +// function logRequest(request: Request, response: Response, next: () => void) { +// logger.log(request); +// next(); +// } +// app.use(logRequest); /** * Extract jwt fields and inject into request body diff --git a/functions/src/mail/sendgrid.ts b/functions/src/mail/sendgrid.ts index f8b4191..803b475 100644 --- a/functions/src/mail/sendgrid.ts +++ b/functions/src/mail/sendgrid.ts @@ -11,7 +11,7 @@ export interface sendgrid_email { from_name: string; to: string; template_id: string; - dynamicSubstitutions: Record; //apparently this is the correct typescript way to define any object + dynamicSubstitutions: Record; } export const sendTestEmail = async (request: Request, response: Response): Promise => { @@ -175,3 +175,31 @@ export const getMailingLists = async (request: Request, response: Response): Pro }); } }; + +export const create_marketing_list = async (name: string): Promise => { + client.setApiKey(functions.config().sendgrid.apikey); + const req: RequestOptions = { + method: "POST", + url: "/v3/marketing/lists", + body: { + name: name, + }, + }; + const result = await client.request(req); + logger.log({ + message: "Successfully created new mailing list", + name: name, + id: result[1].id, + }); + return result[1].id; +}; + +export const get_dynamic_template = async (template_id: string): Promise => { + client.setApiKey(functions.config().sendgrid.apikey); + const req: RequestOptions = { + method: "GET", + url: `/v3/templates/${template_id}`, + }; + const result = await client.request(req); + return result[1].name; +}; From 2fd20a4ddb297d9dfc9e8720d49ddff1f3b4c24b Mon Sep 17 00:00:00 2001 From: Reshmi Ranjith Date: Sat, 13 Feb 2021 14:37:24 -0600 Subject: [PATCH 4/9] add create event function --- functions/src/application/typeform.ts | 4 ++ functions/src/custom/event.ts | 69 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 functions/src/custom/event.ts diff --git a/functions/src/application/typeform.ts b/functions/src/application/typeform.ts index 78356e5..49f4fb2 100644 --- a/functions/src/application/typeform.ts +++ b/functions/src/application/typeform.ts @@ -6,6 +6,7 @@ import { upsert_contact, send_dynamic_template, user_contact, sendgrid_email } f import admin from "firebase-admin"; import { build_vanity_link } from "../custom/vanity"; import { connect_sendgrid } from "../custom/sendgrid_map"; +import { create_event } from "../custom/event"; // import crypto from "crypto"; const profile_collection = "profile"; @@ -185,6 +186,9 @@ export const custom_form_actions = functions.firestore case "Connect Sendgrid": connect_sendgrid(document); break; + case "Event Generator": + create_event(document); + break; default: logger.log(`No custom action found for typeform ${document.typeform_id}... exiting`); return; diff --git a/functions/src/custom/event.ts b/functions/src/custom/event.ts new file mode 100644 index 0000000..91f76a2 --- /dev/null +++ b/functions/src/custom/event.ts @@ -0,0 +1,69 @@ +import { firestore } from "../admin/admin"; +import logger from "../services/logging"; +import { send_dynamic_template, sendgrid_email } from "../mail/sendgrid"; + +export interface EventDoc { + name: string; + path_name: string; + date: string; +} + +const typeform_meta_collection = "events"; + +export const create_event = async (document: FirebaseFirestore.DocumentData): Promise => { + const typeform_results = document.data; + let first_name = ""; + let last_name = ""; + let email = ""; + let name = ""; + let path_name = ""; + let date = ""; + + const first_name_question = "first_name"; + const last_name_question = "last_name"; + const email_question = "email"; + const name_question = "Name of Event"; + const path_name_question = "path"; + const date_question = "date"; + + typeform_results.forEach((element: any) => { + if (element.question.includes(first_name_question)) first_name = element.answer; + if (element.question.includes(last_name_question)) last_name = element.answer; + if (element.question.includes(email_question)) email = element.answer; + if (element.question.includes(name_question)) name = element.answer; + if (element.question.includes(path_name_question)) path_name = element.answer; + if (element.question.includes(date_question)) date = element.answer; + }); + + const data: EventDoc = { + name: name, + path_name: path_name, + date: date, + }; + + const email_options: sendgrid_email = { + from: "development@acmutd.co", + from_name: "ACM Development", + template_id: "d-8d16910adcae4b918ba9c44670d963ac", + to: email, + dynamicSubstitutions: { + first_name: first_name, + last_name: last_name, + name: name, + path_name: path_name, + date: date, + preheader: "Successful Event Check-in Creation Connection", + subject: "Event Creation Confirmation", + }, + }; + create_map(data); + send_dynamic_template(email_options); +}; + +const create_map = (document: EventDoc): void => { + firestore.collection(typeform_meta_collection).doc(document.name).create(document); + logger.log({ + ...document, + message: "Successfully created an event check-in", + }); +}; From 9766801be5b1c6f11000cedf494b281d51fa2188 Mon Sep 17 00:00:00 2001 From: Reshmi Ranjith Date: Sat, 13 Feb 2021 17:50:12 -0600 Subject: [PATCH 5/9] auth0 wip --- functions/src/admin/auth0.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 functions/src/admin/auth0.ts diff --git a/functions/src/admin/auth0.ts b/functions/src/admin/auth0.ts new file mode 100644 index 0000000..70ecceb --- /dev/null +++ b/functions/src/admin/auth0.ts @@ -0,0 +1,23 @@ +import * as axios from "axios"; +import * as functions from "firebase-functions"; + +export const auth0 = async (): Promise => { + const options = { + headers: { "content-type": "application/x-www-form-urlencoded" }, + data: { + grant_type: "client_credentials", + client_id: functions.config().auth0.clientid, + client_secret: functions.config().auth0.secret, + audience: `https://${functions.config().auth0.domain}/api/v2/`, + }, + }; + + axios.default + .post(`https://${functions.config().auth0.domain}/oauth/token`, options) + .then(function (response) { + console.log(response.data); + }) + .catch(function (error) { + console.error(error); + }); +}; From 9e870dbffb476c6faf171db35de9299334b83788 Mon Sep 17 00:00:00 2001 From: Harsha Srikara Date: Sat, 13 Feb 2021 20:20:20 -0600 Subject: [PATCH 6/9] Yay event generator is completed --- functions/src/admin/auth0.ts | 57 ++++++++++++++++++++++++++--------- functions/src/custom/event.ts | 29 ++++++++++++------ 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/functions/src/admin/auth0.ts b/functions/src/admin/auth0.ts index 70ecceb..d47a6ce 100644 --- a/functions/src/admin/auth0.ts +++ b/functions/src/admin/auth0.ts @@ -1,23 +1,52 @@ import * as axios from "axios"; import * as functions from "firebase-functions"; +import * as Sentry from "@sentry/node"; +import logger from "../services/logging"; -export const auth0 = async (): Promise => { - const options = { - headers: { "content-type": "application/x-www-form-urlencoded" }, - data: { +export const get_auth_token = async (): Promise => { + const result = await axios.default.post( + `https://${functions.config().auth0.domain}/oauth/token`, + { grant_type: "client_credentials", - client_id: functions.config().auth0.clientid, - client_secret: functions.config().auth0.secret, + client_id: functions.config().auth0_api.clientid, + client_secret: functions.config().auth0_api.client_secret, audience: `https://${functions.config().auth0.domain}/api/v2/`, }, - }; + { + headers: { + "content-type": "application/json", + }, + } + ); + return result.data.access_token; +}; - axios.default - .post(`https://${functions.config().auth0.domain}/oauth/token`, options) - .then(function (response) { - console.log(response.data); - }) - .catch(function (error) { - console.error(error); +export const add_callback = async (url: string, access_token: string): Promise => { + try { + const result = await axios.default.get( + `https://${functions.config().auth0.domain}/api/v2/clients/${ + functions.config().auth0.clientid + }?fields=callbacks&include_fields=true`, + { + headers: { "content-type": "application/json", authorization: `Bearer ${access_token}` }, + } + ); + const callbacks = result.data.callbacks; + callbacks.push(url); + await axios.default.patch( + `https://${functions.config().auth0.domain}/api/v2/clients/${functions.config().auth0.clientid}`, + { + callbacks: callbacks, + }, + { + headers: { "content-type": "application/json", authorization: `Bearer ${access_token}` }, + } + ); + } catch (err) { + logger.log({ + ...err, + message: "Error occurred in updating callback urls on Auth0", }); + Sentry.captureException(err); + } }; diff --git a/functions/src/custom/event.ts b/functions/src/custom/event.ts index 91f76a2..b69fa74 100644 --- a/functions/src/custom/event.ts +++ b/functions/src/custom/event.ts @@ -1,6 +1,7 @@ import { firestore } from "../admin/admin"; import logger from "../services/logging"; import { send_dynamic_template, sendgrid_email } from "../mail/sendgrid"; +import { get_auth_token, add_callback } from "../admin/auth0"; export interface EventDoc { name: string; @@ -8,7 +9,7 @@ export interface EventDoc { date: string; } -const typeform_meta_collection = "events"; +const event_collection = "events"; export const create_event = async (document: FirebaseFirestore.DocumentData): Promise => { const typeform_results = document.data; @@ -32,15 +33,9 @@ export const create_event = async (document: FirebaseFirestore.DocumentData): Pr if (element.question.includes(email_question)) email = element.answer; if (element.question.includes(name_question)) name = element.answer; if (element.question.includes(path_name_question)) path_name = element.answer; - if (element.question.includes(date_question)) date = element.answer; + if (element.question.includes(date_question)) date = new Date(element.answer).toDateString(); }); - const data: EventDoc = { - name: name, - path_name: path_name, - date: date, - }; - const email_options: sendgrid_email = { from: "development@acmutd.co", from_name: "ACM Development", @@ -50,18 +45,32 @@ export const create_event = async (document: FirebaseFirestore.DocumentData): Pr first_name: first_name, last_name: last_name, name: name, - path_name: path_name, + checkin_link: `https://app.acmutd.co/checkin/${path_name}`, date: date, preheader: "Successful Event Check-in Creation Connection", subject: "Event Creation Confirmation", }, }; + + const data: EventDoc = { + name: name, + path_name: path_name, + date: date, + }; + create_map(data); + add_callback(`https://app.acmutd.co/checkin/${path_name}`, await get_auth_token()); send_dynamic_template(email_options); }; const create_map = (document: EventDoc): void => { - firestore.collection(typeform_meta_collection).doc(document.name).create(document); + firestore + .collection(event_collection) + .doc(document.path_name) + .create({ + ...document, + path_name: `/checkin/${document.path_name}`, + }); logger.log({ ...document, message: "Successfully created an event check-in", From b8cf9db4a633db07cea393e64133aac20300058f Mon Sep 17 00:00:00 2001 From: Harsha Srikara Date: Sat, 13 Feb 2021 20:43:53 -0600 Subject: [PATCH 7/9] change events to event --- functions/src/custom/event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/custom/event.ts b/functions/src/custom/event.ts index b69fa74..52dcc0c 100644 --- a/functions/src/custom/event.ts +++ b/functions/src/custom/event.ts @@ -9,7 +9,7 @@ export interface EventDoc { date: string; } -const event_collection = "events"; +const event_collection = "event"; export const create_event = async (document: FirebaseFirestore.DocumentData): Promise => { const typeform_results = document.data; From 645a57a2687a183ca06559c6e41133da6dba98f3 Mon Sep 17 00:00:00 2001 From: Harsha Srikara Date: Sat, 13 Feb 2021 21:38:07 -0600 Subject: [PATCH 8/9] update template --- functions/src/custom/event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/custom/event.ts b/functions/src/custom/event.ts index 52dcc0c..74e13c4 100644 --- a/functions/src/custom/event.ts +++ b/functions/src/custom/event.ts @@ -39,7 +39,7 @@ export const create_event = async (document: FirebaseFirestore.DocumentData): Pr const email_options: sendgrid_email = { from: "development@acmutd.co", from_name: "ACM Development", - template_id: "d-8d16910adcae4b918ba9c44670d963ac", + template_id: "d-f2d3b8a1b4dd4c14895905b7abb4581b", to: email, dynamicSubstitutions: { first_name: first_name, From c38f090406ccf98531b6e3b887e5253c119c8de2 Mon Sep 17 00:00:00 2001 From: Harsha Srikara Date: Sun, 21 Feb 2021 17:55:05 -0600 Subject: [PATCH 9/9] Documentation + separate routes and exports into their own file --- functions/src/admin/readme.md | 13 +++-- functions/src/application/readme.md | 44 ++++++++++++++ functions/src/application/typeform.ts | 1 - functions/src/divisions/.keep | 0 functions/src/divisions/divisions.ts | 4 ++ functions/src/events/.keep | 0 functions/src/events/events.ts | 4 ++ functions/src/express_configs/readme.md | 10 +++- functions/src/index.ts | 76 ++---------------------- functions/src/routes/express_cf.ts | 19 ++++++ functions/src/routes/express_open.ts | 45 ++++++++++++++ functions/src/routes/express_portal.ts | 32 ++++++++++ functions/src/services/readme.md | 78 +++++++++++++++++++++++++ 13 files changed, 248 insertions(+), 78 deletions(-) delete mode 100644 functions/src/divisions/.keep delete mode 100644 functions/src/events/.keep create mode 100644 functions/src/routes/express_cf.ts create mode 100644 functions/src/routes/express_open.ts create mode 100644 functions/src/routes/express_portal.ts create mode 100644 functions/src/services/readme.md diff --git a/functions/src/admin/readme.md b/functions/src/admin/readme.md index 43eb73c..fa772a2 100644 --- a/functions/src/admin/readme.md +++ b/functions/src/admin/readme.md @@ -1,15 +1,20 @@ -# Firebase Admin +# Admin -`admin.ts` handles centrally initializing the Firebase Admin to access various different services across different files / functions. +[admin.ts](./admin.ts) handles centrally initializing the Firebase Admin to access various different services across different files / functions. -### Usage +### Admin.ts -When needing to use additional services from firebase instantiate them in `admin.ts` and import them where needed. Also use this place to pass in custom arguments or different configuration parameters. +When needing to use additional services from firebase instantiate them in [admin.ts](./admin.ts) and import them where needed. Also use this place to pass in custom arguments or different configuration parameters. +### Auth0.ts + +This service handles performing OAuth exchanges to get access tokens. Also used by the event checkin service to add callback urls to Auth0. ### Notes - The `service-account.json` file is not passed in as a paramter to `admin.initializeApp()` because the backend is deployed on Firebase Functions and those variables are automatically accessible as environment variables. If the backend is shifted elsewhere then those variables will need to be passed in. - If you choose write any standalone scripts store them in this folder. + - When creating services that perform OAuth client-credential exchanges create new files for them here + - Ensure that an `async get_auth_token` is present any services that perform authentication. Reuse the same token per API transaction instead of instantiating a new one for each function call. ### Questions diff --git a/functions/src/application/readme.md b/functions/src/application/readme.md index 8dc5318..af7f419 100644 --- a/functions/src/application/readme.md +++ b/functions/src/application/readme.md @@ -4,3 +4,47 @@ This code drives the majority of the backend responsible for powering the ACM Po ### Portal Functions +``` +// fill at later point in time +``` +### Typeform Functions + +The typeform functions section is very mature and is responsible for all interactions with the third party service. + +##### Open Webhook + +The `typeform_webhook` function present in [typeform.ts](./typeform.ts) is responsible for receiving all incoming webhook requests from typeform. When creating a new typeform make sure to add in the following url as a webhook: `https://us-central1-acm-core.cloudfunctions.net/challenge/typeform`. + +This function will extract all the information within the typeform response body and save only the relevant fields in the following format. An array of `qa` type objects represents the final state of the object that is persisted in Cloud Firestore. This format makes it easy to quickly retrieve typeform responses and perform additional manipulation with Firestore Triggers. + +``` +export interface qa { + question: string; + answer: string; + type: string; +} +``` + +Hidden fields are also extracted from Typeform submissions and saved in the same format. In this situation the `question` will be the hidden field key and the `answer` will be the hidden field value. + +##### Confirmation Emails + +One essential part to ensuring that members who submit forms to ACM are happy and aware that their submission was received is to send them confirmation emails. While typeform natively supports sending confirmation emails it does not support the ability to customize and design them to match the ACM Brand Guidelines. In [typeform.ts](./typeform.ts) we have the `send_confirmation` firestore trigger. This function gets run anytime a new typeform document gets saved in Cloud Firestore. + +This functions queries the `typeform_meta` firestore collection with the `typeform_id` to see whether there exists a confirmation email that should be sent. If the exists information regarding the Sendgrid template / sender information then this firestore trigger will call the Sendgrid API to send out that specific email. Adding a new configuration can be done through the officer utility [Sendgrid x Typeform](https://survey.acmutd.co/email). + +##### Custom Actions + +Sometimes we want to perform a custom action based on the submission of a typeform. These are typically the internal utilities made for the ACM officer to refine workflows. All custom action functions are saved in [Custom Actions](../custom). These are also Firestore Triggers and get executed anytime a new document is created. The following functions have custom triggers: + + - [Vanity Form](https://survey.acmutd.co/vanity) + - [Sendgrid Form](https://survey.acmutd.co/email) + - [Event Form](https://survey.acmutd.co/event) + +### Questions + +Sometimes you may have additional questions. If the answer was not found in this readme please feel free to reach out to the [Director of Development](mailto:development@acmutd.co) for _ACM_ + +We request that you be as detailed as possible in your questions, doubts or concerns to ensure that we can be of maximum assistance. Thank you! + +![ACM Development](https://www.acmutd.co/brand/Development/Banners/light_dark_background.png) \ No newline at end of file diff --git a/functions/src/application/typeform.ts b/functions/src/application/typeform.ts index 49f4fb2..fd0f1fd 100644 --- a/functions/src/application/typeform.ts +++ b/functions/src/application/typeform.ts @@ -194,7 +194,6 @@ export const custom_form_actions = functions.firestore return; } } catch (error) { - console.log(error); logger.log({ ...error, message: "Error occured in custom typeform function", diff --git a/functions/src/divisions/.keep b/functions/src/divisions/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/functions/src/divisions/divisions.ts b/functions/src/divisions/divisions.ts index 8f1826f..f0afeb9 100644 --- a/functions/src/divisions/divisions.ts +++ b/functions/src/divisions/divisions.ts @@ -1,3 +1,7 @@ +/** + * @deprecated file + */ + import { firestore } from "../admin/admin"; import { Response, Request } from "express"; import * as Sentry from "@sentry/node"; diff --git a/functions/src/events/.keep b/functions/src/events/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/functions/src/events/events.ts b/functions/src/events/events.ts index 4912e4a..af1404e 100644 --- a/functions/src/events/events.ts +++ b/functions/src/events/events.ts @@ -1,3 +1,7 @@ +/** + * @deprecated file + */ + import { firestore } from "../admin/admin"; import { Response, Request } from "express"; import * as Sentry from "@sentry/node"; diff --git a/functions/src/express_configs/readme.md b/functions/src/express_configs/readme.md index 62c2603..124163a 100644 --- a/functions/src/express_configs/readme.md +++ b/functions/src/express_configs/readme.md @@ -46,4 +46,12 @@ Unauthenticated requests can be sent to `/portal`. This is useful for situations https://us-central1-acm-core.cloudfunctions.net/portal ``` -The endpoints are deployed under `/portal` \ No newline at end of file +The endpoints are deployed under `/portal` + +### Questions + +Sometimes you may have additional questions. If the answer was not found in this readme please feel free to reach out to the [Director of Development](mailto:development@acmutd.co) for _ACM_ + +We request that you be as detailed as possible in your questions, doubts or concerns to ensure that we can be of maximum assistance. Thank you! + +![ACM Development](https://www.acmutd.co/brand/Development/Banners/light_dark_background.png) diff --git a/functions/src/index.ts b/functions/src/index.ts index 148b01a..568fed0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,82 +1,15 @@ /** - * Handle express routing in this file + * Firebase function exports */ -import app_portal from "./express_configs/express_portal"; -import app_cf from "./express_configs/express_cf"; -import app_open from "./express_configs/express_open"; - -import { Request, Response } from "express"; +import app_portal from "./routes/express_portal"; +import app_cf from "./routes/express_cf"; +import app_open from "./routes/express_open"; import * as functions from "firebase-functions"; -import * as challengeFunctions from "./challenge/challenge"; -import * as hacktoberfestFunctions from "./custom/hacktoberfest"; import * as typeformFunctions from "./application/typeform"; -import * as errorFunctions from "./services/ErrorService"; import * as portalFunctions from "./application/portal"; -import logger, { debug_logger } from "./services/logging"; import * as Sentry from "@sentry/node"; -//this will match every call made to this api. -app_portal.all("/", (request: Request, response: Response, next) => { - logger.log(request); - //next() basically says to run the next route that matches the url - next(); -}); -app_cf.all("/", (request: Request, response: Response, next) => { - logger.log(request); - next(); -}); -app_open.all("/", (request: Request, response: Response, next) => { - next(); -}); - -/** - * Challenges for ACM Development - */ -app_open.post("/tags/:tag", challengeFunctions.createTag); -app_open.get("/tags/:tag/:token", challengeFunctions.getTag); -app_open.patch("/tags/:tag/:token", challengeFunctions.patchTag); -app_open.delete("/tags/:tag/:token", challengeFunctions.deleteTag); - -/** - * typeform webhook - */ -app_open.post("/typeform", typeformFunctions.typeform_webhook); - -/** - * Debugging endpoints - */ -app_open.get("/debug-sentry", errorFunctions.debug_sentry); -app_open.get("/debug-logger", debug_logger); - -/** - * htf-development retrieval - */ -app_open.post("/htf-development", hacktoberfestFunctions.retrieve_record); - -/** - * Cloudflare access protected endpoint - */ -app_cf.get("/verify", portalFunctions.verify); //to be phased out -app_cf.get("/verify-idp", portalFunctions.verify_idp); - -/** - * The two following endpoints are duplicated across both /auth0 and /gsuite - * This is because they have common requirements - * Additional endpoints for separate forms will be on one or the other path - */ -app_portal.get("/auth0/verify-idp", portalFunctions.verify_idp); -app_portal.get("/gsuite/verify-idp", portalFunctions.verify_idp); - -app_portal.get("/auth0/verify", portalFunctions.verify); -app_portal.get("/gsuite/verify", portalFunctions.verify); - -//all initialization requests on portal frontend for forms are get requests -app_portal.get("/auth0/create-blank-profile", portalFunctions.create_blank_profile); -app_portal.get("/auth0/profile", portalFunctions.get_profile); -app_portal.get("/auth0/developer", portalFunctions.get_developer_profile); -app_portal.get("/auth0/checkin", portalFunctions.record_event); - // Automatically send uncaught exception errors to Sentry process.on("uncaughtException", (err) => Sentry.captureException(err)); @@ -87,6 +20,5 @@ export const challenge = functions.https.onRequest(app_open); // firestore triggers export const custom_form_actions = typeformFunctions.custom_form_actions; -export const email_discord_mapper = hacktoberfestFunctions.mapper; export const create_profile = portalFunctions.create_profile; export const typeform_confirmation = typeformFunctions.send_confirmation; diff --git a/functions/src/routes/express_cf.ts b/functions/src/routes/express_cf.ts new file mode 100644 index 0000000..1b8ed8f --- /dev/null +++ b/functions/src/routes/express_cf.ts @@ -0,0 +1,19 @@ +/** + * Handle express routing in this file + */ +import app_cf from "../express_configs/express_cf"; +import { Request, Response } from "express"; +import * as portalFunctions from "../application/portal"; + +app_cf.all("/", (request: Request, response: Response, next: () => void) => { + next(); +}); + +/** + * Cloudflare access protected endpoint + */ +app_cf.get("/verify", portalFunctions.verify); //to be phased out +app_cf.get("/verify-idp", portalFunctions.verify_idp); + +// http server endpoints +export default app_cf; diff --git a/functions/src/routes/express_open.ts b/functions/src/routes/express_open.ts new file mode 100644 index 0000000..cb84a4e --- /dev/null +++ b/functions/src/routes/express_open.ts @@ -0,0 +1,45 @@ +/** + * Handle express routing in this file + */ +import app_open from "../express_configs/express_open"; + +import { Request, Response } from "express"; + +import * as challengeFunctions from "../challenge/challenge"; +import * as hacktoberfestFunctions from "../custom/hacktoberfest"; +import * as typeformFunctions from "../application/typeform"; +import * as errorFunctions from "../services/ErrorService"; +import { debug_logger } from "../services/logging"; + +/** + * Match all requests + */ +app_open.all("/", (request: Request, response: Response, next) => { + next(); +}); + +/** + * Challenges for ACM Development + */ +app_open.post("/tags/:tag", challengeFunctions.createTag); +app_open.get("/tags/:tag/:token", challengeFunctions.getTag); +app_open.patch("/tags/:tag/:token", challengeFunctions.patchTag); +app_open.delete("/tags/:tag/:token", challengeFunctions.deleteTag); + +/** + * typeform webhook + */ +app_open.post("/typeform", typeformFunctions.typeform_webhook); + +/** + * Debugging endpoints + */ +app_open.get("/debug-sentry", errorFunctions.debug_sentry); +app_open.get("/debug-logger", debug_logger); + +/** + * htf-development retrieval + */ +app_open.post("/htf-development", hacktoberfestFunctions.retrieve_record); + +export default app_open; diff --git a/functions/src/routes/express_portal.ts b/functions/src/routes/express_portal.ts new file mode 100644 index 0000000..23d9478 --- /dev/null +++ b/functions/src/routes/express_portal.ts @@ -0,0 +1,32 @@ +/** + * Handle express routing in this file + */ +import app_portal from "../express_configs/express_portal"; +import { Request, Response } from "express"; +import * as portalFunctions from "../application/portal"; + +//this will match every call made to this api. +app_portal.all("/", (request: Request, response: Response, next) => { + //next() basically says to run the next route that matches the url + next(); +}); + +/** + * The two following endpoints are duplicated across both /auth0 and /gsuite + * This is because they have common requirements + * Additional endpoints for separate forms will be on one or the other path + */ +app_portal.get("/auth0/verify-idp", portalFunctions.verify_idp); +app_portal.get("/gsuite/verify-idp", portalFunctions.verify_idp); + +app_portal.get("/auth0/verify", portalFunctions.verify); +app_portal.get("/gsuite/verify", portalFunctions.verify); + +//all initialization requests on portal frontend for forms are get requests +app_portal.get("/auth0/create-blank-profile", portalFunctions.create_blank_profile); +app_portal.get("/auth0/profile", portalFunctions.get_profile); +app_portal.get("/auth0/developer", portalFunctions.get_developer_profile); +app_portal.get("/auth0/checkin", portalFunctions.record_event); + +// http server endpoints +export default app_portal; diff --git a/functions/src/services/readme.md b/functions/src/services/readme.md new file mode 100644 index 0000000..eec87ba --- /dev/null +++ b/functions/src/services/readme.md @@ -0,0 +1,78 @@ +# Logging in ACM Core + +Logging is an important aspect of being able to understand the sequence of events that take place in the API. Several aspects make using regular `console.log` statements not ideal in a production context. We would like to have a logging solution which is robust, scalable, queryable, triggers alerts & more. + +### LogDNA + +The current solution used by ACM Core is using LogDNA as a third party external service. LogDNA has several premium features which are free for students and this guide will explain how to connect your LogDNA account to ACM Core. + + - [LogDNA Student](https://www.logdna.com/github-students) + +Set up a student account by following the steps at the link above. Once you have completed setting up an account grab your ingestion id and return to this readme. + +### Add to Logging Service + +The [logging.ts](./logging.ts) file sets up a `Logger` class that manages sending log information to everyone who has associated an ingestion id with the ACM Core project. + +##### Adding to Firebase Functions Config + + The first thing that we will need to do is add your LogDNA Ingestion ID as an environment variable. You can add it to the firebase functions config as follows. Make sure to replace `yourname` and `your-ingestion-id` in the command below. Make sure to just use your first name and not have any spaces in the command. + +``` +$ firebase functions:config:set logdna.yourname=your-ingestion-id +``` + +After this update the `.runtimeconfig.json` file with the new environment variable. Make sure to be at the root of the repository before running this command. + +``` +$ firebase functions:config:get > functions/.runtimeconfig.json +``` + +##### Updating the Logger + +In [logging.ts](./logging.ts) create a new LogDNA logger instance by duplicating the line below and changing the name of the logger to be your own. Also update the environment variable being passed in to be `functions.config().logdna.yourname` that was set in the previous section. + +``` +const harsha_logger = logdna.createLogger(functions.config().logdna.harsha, options); +``` + +Next append your logger to the array of LogDNA instances within the Logger class. + +``` +private logdna_loggers = [harsha_logger, YOURNAME_logger /*, insert additional loggers */]; +``` + +That's it! Your logger will now receive all the log data from the API and can be accessed in the LogDNA cloud console. + +##### Using the Logger + +An instance of the logger class can be imported into any file as follows + +``` +import logger from "../services/logging"; +``` + +Using the logger is as simple as using the single `.log()` function. + +``` +logger.log("hello world"); +``` + +The logger is capable of receiving objects too which can then be queryed in the LogDNA cloud console for additional visibility. + +``` +logger.log({ + ...my_object, + message: "logging a new message", +}); +``` + +You can also log entire request or response bodies when receiving an http request but this is not receommended since it makes log traversal much more challenging. There is also no need to log information like timestamps or environment context. These variables are automatically logged and tracked. + +### Questions + +Sometimes you may have additional questions. If the answer was not found in this readme please feel free to reach out to the [Director of Development](mailto:development@acmutd.co) for _ACM_ + +We request that you be as detailed as possible in your questions, doubts or concerns to ensure that we can be of maximum assistance. Thank you! + +![ACM Development](https://www.acmutd.co/brand/Development/Banners/light_dark_background.png)