From 7a7233ead4630d12aea291ed44a4caeffaa7b00f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sat, 19 Aug 2023 22:23:15 -0700 Subject: [PATCH] Add api validations. --- lib/middleware.ts | 33 +++++++--- lib/types.ts | 17 ++++- lib/yup.ts | 19 ++++++ pages/api/auth/login.ts | 31 +++++---- pages/api/event-data/events.ts | 29 ++++++--- pages/api/event-data/fields.ts | 23 +++++-- pages/api/event-data/stats.ts | 23 +++++-- pages/api/me/password.ts | 16 ++++- pages/api/me/teams.ts | 19 +++++- pages/api/me/websites.ts | 18 +++++- pages/api/realtime/[id].ts | 16 ++++- pages/api/reports/[id].ts | 36 +++++++++-- pages/api/reports/funnel.ts | 23 ++++++- pages/api/reports/index.ts | 33 ++++++++-- pages/api/reports/insights.ts | 39 ++++++++++- pages/api/reports/retention.ts | 31 +++++---- pages/api/send.ts | 68 +++++++++++--------- pages/api/share/[id].ts | 13 +++- pages/api/teams/[id]/index.ts | 22 ++++++- pages/api/teams/[id]/users/[userId].ts | 14 +++- pages/api/teams/[id]/users/index.ts | 25 +------ pages/api/teams/[id]/websites/[websiteId].ts | 13 +++- pages/api/teams/[id]/websites/index.ts | 21 +++++- pages/api/teams/index.ts | 20 +++++- pages/api/teams/join.ts | 21 ++++-- pages/api/users/[id]/index.ts | 20 +++++- pages/api/users/[id]/teams.ts | 15 ++++- pages/api/users/[id]/usage.ts | 14 +++- pages/api/users/[id]/websites.ts | 25 +++++-- pages/api/users/index.ts | 24 ++++++- pages/api/websites/[id]/active.ts | 12 +++- pages/api/websites/[id]/daterange.ts | 12 +++- pages/api/websites/[id]/events.ts | 22 +++++-- pages/api/websites/[id]/index.ts | 12 +++- pages/api/websites/[id]/metrics.ts | 12 +++- pages/api/websites/[id]/pageviews.ts | 12 +++- pages/api/websites/[id]/reports.ts | 12 +++- pages/api/websites/[id]/reset.ts | 12 +++- pages/api/websites/[id]/stats.ts | 12 +++- pages/api/websites/[id]/values.ts | 12 +++- pages/api/websites/index.ts | 19 +++++- 41 files changed, 690 insertions(+), 180 deletions(-) create mode 100644 lib/yup.ts diff --git a/lib/middleware.ts b/lib/middleware.ts index 414cab23..0cb0cb88 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -1,19 +1,20 @@ +import redis from '@umami/redis-client'; +import cors from 'cors'; +import debug from 'debug'; +import { getAuthToken, parseShareToken } from 'lib/auth'; +import { ROLES } from 'lib/constants'; +import { isUuid, secret } from 'lib/crypto'; +import { findSession } from 'lib/session'; import { - createMiddleware, - unauthorized, badRequest, + createMiddleware, parseSecureToken, tooManyRequest, + unauthorized, } from 'next-basics'; -import debug from 'debug'; -import cors from 'cors'; -import redis from '@umami/redis-client'; -import { findSession } from 'lib/session'; -import { getAuthToken, parseShareToken } from 'lib/auth'; -import { secret, isUuid } from 'lib/crypto'; -import { ROLES } from 'lib/constants'; -import { getUserById } from '../queries'; import { NextApiRequestCollect } from 'pages/api/send'; +import { getUserById } from '../queries'; +import { NextApiRequestQueryBody } from './types'; const log = debug('umami:middleware'); @@ -75,3 +76,15 @@ export const useAuth = createMiddleware(async (req, res, next) => { next(); }); + +export const useValidate = createMiddleware(async (req: any, res, next) => { + try { + const { yup } = req as NextApiRequestQueryBody; + + yup[req.method].validateSync({ ...req.query, ...req.body }); + } catch (e: any) { + return badRequest(res, e.message); + } + + next(); +}); diff --git a/lib/types.ts b/lib/types.ts index 3f3ac533..3f3839a4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -5,11 +5,13 @@ import { EVENT_TYPE, KAFKA_TOPIC, REPORT_FILTER_TYPES, + REPORT_TYPES, ROLES, TEAM_FILTER_TYPES, USER_FILTER_TYPES, WEBSITE_FILTER_TYPES, } from './constants'; +import * as yup from 'yup'; type ObjectValues = T[keyof T]; @@ -18,6 +20,8 @@ export type Role = ObjectValues; export type EventType = ObjectValues; export type DynamicDataType = ObjectValues; export type KafkaTopic = ObjectValues; +export type ReportType = ObjectValues; + export type ReportSearchFilterType = ObjectValues; export type UserSearchFilterType = ObjectValues; export type WebsiteSearchFilterType = ObjectValues; @@ -47,8 +51,8 @@ export interface ReportSearchFilter extends SearchFilter export interface SearchFilter { filter?: string; filterType?: T; - pageSize?: number; - page?: number; + pageSize: number; + page: number; orderBy?: string; } @@ -76,11 +80,19 @@ export interface Auth { }; } +export interface YupRequest { + GET?: yup.ObjectSchema; + POST?: yup.ObjectSchema; + PUT?: yup.ObjectSchema; + DELETE?: yup.ObjectSchema; +} + export interface NextApiRequestQueryBody extends NextApiRequest { auth?: Auth; query: TQuery & { [key: string]: string | string[] }; body: TBody; headers: any; + yup: YupRequest; } export interface NextApiRequestAuth extends NextApiRequest { @@ -168,7 +180,6 @@ export interface RealtimeUpdate { export interface DateRange { startDate: Date; endDate: Date; - unit: string; value: string; } diff --git a/lib/yup.ts b/lib/yup.ts new file mode 100644 index 00000000..a9d21028 --- /dev/null +++ b/lib/yup.ts @@ -0,0 +1,19 @@ +import * as yup from 'yup'; + +export function getDateRangeValidation() { + return { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + }; +} + +// ex: /funnel|insights|retention/i +export function getFilterValidation(matchRegex) { + return { + filter: yup.string(), + filterType: yup.string().matches(matchRegex), + pageSize: yup.number().integer().positive().max(200), + page: yup.number().integer().positive(), + orderBy: yup.string(), + }; +} diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index b9a2be00..47521084 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -1,19 +1,20 @@ +import redis from '@umami/redis-client'; import debug from 'debug'; +import { setAuthKey } from 'lib/auth'; +import { secret } from 'lib/crypto'; +import { useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody, User } from 'lib/types'; import { NextApiResponse } from 'next'; import { - ok, - unauthorized, - badRequest, checkPassword, createSecureToken, - methodNotAllowed, forbidden, + methodNotAllowed, + ok, + unauthorized, } from 'next-basics'; -import redis from '@umami/redis-client'; import { getUserByUsername } from 'queries'; -import { secret } from 'lib/crypto'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { setAuthKey } from 'lib/auth'; +import * as yup from 'yup'; const log = debug('umami:auth'); @@ -27,6 +28,13 @@ export interface LoginResponse { user: User; } +const schema = { + POST: yup.object().shape({ + username: yup.string().required(), + password: yup.string().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -35,13 +43,12 @@ export default async ( return forbidden(res); } + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { username, password } = req.body; - if (!username || !password) { - return badRequest(res); - } - const user = await getUserByUsername(username, { includePassword: true }); if (user && checkPassword(password, user.password)) { diff --git a/pages/api/event-data/events.ts b/pages/api/event-data/events.ts index 9f8f964b..da0afc65 100644 --- a/pages/api/event-data/events.ts +++ b/pages/api/event-data/events.ts @@ -1,26 +1,37 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventDataEvents } from 'queries'; +import * as yup from 'yup'; -export interface EventDataEventsRequestQuery { +export interface EventDataFieldsRequestQuery { websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; - event?: string; + startAt: string; + endAt: string; + event: string; } +const schema = { + GET: yup.object().shape({ + websiteId: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + event: yup.string().required(), + }), +}; + export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { websiteId, startAt, endAt, event } = req.query; diff --git a/pages/api/event-data/fields.ts b/pages/api/event-data/fields.ts index b6a73133..1cd24fe6 100644 --- a/pages/api/event-data/fields.ts +++ b/pages/api/event-data/fields.ts @@ -1,19 +1,27 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventDataFields } from 'queries'; +import * as yup from 'yup'; export interface EventDataFieldsRequestQuery { websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; + startAt: string; + endAt: string; field?: string; } +const schema = { + GET: yup.object().shape({ + websiteId: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + field: yup.string(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -21,6 +29,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { websiteId, startAt, endAt, field } = req.query; diff --git a/pages/api/event-data/stats.ts b/pages/api/event-data/stats.ts index 4ba843be..b7b70dbf 100644 --- a/pages/api/event-data/stats.ts +++ b/pages/api/event-data/stats.ts @@ -1,18 +1,24 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; -import { getEventDataStats } from 'queries'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import * as yup from 'yup'; export interface EventDataStatsRequestQuery { websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; + startAt: string; + endAt: string; } +const schema = { + GET: yup.object().shape({ + websiteId: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -20,6 +26,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { websiteId, startAt, endAt } = req.query; diff --git a/pages/api/me/password.ts b/pages/api/me/password.ts index f9f60fc5..6f49a182 100644 --- a/pages/api/me/password.ts +++ b/pages/api/me/password.ts @@ -1,15 +1,16 @@ +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, User } from 'lib/types'; -import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, checkPassword, + forbidden, hashPassword, methodNotAllowed, - forbidden, ok, } from 'next-basics'; import { getUserById, updateUser } from 'queries'; +import * as yup from 'yup'; export interface UserPasswordRequestQuery { id: string; @@ -20,6 +21,14 @@ export interface UserPasswordRequestBody { newPassword: string; } +const schema = { + POST: yup.object().shape({ + id: yup.string().uuid().required(), + currentPassword: yup.string().required(), + newPassword: yup.string().min(8).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -30,6 +39,9 @@ export default async ( await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { currentPassword, newPassword } = req.body; const { id } = req.auth.user; diff --git a/pages/api/me/teams.ts b/pages/api/me/teams.ts index d323043b..d394ef07 100644 --- a/pages/api/me/teams.ts +++ b/pages/api/me/teams.ts @@ -1,10 +1,20 @@ -import { useCors } from 'lib/middleware'; +import { useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; +import * as yup from 'yup'; -export interface MyTeamsRequestQuery extends SearchFilter {} +export interface MyTeamsRequestQuery extends SearchFilter { + id: string; +} + +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Owner/i), + }), +}; export default async ( req: NextApiRequestQueryBody, @@ -12,7 +22,12 @@ export default async ( ) => { await useCors(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { + req.query.id = req.auth.user.id; + return userTeams(req, res); } diff --git a/pages/api/me/websites.ts b/pages/api/me/websites.ts index 238d1b6e..d4a803a0 100644 --- a/pages/api/me/websites.ts +++ b/pages/api/me/websites.ts @@ -1,11 +1,20 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; - import userWebsites from 'pages/api/users/[id]/websites'; +import * as yup from 'yup'; -export interface MyWebsitesRequestQuery extends SearchFilter {} +export interface MyWebsitesRequestQuery extends SearchFilter { + id: string; +} + +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Domain/i), + }), +}; export default async ( req: NextApiRequestQueryBody, @@ -14,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { req.query.id = req.auth.user.id; diff --git a/pages/api/realtime/[id].ts b/pages/api/realtime/[id].ts index e78599c6..ab7bb406 100644 --- a/pages/api/realtime/[id].ts +++ b/pages/api/realtime/[id].ts @@ -1,22 +1,34 @@ import { subMinutes } from 'date-fns'; import { canViewWebsite } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, RealtimeInit } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRealtimeData } from 'queries'; - +import * as yup from 'yup'; export interface RealtimeRequestQuery { id: string; startAt: number; } +const currentDate = new Date().getTime(); + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + startAt: yup.number().integer().max(currentDate).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { const { id: websiteId, startAt } = req.query; diff --git a/pages/api/reports/[id].ts b/pages/api/reports/[id].ts index 85bc302c..eb4199bc 100644 --- a/pages/api/reports/[id].ts +++ b/pages/api/reports/[id].ts @@ -1,9 +1,10 @@ -import { canUpdateReport, canViewReport, canDeleteReport } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { canDeleteReport, canUpdateReport, canViewReport } from 'lib/auth'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody, ReportType, YupRequest } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getReportById, updateReport, deleteReport } from 'queries'; +import { deleteReport, getReportById, updateReport } from 'queries'; +import * as yup from 'yup'; export interface ReportRequestQuery { id: string; @@ -11,12 +12,34 @@ export interface ReportRequestQuery { export interface ReportRequestBody { websiteId: string; - type: string; + type: ReportType; name: string; description: string; parameters: string; } +const schema: YupRequest = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + websiteId: yup.string().uuid().required(), + type: yup + .string() + .matches(/funnel|insights|retention/i) + .required(), + name: yup.string().max(200).required(), + description: yup.string().max(500), + parameters: yup + .object() + .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), + }), + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -24,6 +47,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: reportId } = req.query; const { user: { id: userId }, diff --git a/pages/api/reports/funnel.ts b/pages/api/reports/funnel.ts index 33882e03..a51817bf 100644 --- a/pages/api/reports/funnel.ts +++ b/pages/api/reports/funnel.ts @@ -1,9 +1,10 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getFunnel } from 'queries'; +import * as yup from 'yup'; export interface FunnelRequestBody { websiteId: string; @@ -22,6 +23,21 @@ export interface FunnelResponse { endAt: number; } +const schema = { + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + urls: yup.array().min(2).of(yup.string()).required(), + window: yup.number().positive().required(), + dateRange: yup + .object() + .shape({ + startDate: yup.date().required(), + endDate: yup.date().required(), + }) + .required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -29,6 +45,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { websiteId, diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index 762f297c..e62a1cc5 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -1,10 +1,11 @@ -import { canViewWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getReportsByUserId, getReportsByWebsiteId } from 'queries'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createReport, getReportsByUserId } from 'queries'; +import * as yup from 'yup'; export interface ReportsRequestQuery extends SearchFilter {} @@ -14,11 +15,28 @@ export interface ReportRequestBody { type: string; description: string; parameters: { - window: string; - urls: string[]; + [key: string]: any; }; } +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Description|Type|Username|Website Name|Website Domain/i), + }), + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + name: yup.string().max(200).required(), + type: yup + .string() + .matches(/funnel|insights|retention/i) + .required(), + description: yup.string().max(500), + parameters: yup + .object() + .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -26,6 +44,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user: { id: userId }, } = req.auth; diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts index 09a07d2f..04e51d4c 100644 --- a/pages/api/reports/insights.ts +++ b/pages/api/reports/insights.ts @@ -1,9 +1,10 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getInsights } from 'queries'; +import * as yup from 'yup'; export interface InsightsRequestBody { websiteId: string; @@ -16,6 +17,37 @@ export interface InsightsRequestBody { groups: { name: string; type: string }[]; } +const schema = { + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + dateRange: yup + .object() + .shape({ + startDate: yup.date().required(), + endDate: yup.date().required(), + }) + .required(), + fields: yup + .array() + .of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + value: yup.string().required(), + }), + ) + .min(1) + .required(), + filters: yup.array().of(yup.string()).min(1).required(), + groups: yup.array().of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + }), + ), + }), +}; + function convertFilters(filters) { return filters.reduce((obj, { name, ...value }) => { obj[name] = value; @@ -31,6 +63,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { websiteId, diff --git a/pages/api/reports/retention.ts b/pages/api/reports/retention.ts index 40b3266b..4006ab12 100644 --- a/pages/api/reports/retention.ts +++ b/pages/api/reports/retention.ts @@ -1,33 +1,43 @@ import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRetention } from 'queries'; +import * as yup from 'yup'; export interface RetentionRequestBody { websiteId: string; - dateRange: { window; startDate: string; endDate: string }; - timezone: string; + dateRange: { startDate: string; endDate: string }; } -export interface RetentionResponse { - startAt: number; - endAt: number; -} +const schema = { + POST: yup.object().shape({ + websiteId: yup.string().uuid().required(), + dateRange: yup + .object() + .shape({ + startDate: yup.date().required(), + endDate: yup.date().required(), + }) + .required(), + }), +}; export default async ( req: NextApiRequestQueryBody, - res: NextApiResponse, + res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { websiteId, dateRange: { startDate, endDate }, - timezone, } = req.body; if (!(await canViewWebsite(req.auth, websiteId))) { @@ -37,7 +47,6 @@ export default async ( const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), - timezone, }); return ok(res, data); diff --git a/pages/api/send.ts b/pages/api/send.ts index f90ded77..a379f261 100644 --- a/pages/api/send.ts +++ b/pages/api/send.ts @@ -1,14 +1,15 @@ -import isbot from 'isbot'; -import ipaddr from 'ipaddr.js'; -import { createToken, ok, send, badRequest, forbidden } from 'next-basics'; -import { saveEvent, saveSessionData } from 'queries'; -import { useCors, useSession } from 'lib/middleware'; -import { getJsonBody, getIpAddress } from 'lib/detect'; -import { secret } from 'lib/crypto'; -import { NextApiRequest, NextApiResponse } from 'next'; import { Resolver } from 'dns/promises'; -import { CollectionType } from 'lib/types'; -import { COLLECTION_TYPE } from 'lib/constants'; +import ipaddr from 'ipaddr.js'; +import isbot from 'isbot'; +import { COLLECTION_TYPE, HOSTNAME_REGEX } from 'lib/constants'; +import { secret } from 'lib/crypto'; +import { getIpAddress, getJsonBody } from 'lib/detect'; +import { useCors, useSession, useValidate } from 'lib/middleware'; +import { CollectionType, YupRequest } from 'lib/types'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { badRequest, createToken, forbidden, ok, send } from 'next-basics'; +import { saveEvent, saveSessionData } from 'queries'; +import * as yup from 'yup'; export interface CollectRequestBody { payload: { @@ -43,8 +44,32 @@ export interface NextApiRequestCollect extends NextApiRequest { city: string; }; headers: { [key: string]: any }; + yup: YupRequest; } +const schema = { + POST: yup.object().shape({ + payload: yup + .object() + .shape({ + data: yup.object(), + hostname: yup.string().matches(HOSTNAME_REGEX).max(100), + language: yup.string().max(35), + referrer: yup.string().max(500), + screen: yup.string().max(11), + title: yup.string().max(500), + url: yup.string().max(500), + website: yup.string().uuid().required(), + name: yup.string().max(50), + }) + .required(), + type: yup + .string() + .matches(/event|identify/i) + .required(), + }), +}; + export default async (req: NextApiRequestCollect, res: NextApiResponse) => { await useCors(req, res); @@ -54,11 +79,8 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { const { type, payload } = getJsonBody(req); - const error = validateBody({ type, payload }); - - if (error) { - return badRequest(res, error); - } + req.yup = schema; + await useValidate(req, res); if (await hasBlockedIp(req)) { return forbidden(res); @@ -118,22 +140,6 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { return send(res, token); }; -function validateBody({ type, payload }: CollectRequestBody) { - if (!type || !payload) { - return 'Invalid payload.'; - } - - if (type !== COLLECTION_TYPE.event && type !== COLLECTION_TYPE.identify) { - return 'Wrong payload type.'; - } - - const { data } = payload; - - if (data && !(typeof data === 'object' && !Array.isArray(data))) { - return 'Invalid event data.'; - } -} - async function hasBlockedIp(req: NextApiRequestCollect) { const ignoreIps = process.env.IGNORE_IP; const ignoreHostnames = process.env.IGNORE_HOSTNAME; diff --git a/pages/api/share/[id].ts b/pages/api/share/[id].ts index 0592d216..ad642283 100644 --- a/pages/api/share/[id].ts +++ b/pages/api/share/[id].ts @@ -1,8 +1,10 @@ -import { NextApiRequestQueryBody } from 'lib/types'; import { secret } from 'lib/crypto'; +import { useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { createToken, methodNotAllowed, notFound, ok } from 'next-basics'; import { getWebsiteByShareId } from 'queries'; +import * as yup from 'yup'; export interface ShareRequestQuery { id: string; @@ -13,10 +15,19 @@ export interface ShareResponse { token: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { + req.yup = schema; + await useValidate(req, res); + const { id: shareId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index 7fb664a0..31c47b2f 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -1,10 +1,11 @@ import { Team } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeam, getTeamById, updateTeam } from 'queries'; +import * as yup from 'yup'; export interface TeamRequestQuery { id: string; @@ -15,12 +16,29 @@ export interface TeamRequestBody { accessCode: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + name: yup.string().max(50).required(), + accessCode: yup.string().max(50).required(), + }), + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: teamId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/teams/[id]/users/[userId].ts b/pages/api/teams/[id]/users/[userId].ts index 1e4ca623..adb635d5 100644 --- a/pages/api/teams/[id]/users/[userId].ts +++ b/pages/api/teams/[id]/users/[userId].ts @@ -1,18 +1,28 @@ import { canDeleteTeamUser } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeamUser } from 'queries'; - +import * as yup from 'yup'; export interface TeamUserRequestQuery { id: string; userId: string; } +const schema = { + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + userId: yup.string().uuid().required(), + }), +}; + export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'DELETE') { const { id: teamId, userId } = req.query; diff --git a/pages/api/teams/[id]/users/index.ts b/pages/api/teams/[id]/users/index.ts index 6f8b077e..52b25da6 100644 --- a/pages/api/teams/[id]/users/index.ts +++ b/pages/api/teams/[id]/users/index.ts @@ -1,9 +1,9 @@ -import { canUpdateTeam, canViewTeam } from 'lib/auth'; +import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getUserByUsername, getUsersByTeamId } from 'queries'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getUsersByTeamId } from 'queries'; export interface TeamUserRequestQuery extends SearchFilter { id: string; @@ -38,24 +38,5 @@ export default async ( return ok(res, users); } - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const { email, roleId: roleId } = req.body; - - // Check for User - const user = await getUserByUsername(email); - - if (!user) { - return badRequest(res, 'The User does not exists.'); - } - - const updated = await createTeamUser(user.id, teamId, roleId); - - return ok(res, updated); - } - return methodNotAllowed(res); }; diff --git a/pages/api/teams/[id]/websites/[websiteId].ts b/pages/api/teams/[id]/websites/[websiteId].ts index 795295d3..ada1efdc 100644 --- a/pages/api/teams/[id]/websites/[websiteId].ts +++ b/pages/api/teams/[id]/websites/[websiteId].ts @@ -1,21 +1,32 @@ import { canDeleteTeamWebsite } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeamWebsite } from 'queries/admin/teamWebsite'; +import * as yup from 'yup'; export interface TeamWebsitesRequestQuery { id: string; websiteId: string; } +const schema = { + DELETE: yup.object().shape({ + id: yup.string().uuid().required(), + websiteId: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: teamId, websiteId } = req.query; if (req.method === 'DELETE') { diff --git a/pages/api/teams/[id]/websites/index.ts b/pages/api/teams/[id]/websites/index.ts index dcd08939..4de32709 100644 --- a/pages/api/teams/[id]/websites/index.ts +++ b/pages/api/teams/[id]/websites/index.ts @@ -1,9 +1,10 @@ import { canViewTeam } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsites, getWebsitesByTeamId } from 'queries'; +import { getWebsitesByTeamId } from 'queries'; import { createTeamWebsites } from 'queries/admin/teamWebsite'; export interface TeamWebsiteRequestQuery extends SearchFilter { @@ -14,12 +15,28 @@ export interface TeamWebsiteRequestBody { websiteIds?: string[]; } +import * as yup from 'yup'; + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + ...getFilterValidation(/All|Name|Domain/i), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + websiteIds: yup.array().of(yup.string()).min(1).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: teamId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 997ed885..dd742b9e 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,23 +1,39 @@ import { Team } from '@prisma/client'; import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createTeam, getTeamsByUserId } from 'queries'; +import * as yup from 'yup'; export interface TeamsRequestQuery extends SearchFilter {} -export interface TeamsRequestBody extends SearchFilter { +export interface TeamsRequestBody { name: string; } +export interface MyTeamsRequestQuery extends SearchFilter {} + +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Owner/i), + }), + POST: yup.object().shape({ + name: yup.string().max(50).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user: { id: userId }, } = req.auth; diff --git a/pages/api/teams/join.ts b/pages/api/teams/join.ts index ce7367a0..06feda8a 100644 --- a/pages/api/teams/join.ts +++ b/pages/api/teams/join.ts @@ -1,21 +1,30 @@ import { Team } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, notFound } from 'next-basics'; -import { createTeamUser, getTeamByAccessCode, getTeamUser } from 'queries'; import { ROLES } from 'lib/constants'; - +import { useAuth, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, notFound, ok } from 'next-basics'; +import { createTeamUser, getTeamByAccessCode, getTeamUser } from 'queries'; +import * as yup from 'yup'; export interface TeamsJoinRequestBody { accessCode: string; } +const schema = { + POST: yup.object().shape({ + accessCode: yup.string().max(50).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'POST') { const { accessCode } = req.body; diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index e09b1b5f..3ac560ed 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,9 +1,10 @@ -import { NextApiRequestQueryBody, Role, User } from 'lib/types'; import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; +import { NextApiRequestQueryBody, Role, User } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteUser, getUserById, getUserByUsername, updateUser } from 'queries'; +import * as yup from 'yup'; export interface UserRequestQuery { id: string; @@ -15,12 +16,27 @@ export interface UserRequestBody { role: Role; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + username: yup.string().max(255), + password: yup.string(), + role: yup.string().matches(/admin|user|view-only/i), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user: { id: userId, isAdmin }, } = req.auth; diff --git a/pages/api/users/[id]/teams.ts b/pages/api/users/[id]/teams.ts index 831a992d..eb34410c 100644 --- a/pages/api/users/[id]/teams.ts +++ b/pages/api/users/[id]/teams.ts @@ -1,9 +1,10 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getTeamsByUserId } from 'queries'; - +import * as yup from 'yup'; export interface UserTeamsRequestQuery extends SearchFilter { id: string; } @@ -14,6 +15,13 @@ export interface UserTeamsRequestBody { shareId: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + ...getFilterValidation('/All|Name|Owner/i'), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -21,6 +29,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user } = req.auth; const { id: userId } = req.query; diff --git a/pages/api/users/[id]/usage.ts b/pages/api/users/[id]/usage.ts index 0118df92..b0fc2055 100644 --- a/pages/api/users/[id]/usage.ts +++ b/pages/api/users/[id]/usage.ts @@ -1,8 +1,9 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventDataUsage, getEventUsage, getUserWebsites } from 'queries'; +import * as yup from 'yup'; export interface UserUsageRequestQuery { id: string; @@ -21,6 +22,14 @@ export interface UserUsageRequestResponse { }[]; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -28,6 +37,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user } = req.auth; if (req.method === 'GET') { diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index 0e9231f7..65e9a0e8 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -1,25 +1,36 @@ -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByUserId } from 'queries'; +import * as yup from 'yup'; export interface UserWebsitesRequestQuery extends SearchFilter { id: string; -} -export interface UserWebsitesRequestBody { - name: string; - domain: string; - shareId: string; + includeTeams?: boolean; + onlyTeams?: boolean; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + includeTeams: yup.boolean(), + onlyTeams: yup.boolean(), + ...getFilterValidation(/All|Name|Domain/i), + }), +}; + export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { user } = req.auth; const { id: userId, page, filter, pageSize, includeTeams, onlyTeams } = req.query; diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 5e913c02..0b523c70 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,8 +1,9 @@ import { canCreateUser, canViewUsers } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; @@ -15,12 +16,31 @@ export interface UsersRequestBody { role?: Role; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Username/i), + }), + POST: yup.object().shape({ + username: yup.string().max(255).required(), + password: yup.string().required(), + id: yup.string().uuid(), + role: yup + .string() + .matches(/admin|user|view-only/i) + .required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + if (req.method === 'GET') { if (!(await canViewUsers(req.auth))) { return unauthorized(res); @@ -28,7 +48,7 @@ export default async ( const { page, filter, pageSize } = req.query; - const users = await getUsers({ page, filter, pageSize: +pageSize || null }); + const users = await getUsers({ page, filter, pageSize: pageSize ? +pageSize : null }); return ok(res, users); } diff --git a/pages/api/websites/[id]/active.ts b/pages/api/websites/[id]/active.ts index 99c8d999..abc23dd7 100644 --- a/pages/api/websites/[id]/active.ts +++ b/pages/api/websites/[id]/active.ts @@ -1,14 +1,21 @@ import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getActiveVisitors } from 'queries'; +import * as yup from 'yup'; export interface WebsiteActiveRequestQuery { id: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/daterange.ts b/pages/api/websites/[id]/daterange.ts index dc043560..bfa5338e 100644 --- a/pages/api/websites/[id]/daterange.ts +++ b/pages/api/websites/[id]/daterange.ts @@ -1,14 +1,21 @@ import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsiteDateRange } from 'queries'; +import * as yup from 'yup'; export interface WebsiteDateRangeRequestQuery { id: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/events.ts b/pages/api/websites/[id]/events.ts index 7d4f999f..427cb40e 100644 --- a/pages/api/websites/[id]/events.ts +++ b/pages/api/websites/[id]/events.ts @@ -1,6 +1,6 @@ import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -16,9 +16,21 @@ export interface WebsiteEventsRequestQuery { unit: string; timezone: string; url: string; - eventName: string; } +import * as yup from 'yup'; + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), + unit: yup.string().required(), + timezone: yup.string().required(), + url: yup.string(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -26,7 +38,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { id: websiteId, timezone, url, eventName } = req.query; + req.yup = schema; + await useValidate(req, res); + + const { id: websiteId, timezone, url } = req.query; const { startDate, endDate, unit } = await parseDateRangeQuery(req); if (req.method === 'GET') { @@ -44,7 +59,6 @@ export default async ( timezone, unit, url, - eventName, }); return ok(res, events); diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 3d053d0e..597568de 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -2,7 +2,7 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { Website, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { deleteWebsite, getWebsiteById, updateWebsite } from 'queries'; import { SHARE_ID_REGEX } from 'lib/constants'; @@ -16,6 +16,13 @@ export interface WebsiteRequestBody { shareId: string; } +import * as yup from 'yup'; + +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -23,6 +30,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index 7c84583c..67c15eca 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -2,10 +2,11 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS } from 'lib/constants'; import { getPageviewMetrics, getSessionMetrics } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import * as yup from 'yup'; export interface WebsiteMetricsRequestQuery { id: string; @@ -26,6 +27,12 @@ export interface WebsiteMetricsRequestQuery { language: string; } +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -33,6 +40,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, type, diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index c5532e76..9985ca89 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -3,7 +3,7 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { getPageviewStats, getSessionStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; @@ -24,6 +24,13 @@ export interface WebsitePageviewRequestQuery { city?: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -31,6 +38,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, timezone, diff --git a/pages/api/websites/[id]/reports.ts b/pages/api/websites/[id]/reports.ts index 60c6f714..738f6b37 100644 --- a/pages/api/websites/[id]/reports.ts +++ b/pages/api/websites/[id]/reports.ts @@ -1,5 +1,5 @@ import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -9,6 +9,13 @@ export interface ReportsRequestQuery extends SearchFilter, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index 23b5305d..cfd5e767 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -1,6 +1,6 @@ import { NextApiRequestQueryBody } from 'lib/types'; import { canUpdateWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { resetWebsite } from 'queries'; @@ -9,6 +9,13 @@ export interface WebsiteResetRequestQuery { id: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -16,6 +23,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId } = req.query; if (req.method === 'POST') { diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index a77c7eaf..caf54910 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -2,7 +2,7 @@ import { subMinutes, differenceInMinutes } from 'date-fns'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; import { parseDateRangeQuery } from 'lib/query'; import { getWebsiteStats } from 'queries'; @@ -24,6 +24,13 @@ export interface WebsiteStatsRequestQuery { city: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -31,6 +38,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, url, diff --git a/pages/api/websites/[id]/values.ts b/pages/api/websites/[id]/values.ts index ad8625bd..d90a1682 100644 --- a/pages/api/websites/[id]/values.ts +++ b/pages/api/websites/[id]/values.ts @@ -1,6 +1,6 @@ import { NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; @@ -10,6 +10,13 @@ export interface WebsiteResetRequestQuery { id: string; } +import * as yup from 'yup'; +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, @@ -17,6 +24,9 @@ export default async ( await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); + const { id: websiteId, type } = req.query; if (req.method === 'GET') { diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index f94fa037..d724f12f 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,11 +1,13 @@ import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; -import { useAuth, useCors } from 'lib/middleware'; +import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; import userWebsites from 'pages/api/users/[id]/websites'; +import * as yup from 'yup'; +import { getFilterValidation } from 'lib/yup'; export interface WebsitesRequestQuery extends SearchFilter {} @@ -15,12 +17,25 @@ export interface WebsitesRequestBody { shareId: string; } +const schema = { + GET: yup.object().shape({ + ...getFilterValidation(/All|Name|Domain/i), + }), + POST: yup.object().shape({ + name: yup.string().max(100).required(), + domain: yup.string().max(500).required(), + shareId: yup.string().max(50), + }), +}; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); const { user: { id: userId }, @@ -30,7 +45,7 @@ export default async ( req.query.id = userId; req.query.pageSize = 100; - return userWebsites(req, res); + return userWebsites(req as any, res); } if (req.method === 'POST') {