diff --git a/src/lib/yup.ts b/src/lib/yup.ts index a9d21028..8b2eceee 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -1,11 +1,10 @@ +import moment from 'moment'; import * as yup from 'yup'; -export function getDateRangeValidation() { - return { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - }; -} +export const DateRangeValidation = { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), +}; // ex: /funnel|insights|retention/i export function getFilterValidation(matchRegex) { @@ -17,3 +16,9 @@ export function getFilterValidation(matchRegex) { orderBy: yup.string(), }; } + +export const TimezoneTest = yup.string().test( + 'timezone', + () => `Invalid timezone`, + value => !moment.tz.zone(value), +); diff --git a/src/pages/api/reports/retention.ts b/src/pages/api/reports/retention.ts index 4006ab12..c7a5e9af 100644 --- a/src/pages/api/reports/retention.ts +++ b/src/pages/api/reports/retention.ts @@ -1,6 +1,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; +import { TimezoneTest } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRetention } from 'queries'; @@ -8,7 +9,7 @@ import * as yup from 'yup'; export interface RetentionRequestBody { websiteId: string; - dateRange: { startDate: string; endDate: string }; + dateRange: { startDate: string; endDate: string; timezone: string }; } const schema = { @@ -19,6 +20,7 @@ const schema = { .shape({ startDate: yup.date().required(), endDate: yup.date().required(), + timezone: TimezoneTest, }) .required(), }), @@ -37,7 +39,7 @@ export default async ( if (req.method === 'POST') { const { websiteId, - dateRange: { startDate, endDate }, + dateRange: { startDate, endDate, timezone }, } = req.body; if (!(await canViewWebsite(req.auth, websiteId))) { @@ -47,6 +49,7 @@ export default async ( const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), + timezone, }); return ok(res, data); diff --git a/src/pages/api/teams/[id]/users/[userId].ts b/src/pages/api/teams/[id]/users/[userId].ts index adb635d5..107aba64 100644 --- a/src/pages/api/teams/[id]/users/[userId].ts +++ b/src/pages/api/teams/[id]/users/[userId].ts @@ -5,6 +5,7 @@ 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; diff --git a/src/pages/api/teams/[id]/users/index.ts b/src/pages/api/teams/[id]/users/index.ts index d0efba25..36e9f320 100644 --- a/src/pages/api/teams/[id]/users/index.ts +++ b/src/pages/api/teams/[id]/users/index.ts @@ -1,24 +1,27 @@ import { canViewTeam } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUsersByTeamId } from 'queries'; - +import * as yup from 'yup'; export interface TeamUserRequestQuery extends SearchFilter { id: string; } -export interface TeamUserRequestBody { - email: string; - roleId: string; -} +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); const { id: teamId } = req.query; diff --git a/src/pages/api/websites/[id]/events.ts b/src/pages/api/websites/[id]/events.ts index 427cb40e..422200f8 100644 --- a/src/pages/api/websites/[id]/events.ts +++ b/src/pages/api/websites/[id]/events.ts @@ -6,6 +6,8 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventMetrics } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import * as yup from 'yup'; +import { TimezoneTest } from 'lib/yup'; const unitTypes = ['year', 'month', 'hour', 'day']; @@ -18,15 +20,13 @@ export interface WebsiteEventsRequestQuery { url: 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(), + timezone: TimezoneTest.required(), url: yup.string(), }), }; diff --git a/src/pages/api/websites/[id]/index.ts b/src/pages/api/websites/[id]/index.ts index 0e5aacce..e7c7e004 100644 --- a/src/pages/api/websites/[id]/index.ts +++ b/src/pages/api/websites/[id]/index.ts @@ -22,6 +22,12 @@ const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + name: yup.string().required(), + domain: yup.string().required(), + shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }), + }), }; export default async ( @@ -55,10 +61,6 @@ export default async ( let website; - if (shareId && !shareId.match(SHARE_ID_REGEX)) { - return serverError(res, 'Invalid share ID.'); - } - try { website = await updateWebsite(websiteId, { name, domain, shareId }); } catch (e: any) { diff --git a/src/pages/api/websites/[id]/metrics.ts b/src/pages/api/websites/[id]/metrics.ts index b8c37339..89f90fc4 100644 --- a/src/pages/api/websites/[id]/metrics.ts +++ b/src/pages/api/websites/[id]/metrics.ts @@ -33,6 +33,18 @@ const schema = { type: yup.string().required(), startAt: yup.number().required(), endAt: yup.number().required(), + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + query: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), + language: yup.string(), + event: yup.string(), }), }; diff --git a/src/pages/api/websites/[id]/pageviews.ts b/src/pages/api/websites/[id]/pageviews.ts index 9985ca89..8c10ffeb 100644 --- a/src/pages/api/websites/[id]/pageviews.ts +++ b/src/pages/api/websites/[id]/pageviews.ts @@ -1,18 +1,17 @@ -import moment from 'moment-timezone'; -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, useValidate } from 'lib/middleware'; -import { getPageviewStats, getSessionStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getPageviewStats, getSessionStats } from 'queries'; export interface WebsitePageviewRequestQuery { id: string; startAt: number; endAt: number; - unit: string; - timezone: string; + unit?: string; + timezone?: string; url?: string; referrer?: string; title?: string; @@ -24,10 +23,24 @@ export interface WebsitePageviewRequestQuery { city?: string; } +import { TimezoneTest } from 'lib/yup'; import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + startAt: yup.number().required(), + endAt: yup.number().required(), + unit: yup.string(), + timezone: TimezoneTest, + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), }), }; @@ -62,10 +75,6 @@ export default async ( const { startDate, endDate, unit } = await parseDateRangeQuery(req); - if (!moment.tz.zone(timezone)) { - return badRequest(res); - } - const filters = { startDate, endDate, diff --git a/src/pages/api/websites/[id]/reports.ts b/src/pages/api/websites/[id]/reports.ts index 2c7707e8..36e97a46 100644 --- a/src/pages/api/websites/[id]/reports.ts +++ b/src/pages/api/websites/[id]/reports.ts @@ -1,6 +1,7 @@ import { canViewWebsite } from 'lib/auth'; 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 { getReportsByWebsiteId } from 'queries'; @@ -13,6 +14,7 @@ import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + ...getFilterValidation(/All|Name|Description|Type|Username|Website Name|Website Domain/i), }), }; diff --git a/src/pages/api/websites/[id]/reset.ts b/src/pages/api/websites/[id]/reset.ts index cfd5e767..b17fdade 100644 --- a/src/pages/api/websites/[id]/reset.ts +++ b/src/pages/api/websites/[id]/reset.ts @@ -4,14 +4,14 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { resetWebsite } from 'queries'; +import * as yup from 'yup'; export interface WebsiteResetRequestQuery { id: string; } -import * as yup from 'yup'; const schema = { - GET: yup.object().shape({ + POST: yup.object().shape({ id: yup.string().uuid().required(), }), }; @@ -22,7 +22,6 @@ export default async ( ) => { await useCors(req, res); await useAuth(req, res); - req.yup = schema; await useValidate(req, res); diff --git a/src/pages/api/websites/[id]/stats.ts b/src/pages/api/websites/[id]/stats.ts index caf54910..e0c71e40 100644 --- a/src/pages/api/websites/[id]/stats.ts +++ b/src/pages/api/websites/[id]/stats.ts @@ -11,23 +11,36 @@ export interface WebsiteStatsRequestQuery { id: string; startAt: number; endAt: number; - url: string; - referrer: string; - title: string; - query: string; - event: string; - os: string; - browser: string; - device: string; - country: string; - region: string; - city: string; + url?: string; + referrer?: string; + title?: string; + query?: string; + event?: string; + os?: string; + browser?: string; + device?: string; + country?: string; + region?: string; + city?: string; } import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + startAt: yup.number().required(), + endAt: yup.number().required(), + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + query: yup.string(), + event: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), }), }; diff --git a/src/queries/analytics/reports/getRetention.ts b/src/queries/analytics/reports/getRetention.ts index 3c384b6e..7526644f 100644 --- a/src/queries/analytics/reports/getRetention.ts +++ b/src/queries/analytics/reports/getRetention.ts @@ -8,7 +8,7 @@ export async function getRetention( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ] ) { @@ -23,7 +23,7 @@ async function relationalQuery( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ): Promise< { @@ -103,7 +103,7 @@ async function clickhouseQuery( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ): Promise< {