diff --git a/packages/doorman-api/src/functions/api/door/onboard.ts b/packages/doorman-api/src/functions/api/door/onboard.ts index d1ef747..9edcea5 100644 --- a/packages/doorman-api/src/functions/api/door/onboard.ts +++ b/packages/doorman-api/src/functions/api/door/onboard.ts @@ -8,29 +8,37 @@ import { createDynaBridgeClient } from "../../../utils/ddb"; import DiscordOauth2 from "discord-oauth2"; import { DoorAliasSchema, getDoorAliasID } from "../../../schema/DoorAlias"; import { Counter } from "prom-client"; +import { z } from "zod"; +import zu from "zod_utilz"; +import { setResponseJson } from "../../../utils/responseUtils"; -export interface OnboardRequest extends ServerlessEventObject<{}, UserAgentHeader> { - newConfig?: string; +export const OnboardRequestSchema = z.object({ + newConfig: zu.stringToJSON().optional(), + code: z.string().optional(), + state: zu.stringToJSON().optional(), +}).partial() +.refine(data => data.newConfig || (data.code && data.state), 'newConfig or (code and state) must be specified'); - // for oauth redirect - code?: string; - state?: string; -} +export type OnboardRequest = z.infer; -export interface DiscordOnboardingState { - name: string; - id: string; - apiRedirect: string; -} +export interface OnboardRequestTwilio extends ServerlessEventObject { }; + +export const DiscordOnboardingStateSchema = z.object({ + name: z.string(), + id: z.string(), + apiRedirect: z.string(), +}); + +export type DiscordOnboardingState = z.infer; const ONBOARDING_SCOPE = ['identify', 'email', 'guilds.join']; -export const handler: ServerlessFunctionSignature = withMetrics('onboard', async (context, event, callback, metricsRegistry) => { +export const handler: ServerlessFunctionSignature = withMetrics('onboard', async (context, event, callback, metricsRegistry) => { const response = new Twilio.Response(); - response.appendHeader('Content-Type', 'application/json'); - registerMetrics(metricsRegistry); + const req = OnboardRequestSchema.parse(event); + const db = createDynaBridgeClient(context); // return oauth link @@ -41,23 +49,13 @@ export const handler: ServerlessFunctionSignature }); // create door config route - if (event.code && event.state) { - const code = event.code; - let discordState: DiscordOnboardingState; - - try { - discordState = JSON.parse(event.state); - } catch (e) { - response.setStatusCode(400); - response.setBody({ err: "invalid state" }); - return callback(null, response); - } + if (req.code && req.state) { + const code = req.code; + let discordState: DiscordOnboardingState = DiscordOnboardingStateSchema.parse(req.state); const config = await db.entities.onboardDoorConfig.findById(getOnboardDoorId(discordState.name)); if (!config || config.nonce !== discordState.id) { - response.setStatusCode(404) - .setBody({ err: "approval not found" }); - + setResponseJson(response, 404, { err: "approval not found" }); return callback(null, response); } @@ -73,8 +71,7 @@ export const handler: ServerlessFunctionSignature }); } catch (err) { console.log("something went wrong with discord authorization"); - response.setStatusCode(401); - response.setBody({ err }); + setResponseJson(response, 401, { err }); return callback(null, response); } @@ -84,9 +81,7 @@ export const handler: ServerlessFunctionSignature console.log(profileId); if (!profileId) { - response.setStatusCode(404); - response.setBody({ err: "profile not found" }); - + setResponseJson(response, 404, { err: "profile not found" }); return callback(null, response); } @@ -144,37 +139,26 @@ export const handler: ServerlessFunctionSignature // and all must succeed or none succeed try { await db.transaction([createDoorAlias, createDoorConfig, deleteOnboardingConfig]); - response.setStatusCode(200); - response.setBody({ redirect: context.DOORMAN_URL + `?door=${config.name}` }); + setResponseJson(response, 200, { redirect: context.DOORMAN_URL + `?door=${config.name}` }); } catch (e) { getMetricFromRegistry(metricsRegistry, OnboardMetrics.TRANSACTION_CONFLICT).inc(1); console.error(e); - response.setStatusCode(409); - response.setBody({ err: "something went wrong during onboarding" }); + setResponseJson(response, 409, { err: "something went wrong during onboarding" }); } return callback(null, response); } - let newConfig = event.newConfig; + let newConfig = req.newConfig; if (!newConfig) { - response.setStatusCode(400); + setResponseJson(response, 400, { err: "missing newConfig" }); return callback(null, response); } - let newConfigParsed: OnboardDoorReq; - console.log("parsing config"); - try { - newConfigParsed = OnboardDoorReqSchema.parse(JSON.parse(newConfig)); - } catch (err) { - response.setStatusCode(400); - response.setBody({ err }); - return callback(null, response); - } - + let newConfigParsed: OnboardDoorReq = OnboardDoorReqSchema.parse(newConfig); const newConfigObj = createOnboardDoorConfig(newConfigParsed); // check if this name or buzzer is already registered? @@ -182,8 +166,7 @@ export const handler: ServerlessFunctionSignature if (existingAlias) { getMetricFromRegistry(metricsRegistry, OnboardMetrics.BUZZER_CONFLICT).inc({ buzzer: newConfigObj.buzzer }, 1); - response.setStatusCode(409); - response.setBody({ err: `A buzzer is already registered with the number ${newConfigObj.buzzer}` }); + setResponseJson(response, 409, { err: `A buzzer is already registered with the number ${newConfigObj.buzzer}` }); return callback(null, response); } @@ -191,8 +174,7 @@ export const handler: ServerlessFunctionSignature if (existingConfig) { getMetricFromRegistry(metricsRegistry, OnboardMetrics.NAME_CONFLICT).inc({ name: newConfigObj.name }, 1); - response.setStatusCode(409); - response.setBody({ err: `A buzzer is already registered with the name ${newConfigObj.name}` }); + setResponseJson(response, 409, { err: `A buzzer is already registered with the name ${newConfigObj.name}` }); return callback(null, response); } @@ -202,11 +184,11 @@ export const handler: ServerlessFunctionSignature // save to DB, this is a middle step until discord auth comes in await db.entities.onboardDoorConfig.save(newConfigObj); - const discordState: DiscordOnboardingState = { + const discordState: DiscordOnboardingState = DiscordOnboardingStateSchema.parse({ name: newConfigObj.name, id: newConfigObj.nonce, apiRedirect: '/api/door/onboard', - } + }); const redirect = oauth.generateAuthUrl({ scope: ONBOARDING_SCOPE, @@ -214,7 +196,6 @@ export const handler: ServerlessFunctionSignature state: JSON.stringify(discordState), }); - response.setStatusCode(200); - response.setBody({ redirect }); + setResponseJson(response, 200, { redirect }); return callback(null, response); });