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

feat: platform api v1 - get and update webhook settings #6608

Merged
merged 16 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions shared/constants/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export const STORAGE_FORM_SETTINGS_FIELDS = <const>[
'business',
]

export const WEBHOOK_SETTINGS_FIELDS = <const>['responseMode', 'webhook']

export const ADMIN_FORM_META_FIELDS = <const>[
'admin',
'title',
Expand Down
10 changes: 10 additions & 0 deletions shared/types/form/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,18 @@ export type StorageFormSettings = Pick<

export type FormSettings = EmailFormSettings | StorageFormSettings

export type FormWebhookSettings = Pick<FormSettings, 'webhook'>

export type FormWebhookResponseModeSettings = Pick<
FormSettings,
'webhook' | 'responseMode'
>
export type SettingsUpdateDto = PartialDeep<FormSettings>

export type WebhookSettingsUpdateDto = Pick<FormSettings, 'webhook'> & {
userEmail: string
}

/**
* Misnomer. More of a public form auth session.
*/
Expand Down
1 change: 1 addition & 0 deletions shared/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const UserBase = z.object({
keyHash: z.string(),
createdAt: z.date(),
lastUsedAt: z.date().optional(),
isPlatform: z.boolean().optional(),
})
.optional(),
})
Expand Down
6 changes: 6 additions & 0 deletions src/app/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,12 @@ export const optionalVarsSchema: Schema<IOptionalVarsSchema> = {
default: 100,
env: 'PUBLIC_API_RATE_LIMIT',
},
platformApi: {
doc: 'Per-minute, per-IP, per-instance request limit for platform APIs',
wanlingt marked this conversation as resolved.
Show resolved Hide resolved
format: 'int',
default: 100,
env: 'PLATFORM_API_RATE_LIMIT',
},
},
reactMigration: {
useFetchForSubmissions: {
Expand Down
12 changes: 12 additions & 0 deletions src/app/models/form.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
MB,
STORAGE_FORM_SETTINGS_FIELDS,
STORAGE_PUBLIC_FORM_FIELDS,
WEBHOOK_SETTINGS_FIELDS,
} from '../../../shared/constants'
import {
AdminDashboardFormMetaDto,
Expand All @@ -36,6 +37,8 @@ import {
FormSettings,
FormStartPage,
FormStatus,
FormWebhookResponseModeSettings,
FormWebhookSettings,
LogicConditionState,
LogicDto,
LogicType,
Expand Down Expand Up @@ -700,6 +703,15 @@ const compileFormModel = (db: Mongoose): IFormModel => {
return formSettings
}

FormDocumentSchema.methods.getWebhookAndResponseModeSettings =
function (): FormWebhookSettings {
const formSettings = pick(
this,
WEBHOOK_SETTINGS_FIELDS,
) as FormWebhookResponseModeSettings
return formSettings
}

FormDocumentSchema.methods.getPublicView = function (): PublicForm {
const basePublicView =
this.responseMode === FormResponseMode.Encrypt
Expand Down
3 changes: 3 additions & 0 deletions src/app/models/user.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ const compileUserModel = (db: Mongoose) => {
},
createdAt: Date,
lastUsedAt: Date,
isPlatform: {
type: Boolean,
},
},
},
},
Expand Down
87 changes: 86 additions & 1 deletion src/app/modules/auth/auth.middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import { StatusCodes } from 'http-status-codes'
import { createLoggerWithLabel } from '../../config/logger'
import { createReqMeta } from '../../utils/request'
import { ControllerHandler } from '../core/core.types'
import { UNAUTHORIZED_USER_MESSAGE } from '../user/user.constant'
import * as UserService from '../user/user.service'

import { getUserByApiKey } from './auth.service'
import {
getUserIdFromSession,
isCronPaymentAuthValid,
isUserInSession,
mapRouteError,
mapRoutePublicApiError,
} from './auth.utils'

Expand Down Expand Up @@ -163,7 +167,6 @@ export const authenticateApiKey: ControllerHandler = (req, res, next) => {
meta: {
action: 'authenticateApiKey',
userId: apiKeyMatch.groups.userId,
token: bearerMatch.groups.token,
},
})
return getUserByApiKey(apiKeyMatch.groups.userId, bearerMatch.groups.token)
Expand All @@ -182,3 +185,85 @@ export const authenticateApiKey: ControllerHandler = (req, res, next) => {
return res.status(statusCode).json({ message: errorMessage })
})
}

/**
* Middleware that checks if user is a platform user
*/
const isPlatformApiUser: ControllerHandler<
unknown,
unknown,
{ userEmail: string }
> = (req, res, next) => {
const { userEmail } = req.body
const sessionUserId = getUserIdFromSession(req.session)
if (!sessionUserId) {
return res.status(StatusCodes.UNAUTHORIZED).json(UNAUTHORIZED_USER_MESSAGE)
}
return UserService.getPopulatedApiUserById(sessionUserId)
.map((retrievedUser) => {
if (!retrievedUser) {
return res
.status(StatusCodes.UNAUTHORIZED)
.json(UNAUTHORIZED_USER_MESSAGE)
}
if (!retrievedUser.apiToken?.isPlatform || !userEmail) {
// Exit this middleware if
// 1. User is not a platform
// 2. User is a platform but has no userEmail provided
return next()
} else {
return UserService.findUserByEmail(userEmail)
.map((emailUser) => {
if (!emailUser) {
return res
.status(StatusCodes.UNPROCESSABLE_ENTITY)
.json('User not found')
}
logger.info({
message: 'API user is a platform',
meta: {
action: 'isPlatformApiUser',
...createReqMeta(req),
reqBody: req.body,
apiUser: sessionUserId,
userEmail,
},
})
req.session.user = { _id: emailUser._id }
return next()
})
.mapErr((error) => {
logger.error({
message: 'Error occurred whilst retrieving user from userEmail',
meta: {
action: 'isPlatformApiUser',
apiUser: sessionUserId,
userEmail,
},
error,
})

const { errorMessage, statusCode } = mapRouteError(error)
return res.status(statusCode).json({ message: errorMessage })
})
}
})
.mapErr((error) => {
logger.error({
message: 'Error occurred whilst retrieving user',
meta: {
action: 'isPlatformApiUser',
userId: sessionUserId,
},
error,
})

const { errorMessage, statusCode } = mapRouteError(error)
return res.status(statusCode).json({ message: errorMessage })
})
}

export const authenticateApiKeyAndPlatform = [
authenticateApiKey,
isPlatformApiUser,
] as ControllerHandler[]
4 changes: 2 additions & 2 deletions src/app/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
PrivateFormError,
} from '../form/form.errors'
import * as FormService from '../form/form.service'
import { findUserById } from '../user/user.service'
import { findApiUserById } from '../user/user.service'

import {
InvalidDomainError,
Expand Down Expand Up @@ -360,7 +360,7 @@ export const getUserByApiKey = (
userId: string,
token: string,
): ResultAsync<IUserSchema, Error> => {
return findUserById(userId).andThen((user) => {
return findApiUserById(userId).andThen((user) => {
if (!user.apiToken?.keyHash) {
return errAsync(new MissingTokenError())
}
Expand Down
Loading
Loading