From 87cf5d52985e1dbeb0a2b07c86ef443bb4bb2c6d Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Aug 2023 11:10:44 -0700 Subject: [PATCH 01/32] Fix UserWebsites. --- .../pages/settings/users/UserWebsites.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/components/pages/settings/users/UserWebsites.js b/components/pages/settings/users/UserWebsites.js index 144fae44..d61df4d8 100644 --- a/components/pages/settings/users/UserWebsites.js +++ b/components/pages/settings/users/UserWebsites.js @@ -2,12 +2,19 @@ import { Loading } from 'react-basics'; import useApi from 'hooks/useApi'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; export function UserWebsites({ userId }) { const { formatMessage, messages } = useMessages(); + const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = + useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery(['user:websites', userId], () => - get(`/users/${userId}/websites`), + const { data, isLoading } = useQuery(['user:websites', userId, filter, page, pageSize], () => + get(`/users/${userId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; @@ -17,7 +24,15 @@ export function UserWebsites({ userId }) { return (
- {hasData && } + {hasData && ( + + )} {!hasData && formatMessage(messages.noDataAvailable)}
); From 1aa407027ee234711bb60dfbe163ddd3f3fc4699 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Aug 2023 11:16:03 -0700 Subject: [PATCH 02/32] Remove password. --- queries/admin/user.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/queries/admin/user.ts b/queries/admin/user.ts index dfb923f3..ca459b9f 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -37,7 +37,7 @@ export async function getUserByUsername(username: string, options: GetUserOption } export async function getUsers( - UserSearchFilter: UserSearchFilter = {}, + UserSearchFilter: UserSearchFilter, options?: { include?: Prisma.UserInclude }, ): Promise> { const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter; @@ -72,14 +72,22 @@ export async function getUsers( ...UserSearchFilter, }); - const users = await prisma.client.user.findMany({ - where: { - ...where, - deletedAt: null, - }, - ...pageFilters, - ...(options?.include && { include: options.include }), - }); + const users = await prisma.client.user + .findMany({ + where: { + ...where, + deletedAt: null, + }, + ...pageFilters, + ...(options?.include && { include: options.include }), + }) + .then(a => { + return a.map(a => { + const { password, ...rest } = a; + + return rest; + }); + }); const count = await prisma.client.user.count({ where: { ...where, From a296ecb96b321b4c3821d600d30b38632a735de3 Mon Sep 17 00:00:00 2001 From: Yash Khandelwal Date: Sat, 19 Aug 2023 00:29:06 +0530 Subject: [PATCH 03/32] Fixed error when opening user websites in the settings. --- .../pages/settings/users/UserWebsites.js | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/components/pages/settings/users/UserWebsites.js b/components/pages/settings/users/UserWebsites.js index 144fae44..8189ad8d 100644 --- a/components/pages/settings/users/UserWebsites.js +++ b/components/pages/settings/users/UserWebsites.js @@ -1,25 +1,38 @@ -import { Loading } from 'react-basics'; import useApi from 'hooks/useApi'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; import useMessages from 'hooks/useMessages'; +import useApiFilter from 'hooks/useApiFilter'; +import Page from 'components/layout/Page'; +import useConfig from 'hooks/useConfig'; export function UserWebsites({ userId }) { + const { cloudMode } = useConfig(); const { formatMessage, messages } = useMessages(); + const { filter, page, pageSize, handlePageSizeChange, handleFilterChange, handlePageChange } = useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery(['user:websites', userId], () => - get(`/users/${userId}/websites`), + const { data, isLoading, error } = useQuery(['user:websites', userId, filter, page, pageSize], () => + get(`/users/${userId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; - if (isLoading) { - return ; - } - return ( -
- {hasData && } + + {hasData && ( + ) + } {!hasData && formatMessage(messages.noDataAvailable)} -
+ ); } From c213b6414f0943dac8050fbc1db33f2d2c7203c6 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Aug 2023 12:39:31 -0700 Subject: [PATCH 04/32] Default list size. --- lib/prisma.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/prisma.ts b/lib/prisma.ts index 12bafa51..8fa7e8ae 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -185,7 +185,9 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const { pageSize = 10, page = 1, orderBy } = filters; + const pageSize = filters?.pageSize || 10; + const page = filters?.page || 1; + const orderBy = filters?.orderBy; return [ { From 7d5a24044a475717b36931c8f4dd724be3563c5b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 18 Aug 2023 21:52:59 -0700 Subject: [PATCH 05/32] Code cleanup. --- .eslintrc.json | 3 ++- components/common/SettingsTable.js | 6 ++--- components/pages/reports/ReportTemplates.js | 4 ++-- components/pages/reports/ReportsPage.js | 6 ++--- .../pages/settings/teams/TeamWebsitesTable.js | 6 +---- .../pages/settings/users/UserWebsites.js | 22 +++++++++---------- .../pages/settings/websites/WebsitesTable.js | 6 ++--- .../pages/websites/WebsiteReportsPage.js | 6 ++--- lib/prisma.ts | 4 +--- queries/admin/user.ts | 14 +++++------- 10 files changed, 34 insertions(+), 43 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 25e83d5a..f6d90cca 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -51,7 +51,8 @@ "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-empty-interface": "off" + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] }, "globals": { "React": "writable" diff --git a/components/common/SettingsTable.js b/components/common/SettingsTable.js index e9491331..eb7a6411 100644 --- a/components/common/SettingsTable.js +++ b/components/common/SettingsTable.js @@ -1,4 +1,4 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import Empty from 'components/common/Empty'; import useMessages from 'hooks/useMessages'; import { useState } from 'react'; import { @@ -36,7 +36,7 @@ export function SettingsTable({ return ( <> - {showSearch && ( + {showSearch && !!value.length && ( )} {value.length === 0 && filterValue && ( - + )} {value.length > 0 && ( diff --git a/components/pages/reports/ReportTemplates.js b/components/pages/reports/ReportTemplates.js index 0f5e710d..57cb113e 100644 --- a/components/pages/reports/ReportTemplates.js +++ b/components/pages/reports/ReportTemplates.js @@ -30,7 +30,7 @@ function ReportItem({ title, description, url, icon }) { ); } -export function ReportTemplates() { +export function ReportTemplates({ showHeader = true }) { const { formatMessage, labels } = useMessages(); const reports = [ @@ -56,7 +56,7 @@ export function ReportTemplates() { return ( - + {showHeader && }
{reports.map(({ title, description, url, icon }) => { return ( diff --git a/components/pages/reports/ReportsPage.js b/components/pages/reports/ReportsPage.js index 95959832..7ae102b0 100644 --- a/components/pages/reports/ReportsPage.js +++ b/components/pages/reports/ReportsPage.js @@ -7,7 +7,7 @@ import { Button, Icon, Icons, Text } from 'react-basics'; import ReportsTable from './ReportsTable'; export function ReportsPage() { - const { formatMessage, labels, messages } = useMessages(); + const { formatMessage, labels } = useMessages(); const { reports, error, @@ -47,9 +47,7 @@ export function ReportsPage() { showDomain={true} /> )} - {!hasData && ( - - )} + {!hasData && } ); } diff --git a/components/pages/settings/teams/TeamWebsitesTable.js b/components/pages/settings/teams/TeamWebsitesTable.js index 564c8a78..f89a9166 100644 --- a/components/pages/settings/teams/TeamWebsitesTable.js +++ b/components/pages/settings/teams/TeamWebsitesTable.js @@ -55,11 +55,7 @@ export function TeamWebsitesTable({ {canRemove && ( - + )} ); diff --git a/components/pages/settings/users/UserWebsites.js b/components/pages/settings/users/UserWebsites.js index 709f6e4d..df8c9f57 100644 --- a/components/pages/settings/users/UserWebsites.js +++ b/components/pages/settings/users/UserWebsites.js @@ -1,24 +1,25 @@ +import Page from 'components/layout/Page'; import useApi from 'hooks/useApi'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useMessages from 'hooks/useMessages'; import useApiFilter from 'hooks/useApiFilter'; export function UserWebsites({ userId }) { - const { formatMessage, messages } = useMessages(); const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = useApiFilter(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery(['user:websites', userId, filter, page, pageSize], () => - get(`/users/${userId}/websites`, { - filter, - page, - pageSize, - }), + const { data, isLoading, error } = useQuery( + ['user:websites', userId, filter, page, pageSize], + () => + get(`/users/${userId}/websites`, { + filter, + page, + pageSize, + }), ); const hasData = data && data.length !== 0; return ( -
+ {hasData && ( )} - {!hasData && formatMessage(messages.noDataAvailable)} -
+ ); } diff --git a/components/pages/settings/websites/WebsitesTable.js b/components/pages/settings/websites/WebsitesTable.js index 08c906aa..89898c81 100644 --- a/components/pages/settings/websites/WebsitesTable.js +++ b/components/pages/settings/websites/WebsitesTable.js @@ -1,7 +1,7 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import Link from 'next/link'; import { Button, Text, Icon, Icons } from 'react-basics'; import SettingsTable from 'components/common/SettingsTable'; +import Empty from 'components/common/Empty'; import useMessages from 'hooks/useMessages'; import useConfig from 'hooks/useConfig'; import useUser from 'hooks/useUser'; @@ -15,7 +15,7 @@ export function WebsitesTable({ showTeam, showEditButton, }) { - const { formatMessage, labels, messages } = useMessages(); + const { formatMessage, labels } = useMessages(); const { openExternal } = useConfig(); const { user } = useUser(); @@ -82,7 +82,7 @@ export function WebsitesTable({ }} )} - {!showTable && } + {!showTable && } ); } diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js index b04c50d1..be4ee800 100644 --- a/components/pages/websites/WebsiteReportsPage.js +++ b/components/pages/websites/WebsiteReportsPage.js @@ -1,5 +1,5 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import Page from 'components/layout/Page'; +import Empty from 'components/common/Empty'; import ReportsTable from 'components/pages/reports/ReportsTable'; import { useMessages, useWebsiteReports } from 'hooks'; import Link from 'next/link'; @@ -7,7 +7,7 @@ import { Button, Flexbox, Icon, Icons, Text } from 'react-basics'; import WebsiteHeader from './WebsiteHeader'; export function WebsiteReportsPage({ websiteId }) { - const { formatMessage, labels, messages } = useMessages(); + const { formatMessage, labels } = useMessages(); const { reports, error, @@ -48,7 +48,7 @@ export function WebsiteReportsPage({ websiteId }) { filterValue={filter} /> )} - {!hasData && } + {!hasData && } ); } diff --git a/lib/prisma.ts b/lib/prisma.ts index 8fa7e8ae..a9832c28 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -185,9 +185,7 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const pageSize = filters?.pageSize || 10; - const page = filters?.page || 1; - const orderBy = filters?.orderBy; + const { pageSize = 10, page = 1, orderBy } = filters || {}; return [ { diff --git a/queries/admin/user.ts b/queries/admin/user.ts index ca459b9f..dfe8ea28 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -37,10 +37,10 @@ export async function getUserByUsername(username: string, options: GetUserOption } export async function getUsers( - UserSearchFilter: UserSearchFilter, + searchFilter: UserSearchFilter, options?: { include?: Prisma.UserInclude }, ): Promise> { - const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter; + const { teamId, filter, filterType = USER_FILTER_TYPES.all } = searchFilter; const mode = prisma.getSearchMode(); const where: Prisma.UserWhereInput = { @@ -67,9 +67,10 @@ export async function getUsers( }, }), }; + const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'username', - ...UserSearchFilter, + ...searchFilter, }); const users = await prisma.client.user @@ -82,12 +83,9 @@ export async function getUsers( ...(options?.include && { include: options.include }), }) .then(a => { - return a.map(a => { - const { password, ...rest } = a; - - return rest; - }); + return a.map(({ password, ...rest }) => rest); }); + const count = await prisma.client.user.count({ where: { ...where, From 7a7233ead4630d12aea291ed44a4caeffaa7b00f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sat, 19 Aug 2023 22:23:15 -0700 Subject: [PATCH 06/32] 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') { From ede658771e952ae03a3479d3bb0aed4916cae64c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 21 Aug 2023 02:06:09 -0700 Subject: [PATCH 07/32] Moved code into src folder. Added build for component library. --- .eslintrc.json | 17 +- jsconfig.json | 4 +- package.components.json | 10 + package.json | 25 +- rollup.components.config.mjs | 99 +++++ ...ker.config.js => rollup.tracker.config.mjs | 2 +- scripts/check-lang.js | 2 +- {assets => src/assets}/add-user.svg | 0 {assets => src/assets}/bar-chart.svg | 0 {assets => src/assets}/bars.svg | 0 {assets => src/assets}/bolt.svg | 0 {assets => src/assets}/calendar.svg | 0 {assets => src/assets}/clock.svg | 0 {assets => src/assets}/dashboard.svg | 0 {assets => src/assets}/expand.svg | 0 {assets => src/assets}/eye.svg | 0 {assets => src/assets}/funnel.svg | 0 {assets => src/assets}/gear.svg | 0 {assets => src/assets}/globe.svg | 0 {assets => src/assets}/lightbulb.svg | 0 {assets => src/assets}/link.svg | 0 {assets => src/assets}/lock.svg | 0 {assets => src/assets}/logo.svg | 0 {assets => src/assets}/magnet.svg | 0 {assets => src/assets}/moon.svg | 0 {assets => src/assets}/nodes.svg | 0 {assets => src/assets}/overview.svg | 0 {assets => src/assets}/profile.svg | 0 {assets => src/assets}/redo.svg | 0 {assets => src/assets}/reports.svg | 0 {assets => src/assets}/sun.svg | 0 {assets => src/assets}/user.svg | 0 {assets => src/assets}/users.svg | 0 {assets => src/assets}/visitor.svg | 0 {assets => src/assets}/website.svg | 0 .../components}/common/ConfirmDeleteForm.js | 2 +- .../components}/common/Empty.js | 2 +- .../components}/common/Empty.module.css | 0 .../components}/common/EmptyPlaceholder.js | 0 .../components}/common/ErrorBoundary.js | 2 +- .../common/ErrorBoundry.module.css | 0 .../components}/common/ErrorMessage.js | 2 +- .../common/ErrorMessage.module.css | 0 .../components}/common/Favicon.js | 0 .../components}/common/Favicon.module.css | 0 .../components}/common/FilterButtons.js | 0 .../components}/common/FilterLink.js | 4 +- .../components}/common/FilterLink.module.css | 0 .../components}/common/HamburgerButton.js | 4 +- .../common/HamburgerButton.module.css | 0 .../components}/common/HoverTooltip.js | 0 .../common/HoverTooltip.module.css | 0 .../components}/common/LinkButton.js | 4 +- .../components}/common/LinkButton.module.css | 0 .../components}/common/MobileMenu.js | 0 .../components}/common/MobileMenu.module.css | 0 .../components}/common/Pager.js | 2 +- .../components}/common/Pager.module.css | 0 .../components}/common/SettingsTable.js | 2 +- .../common/SettingsTable.module.css | 0 .../components}/common/UpdateNotice.js | 2 +- .../common/UpdateNotice.module.css | 0 .../components}/common/WorldMap.js | 6 +- .../components}/common/WorldMap.module.css | 0 .../components}/declarations.d.ts | 0 {hooks => src/components/hooks}/index.js | 0 {hooks => src/components/hooks}/useApi.ts | 0 .../components/hooks}/useApiFilter.ts | 0 {hooks => src/components/hooks}/useConfig.js | 2 +- .../components/hooks}/useCountryNames.js | 0 .../components/hooks}/useDateRange.js | 0 .../components/hooks}/useDocumentClick.js | 0 .../components/hooks}/useEscapeKey.js | 0 {hooks => src/components/hooks}/useFilters.js | 2 +- .../components/hooks}/useForceUpdate.js | 0 {hooks => src/components/hooks}/useFormat.js | 0 .../components/hooks}/useLanguageNames.js | 0 {hooks => src/components/hooks}/useLocale.js | 2 +- .../components/hooks}/useMessages.js | 0 .../components/hooks}/usePageQuery.js | 0 {hooks => src/components/hooks}/useReport.js | 0 {hooks => src/components/hooks}/useReports.js | 2 +- .../components/hooks}/useRequireLogin.js | 4 +- .../components/hooks}/useShareToken.js | 0 {hooks => src/components/hooks}/useSticky.js | 0 {hooks => src/components/hooks}/useTheme.js | 0 .../components/hooks}/useTimezone.js | 0 {hooks => src/components/hooks}/useUser.js | 0 {hooks => src/components/hooks}/useWebsite.js | 0 .../components/hooks}/useWebsiteReports.js | 2 +- {components => src/components}/icons.ts | 2 +- .../components}/input/DateFilter.js | 4 +- .../components}/input/LanguageButton.js | 2 +- .../input/LanguageButton.module.css | 0 .../components}/input/LogoutButton.js | 4 +- .../components}/input/MonthSelect.js | 2 +- .../components}/input/MonthSelect.module.css | 0 .../components}/input/ProfileButton.js | 8 +- .../input/ProfileButton.module.css | 0 .../components}/input/RefreshButton.js | 4 +- .../components}/input/SettingsButton.js | 2 +- .../input/SettingsButton.module.css | 0 .../components}/input/ThemeButton.js | 2 +- .../components}/input/ThemeButton.module.css | 0 .../components}/input/WebsiteDateFilter.js | 6 +- .../input/WebsiteDateFilter.module.css | 0 .../components}/input/WebsiteSelect.js | 4 +- .../components}/layout/AppLayout.js | 2 +- .../components}/layout/AppLayout.module.css | 0 .../components}/layout/Footer.js | 0 .../components}/layout/Footer.module.css | 0 {components => src/components}/layout/Grid.js | 0 .../components}/layout/Grid.module.css | 0 .../components}/layout/Header.js | 0 .../components}/layout/Header.module.css | 0 .../components}/layout/NavBar.js | 4 +- .../components}/layout/NavBar.module.css | 0 .../components}/layout/NavGroup.js | 0 .../components}/layout/NavGroup.module.css | 0 {components => src/components}/layout/Page.js | 2 +- .../components}/layout/Page.module.css | 0 .../components}/layout/PageHeader.js | 0 .../components}/layout/PageHeader.module.css | 0 .../components}/layout/ReportsLayout.js | 4 +- .../layout/ReportsLayout.module.css | 0 .../components}/layout/SettingsLayout.js | 6 +- .../layout/SettingsLayout.module.css | 0 .../components}/layout/ShareLayout.js | 0 .../components}/layout/SideNav.js | 0 .../components}/layout/SideNav.module.css | 0 {components => src/components}/messages.js | 0 .../components}/metrics/ActiveUsers.js | 4 +- .../metrics/ActiveUsers.module.css | 0 .../components}/metrics/BarChart.js | 4 +- .../components}/metrics/BarChart.module.css | 0 .../components}/metrics/BrowsersTable.js | 4 +- .../components}/metrics/CitiesTable.js | 4 +- .../components}/metrics/CountriesTable.js | 4 +- .../components}/metrics/DataTable.js | 2 +- .../components}/metrics/DataTable.module.css | 0 .../components}/metrics/DatePickerForm.js | 4 +- .../metrics/DatePickerForm.module.css | 0 .../components}/metrics/DevicesTable.js | 4 +- .../components}/metrics/EventsChart.js | 2 +- .../metrics/EventsChart.module.css | 0 .../components}/metrics/EventsTable.js | 2 +- .../components}/metrics/FilterTags.js | 4 +- .../components}/metrics/FilterTags.module.css | 0 .../components}/metrics/LanguagesTable.js | 6 +- .../components}/metrics/Legend.js | 6 +- .../components}/metrics/Legend.module.css | 0 .../components}/metrics/MetricCard.js | 0 .../components}/metrics/MetricCard.module.css | 0 .../components}/metrics/MetricsBar.js | 0 .../components}/metrics/MetricsBar.module.css | 0 .../components}/metrics/MetricsTable.js | 14 +- .../metrics/MetricsTable.module.css | 0 .../components}/metrics/OSTable.js | 2 +- .../components}/metrics/PagesTable.js | 4 +- .../components}/metrics/PageviewsChart.js | 4 +- .../metrics/QueryParametersTable.js | 2 +- .../metrics/QueryParametersTable.module.css | 0 .../components}/metrics/RealtimeChart.js | 2 +- .../components}/metrics/ReferrersTable.js | 2 +- .../components}/metrics/RegionsTable.js | 6 +- .../components}/metrics/ScreenTable.js | 2 +- .../components}/pages/console/TestConsole.js | 2 +- .../pages/console/TestConsole.module.css | 0 .../components}/pages/dashboard/Dashboard.js | 6 +- .../pages/dashboard/DashboardEdit.js | 2 +- .../pages/dashboard/DashboardEdit.module.css | 0 .../dashboard/DashboardSettingsButton.js | 4 +- .../DashboardSettingsButton.module.css | 0 .../pages/event-data/EventDataMetricsBar.js | 4 +- .../event-data/EventDataMetricsBar.module.css | 0 .../pages/event-data/EventDataTable.js | 2 +- .../pages/event-data/EventDataValueTable.js | 2 +- .../components}/pages/login/LoginForm.js | 4 +- .../pages/login/LoginForm.module.css | 0 .../components}/pages/login/LoginLayout.js | 2 +- .../pages/login/LoginLayout.module.css | 0 .../pages/realtime/RealtimeCountries.js | 6 +- .../realtime/RealtimeCountries.module.css | 0 .../pages/realtime/RealtimeHeader.js | 2 +- .../pages/realtime/RealtimeHeader.module.css | 0 .../pages/realtime/RealtimeHome.js | 4 +- .../components}/pages/realtime/RealtimeLog.js | 6 +- .../pages/realtime/RealtimeLog.module.css | 0 .../pages/realtime/RealtimePage.js | 4 +- .../pages/realtime/RealtimePage.module.css | 0 .../pages/realtime/RealtimeUrls.js | 2 +- .../pages/reports/BaseParameters.js | 2 +- .../components}/pages/reports/FieldAddForm.js | 0 .../pages/reports/FieldAddForm.module.css | 0 .../pages/reports/FieldAggregateForm.js | 2 +- .../pages/reports/FieldFilterForm.js | 2 +- .../pages/reports/FieldFilterForm.module.css | 0 .../pages/reports/FieldSelectForm.js | 2 +- .../pages/reports/FieldSelectForm.module.css | 0 .../pages/reports/FilterSelectForm.js | 2 +- .../pages/reports/ParameterList.js | 2 +- .../pages/reports/ParameterList.module.css | 0 .../components}/pages/reports/PopupForm.js | 0 .../pages/reports/PopupForm.module.css | 0 .../components}/pages/reports/Report.js | 2 +- .../components}/pages/reports/ReportBody.js | 0 .../pages/reports/ReportDetails.js | 0 .../components}/pages/reports/ReportHeader.js | 2 +- .../pages/reports/ReportHeader.module.css | 0 .../components}/pages/reports/ReportMenu.js | 0 .../pages/reports/ReportTemplates.js | 2 +- .../pages/reports/ReportTemplates.module.css | 0 .../components}/pages/reports/ReportsPage.js | 2 +- .../components}/pages/reports/ReportsTable.js | 4 +- .../reports/event-data/EventDataParameters.js | 2 +- .../event-data/EventDataParameters.module.css | 0 .../reports/event-data/EventDataReport.js | 0 .../reports/event-data/EventDataTable.js | 2 +- .../pages/reports/funnel/FunnelChart.js | 6 +- .../reports/funnel/FunnelChart.module.css | 0 .../pages/reports/funnel/FunnelParameters.js | 2 +- .../pages/reports/funnel/FunnelReport.js | 0 .../reports/funnel/FunnelReport.module.css | 0 .../pages/reports/funnel/FunnelTable.js | 2 +- .../pages/reports/funnel/UrlAddForm.js | 2 +- .../reports/funnel/UrlAddForm.module.css | 0 .../reports/insights/InsightsParameters.js | 2 +- .../insights/InsightsParameters.module.css | 0 .../pages/reports/insights/InsightsReport.js | 0 .../pages/reports/insights/InsightsTable.js | 2 +- .../pages/reports/reports.module.css | 0 .../reports/retention/RetentionParameters.js | 2 +- .../reports/retention/RetentionReport.js | 0 .../retention/RetentionReport.module.css | 0 .../pages/reports/retention/RetentionTable.js | 2 +- .../retention/RetentionTable.module.css | 0 .../settings/profile/DateRangeSetting.js | 4 +- .../pages/settings/profile/LanguageSetting.js | 4 +- .../settings/profile/PasswordChangeButton.js | 2 +- .../settings/profile/PasswordEditForm.js | 4 +- .../pages/settings/profile/ProfileDetails.js | 6 +- .../pages/settings/profile/ProfileSettings.js | 2 +- .../pages/settings/profile/ThemeSetting.js | 2 +- .../settings/profile/ThemeSetting.module.css | 0 .../pages/settings/profile/TimezoneSetting.js | 4 +- .../pages/settings/teams/TeamAddForm.js | 4 +- .../settings/teams/TeamAddWebsiteForm.js | 4 +- .../pages/settings/teams/TeamDeleteForm.js | 4 +- .../pages/settings/teams/TeamEditForm.js | 4 +- .../pages/settings/teams/TeamJoinForm.js | 4 +- .../pages/settings/teams/TeamLeaveForm.js | 4 +- .../settings/teams/TeamMemberRemoveButton.js | 4 +- .../pages/settings/teams/TeamMembers.js | 6 +- .../pages/settings/teams/TeamMembersTable.js | 4 +- .../pages/settings/teams/TeamSettings.js | 6 +- .../settings/teams/TeamWebsiteRemoveButton.js | 4 +- .../pages/settings/teams/TeamWebsites.js | 6 +- .../pages/settings/teams/TeamWebsitesTable.js | 6 +- .../pages/settings/teams/TeamsList.js | 8 +- .../pages/settings/teams/TeamsTable.js | 6 +- .../pages/settings/teams/WebsiteTags.js | 0 .../settings/teams/WebsiteTags.module.css | 0 .../pages/settings/users/UserAddButton.js | 2 +- .../pages/settings/users/UserAddForm.js | 4 +- .../pages/settings/users/UserDeleteForm.js | 4 +- .../pages/settings/users/UserEditForm.js | 4 +- .../pages/settings/users/UserSettings.js | 6 +- .../pages/settings/users/UserWebsites.js | 4 +- .../pages/settings/users/UsersList.js | 8 +- .../pages/settings/users/UsersTable.js | 8 +- .../pages/settings/websites/ShareUrl.js | 6 +- .../pages/settings/websites/TrackingCode.js | 4 +- .../pages/settings/websites/WebsiteAddForm.js | 4 +- .../pages/settings/websites/WebsiteData.js | 2 +- .../settings/websites/WebsiteDeleteForm.js | 4 +- .../settings/websites/WebsiteEditForm.js | 4 +- .../settings/websites/WebsiteResetForm.js | 4 +- .../settings/websites/WebsiteSettings.js | 6 +- .../pages/settings/websites/WebsitesList.js | 8 +- .../pages/settings/websites/WebsitesTable.js | 6 +- .../websites/WebsitesTable.module.css | 0 .../pages/websites/WebsiteChart.js | 2 +- .../pages/websites/WebsiteChart.module.css | 0 .../pages/websites/WebsiteChartList.js | 2 +- .../pages/websites/WebsiteDetailsPage.js | 4 +- .../pages/websites/WebsiteEventData.js | 2 +- .../websites/WebsiteEventData.module.css | 0 .../pages/websites/WebsiteEventDataPage.js | 0 .../pages/websites/WebsiteHeader.js | 4 +- .../pages/websites/WebsiteHeader.module.css | 0 .../pages/websites/WebsiteList.module.css | 0 .../pages/websites/WebsiteMenuView.js | 6 +- .../pages/websites/WebsiteMenuView.module.css | 0 .../pages/websites/WebsiteMetricsBar.js | 2 +- .../websites/WebsiteMetricsBar.module.css | 0 .../pages/websites/WebsiteReportsPage.js | 2 +- .../pages/websites/WebsiteTableView.js | 0 .../websites/WebsiteTableView.module.css | 0 .../pages/websites/WebsitesPage.js | 6 +- src/index.ts | 61 +++ {lang => src/lang}/am-ET.json | 0 {lang => src/lang}/ar-SA.json | 0 {lang => src/lang}/be-BY.json | 0 {lang => src/lang}/bn-BD.json | 0 {lang => src/lang}/ca-ES.json | 0 {lang => src/lang}/cs-CZ.json | 0 {lang => src/lang}/da-DK.json | 0 {lang => src/lang}/de-CH.json | 0 {lang => src/lang}/de-DE.json | 0 {lang => src/lang}/el-GR.json | 0 {lang => src/lang}/en-GB.json | 0 {lang => src/lang}/en-US.json | 0 {lang => src/lang}/es-ES.json | 0 {lang => src/lang}/es-MX.json | 0 {lang => src/lang}/fa-IR.json | 0 {lang => src/lang}/fi-FI.json | 0 {lang => src/lang}/fo-FO.json | 0 {lang => src/lang}/fr-FR.json | 0 {lang => src/lang}/ga-ES.json | 0 {lang => src/lang}/he-IL.json | 0 {lang => src/lang}/hi-IN.json | 0 {lang => src/lang}/hr-HR.json | 0 {lang => src/lang}/hu-HU.json | 0 {lang => src/lang}/id-ID.json | 0 {lang => src/lang}/it-IT.json | 0 {lang => src/lang}/ja-JP.json | 0 {lang => src/lang}/km-KH.json | 0 {lang => src/lang}/ko-KR.json | 0 {lang => src/lang}/lt-LT.json | 0 {lang => src/lang}/mn-MN.json | 0 {lang => src/lang}/ms-MY.json | 0 {lang => src/lang}/my-MM.json | 0 {lang => src/lang}/nb-NO.json | 0 {lang => src/lang}/nl-NL.json | 0 {lang => src/lang}/pl-PL.json | 0 {lang => src/lang}/pt-BR.json | 0 {lang => src/lang}/pt-PT.json | 0 {lang => src/lang}/ro-RO.json | 0 {lang => src/lang}/ru-RU.json | 0 {lang => src/lang}/si-LK.json | 0 {lang => src/lang}/sk-SK.json | 0 {lang => src/lang}/sl-SI.json | 0 {lang => src/lang}/sv-SE.json | 0 {lang => src/lang}/ta-IN.json | 0 {lang => src/lang}/th-TH.json | 0 {lang => src/lang}/tr-TR.json | 0 {lang => src/lang}/uk-UA.json | 0 {lang => src/lang}/ur-PK.json | 0 {lang => src/lang}/vi-VN.json | 0 {lang => src/lang}/zh-CN.json | 0 {lang => src/lang}/zh-TW.json | 0 {lib => src/lib}/auth.ts | 0 {lib => src/lib}/cache.ts | 0 {lib => src/lib}/charts.js | 0 {lib => src/lib}/clickhouse.ts | 0 {lib => src/lib}/client.ts | 0 {lib => src/lib}/constants.ts | 0 {lib => src/lib}/crypto.js | 0 {lib => src/lib}/data.ts | 0 {lib => src/lib}/date.js | 0 {lib => src/lib}/db.js | 0 {lib => src/lib}/detect.ts | 0 {lib => src/lib}/filters.js | 0 {lib => src/lib}/format.js | 0 {lib => src/lib}/kafka.ts | 0 {lib => src/lib}/lang.js | 0 {lib => src/lib}/load.ts | 0 {lib => src/lib}/middleware.ts | 0 {lib => src/lib}/prisma.ts | 0 {lib => src/lib}/query.ts | 0 {lib => src/lib}/session.ts | 0 {lib => src/lib}/sql.ts | 0 {lib => src/lib}/types.ts | 0 {lib => src/lib}/yup.ts | 0 {pages => src/pages}/404.js | 2 +- {pages => src/pages}/_app.js | 4 +- {pages => src/pages}/api/auth/login.ts | 0 {pages => src/pages}/api/auth/logout.ts | 0 {pages => src/pages}/api/auth/sso.ts | 0 {pages => src/pages}/api/auth/verify.ts | 0 {pages => src/pages}/api/config.ts | 0 {pages => src/pages}/api/event-data/events.ts | 0 {pages => src/pages}/api/event-data/fields.ts | 0 {pages => src/pages}/api/event-data/stats.ts | 0 {pages => src/pages}/api/heartbeat.ts | 0 {pages => src/pages}/api/me/index.ts | 0 {pages => src/pages}/api/me/password.ts | 0 {pages => src/pages}/api/me/teams.ts | 0 {pages => src/pages}/api/me/websites.ts | 0 {pages => src/pages}/api/realtime/[id].ts | 0 {pages => src/pages}/api/reports/[id].ts | 0 {pages => src/pages}/api/reports/funnel.ts | 0 {pages => src/pages}/api/reports/index.ts | 0 {pages => src/pages}/api/reports/insights.ts | 0 {pages => src/pages}/api/reports/retention.ts | 0 {pages => src/pages}/api/scripts/telemetry.js | 0 {pages => src/pages}/api/send.ts | 0 {pages => src/pages}/api/share/[id].ts | 0 {pages => src/pages}/api/teams/[id]/index.ts | 0 .../pages}/api/teams/[id]/users/[userId].ts | 0 .../pages}/api/teams/[id]/users/index.ts | 0 .../api/teams/[id]/websites/[websiteId].ts | 0 .../pages}/api/teams/[id]/websites/index.ts | 0 {pages => src/pages}/api/teams/index.ts | 0 {pages => src/pages}/api/teams/join.ts | 0 {pages => src/pages}/api/users/[id]/index.ts | 0 {pages => src/pages}/api/users/[id]/teams.ts | 0 {pages => src/pages}/api/users/[id]/usage.ts | 0 .../pages}/api/users/[id]/websites.ts | 0 {pages => src/pages}/api/users/index.ts | 0 .../pages}/api/websites/[id]/active.ts | 0 .../pages}/api/websites/[id]/daterange.ts | 0 .../pages}/api/websites/[id]/events.ts | 0 .../pages}/api/websites/[id]/index.ts | 0 .../pages}/api/websites/[id]/metrics.ts | 0 .../pages}/api/websites/[id]/pageviews.ts | 0 .../pages}/api/websites/[id]/reports.ts | 0 .../pages}/api/websites/[id]/reset.ts | 0 .../pages}/api/websites/[id]/stats.ts | 0 .../pages}/api/websites/[id]/values.ts | 0 {pages => src/pages}/api/websites/index.ts | 0 {pages => src/pages}/console/[[...id]].js | 0 {pages => src/pages}/dashboard/index.js | 2 +- {pages => src/pages}/index.js | 0 {pages => src/pages}/login.js | 0 {pages => src/pages}/logout.js | 2 +- {pages => src/pages}/reports/[id].js | 2 +- {pages => src/pages}/reports/create.js | 2 +- {pages => src/pages}/reports/funnel.js | 2 +- {pages => src/pages}/reports/index.js | 2 +- {pages => src/pages}/reports/insights.js | 2 +- {pages => src/pages}/reports/retention.js | 2 +- .../pages}/settings/profile/index.js | 2 +- {pages => src/pages}/settings/teams/[id].js | 2 +- {pages => src/pages}/settings/teams/index.js | 2 +- {pages => src/pages}/settings/users/[id].js | 2 +- {pages => src/pages}/settings/users/index.js | 2 +- .../pages}/settings/websites/[id].js | 2 +- .../pages}/settings/websites/index.js | 2 +- {pages => src/pages}/share/[...id].js | 2 +- {pages => src/pages}/sso.js | 0 .../pages}/websites/[id]/event-data.js | 2 +- {pages => src/pages}/websites/[id]/index.js | 2 +- .../pages}/websites/[id]/realtime.js | 0 {pages => src/pages}/websites/[id]/reports.js | 0 {pages => src/pages}/websites/index.js | 2 +- {queries => src/queries}/admin/report.ts | 0 {queries => src/queries}/admin/team.ts | 0 {queries => src/queries}/admin/teamUser.ts | 0 {queries => src/queries}/admin/teamWebsite.ts | 0 {queries => src/queries}/admin/user.ts | 0 {queries => src/queries}/admin/website.ts | 0 .../analytics/eventData/getEventDataEvents.ts | 0 .../analytics/eventData/getEventDataFields.ts | 0 .../analytics/eventData/getEventDataStats.ts | 0 .../analytics/eventData/getEventDataUsage.ts | 0 .../analytics/eventData/saveEventData.ts | 0 .../analytics/events/getEventMetrics.ts | 0 .../analytics/events/getEventUsage.ts | 0 .../queries}/analytics/events/getEvents.ts | 0 .../queries}/analytics/events/saveEvent.ts | 0 .../queries}/analytics/getActiveVisitors.ts | 0 .../queries}/analytics/getRealtimeData.ts | 0 .../queries}/analytics/getValues.ts | 0 .../queries}/analytics/getWebsiteDateRange.ts | 0 .../queries}/analytics/getWebsiteStats.ts | 0 .../analytics/pageviews/getPageviewMetrics.ts | 0 .../analytics/pageviews/getPageviewStats.ts | 0 .../queries}/analytics/reports/getFunnel.ts | 0 .../queries}/analytics/reports/getInsights.ts | 0 .../analytics/reports/getRetention.ts | 0 .../analytics/sessions/createSession.ts | 0 .../queries}/analytics/sessions/getSession.ts | 0 .../analytics/sessions/getSessionMetrics.ts | 0 .../analytics/sessions/getSessionStats.ts | 0 .../analytics/sessions/getSessions.ts | 0 .../analytics/sessions/saveSessionData.ts | 0 {queries => src/queries}/index.js | 0 {store => src/store}/app.js | 0 {store => src/store}/dashboard.js | 0 {store => src/store}/queries.js | 0 {store => src/store}/version.js | 0 {store => src/store}/websites.ts | 0 {styles => src/styles}/index.css | 0 {styles => src/styles}/locale.css | 0 {styles => src/styles}/variables.css | 0 {tracker => src/tracker}/index.d.ts | 0 {tracker => src/tracker}/index.js | 0 tsconfig.json | 6 +- yarn.lock | 409 ++++++++++++------ 490 files changed, 749 insertions(+), 442 deletions(-) create mode 100644 package.components.json create mode 100644 rollup.components.config.mjs rename rollup.tracker.config.js => rollup.tracker.config.mjs (93%) rename {assets => src/assets}/add-user.svg (100%) rename {assets => src/assets}/bar-chart.svg (100%) rename {assets => src/assets}/bars.svg (100%) rename {assets => src/assets}/bolt.svg (100%) rename {assets => src/assets}/calendar.svg (100%) rename {assets => src/assets}/clock.svg (100%) rename {assets => src/assets}/dashboard.svg (100%) rename {assets => src/assets}/expand.svg (100%) rename {assets => src/assets}/eye.svg (100%) rename {assets => src/assets}/funnel.svg (100%) rename {assets => src/assets}/gear.svg (100%) rename {assets => src/assets}/globe.svg (100%) rename {assets => src/assets}/lightbulb.svg (100%) rename {assets => src/assets}/link.svg (100%) rename {assets => src/assets}/lock.svg (100%) rename {assets => src/assets}/logo.svg (100%) rename {assets => src/assets}/magnet.svg (100%) rename {assets => src/assets}/moon.svg (100%) rename {assets => src/assets}/nodes.svg (100%) rename {assets => src/assets}/overview.svg (100%) rename {assets => src/assets}/profile.svg (100%) rename {assets => src/assets}/redo.svg (100%) rename {assets => src/assets}/reports.svg (100%) rename {assets => src/assets}/sun.svg (100%) rename {assets => src/assets}/user.svg (100%) rename {assets => src/assets}/users.svg (100%) rename {assets => src/assets}/visitor.svg (100%) rename {assets => src/assets}/website.svg (100%) rename {components => src/components}/common/ConfirmDeleteForm.js (93%) rename {components => src/components}/common/Empty.js (86%) rename {components => src/components}/common/Empty.module.css (100%) rename {components => src/components}/common/EmptyPlaceholder.js (100%) rename {components => src/components}/common/ErrorBoundary.js (93%) rename {components => src/components}/common/ErrorBoundry.module.css (100%) rename {components => src/components}/common/ErrorMessage.js (88%) rename {components => src/components}/common/ErrorMessage.module.css (100%) rename {components => src/components}/common/Favicon.js (100%) rename {components => src/components}/common/Favicon.module.css (100%) rename {components => src/components}/common/FilterButtons.js (100%) rename {components => src/components}/common/FilterLink.js (91%) rename {components => src/components}/common/FilterLink.module.css (100%) rename {components => src/components}/common/HamburgerButton.js (93%) rename {components => src/components}/common/HamburgerButton.module.css (100%) rename {components => src/components}/common/HoverTooltip.js (100%) rename {components => src/components}/common/HoverTooltip.module.css (100%) rename {components => src/components}/common/LinkButton.js (77%) rename {components => src/components}/common/LinkButton.module.css (100%) rename {components => src/components}/common/MobileMenu.js (100%) rename {components => src/components}/common/MobileMenu.module.css (100%) rename {components => src/components}/common/Pager.js (95%) rename {components => src/components}/common/Pager.module.css (100%) rename {components => src/components}/common/SettingsTable.js (98%) rename {components => src/components}/common/SettingsTable.module.css (100%) rename {components => src/components}/common/UpdateNotice.js (96%) rename {components => src/components}/common/UpdateNotice.module.css (100%) rename {components => src/components}/common/WorldMap.js (94%) rename {components => src/components}/common/WorldMap.module.css (100%) rename {components => src/components}/declarations.d.ts (100%) rename {hooks => src/components/hooks}/index.js (100%) rename {hooks => src/components/hooks}/useApi.ts (100%) rename {hooks => src/components/hooks}/useApiFilter.ts (100%) rename {hooks => src/components/hooks}/useConfig.js (91%) rename {hooks => src/components/hooks}/useCountryNames.js (100%) rename {hooks => src/components/hooks}/useDateRange.js (100%) rename {hooks => src/components/hooks}/useDocumentClick.js (100%) rename {hooks => src/components/hooks}/useEscapeKey.js (100%) rename {hooks => src/components/hooks}/useFilters.js (97%) rename {hooks => src/components/hooks}/useForceUpdate.js (100%) rename {hooks => src/components/hooks}/useFormat.js (100%) rename {hooks => src/components/hooks}/useLanguageNames.js (100%) rename {hooks => src/components/hooks}/useLocale.js (96%) rename {hooks => src/components/hooks}/useMessages.js (100%) rename {hooks => src/components/hooks}/usePageQuery.js (100%) rename {hooks => src/components/hooks}/useReport.js (100%) rename {hooks => src/components/hooks}/useReports.js (94%) rename {hooks => src/components/hooks}/useRequireLogin.js (84%) rename {hooks => src/components/hooks}/useShareToken.js (100%) rename {hooks => src/components/hooks}/useSticky.js (100%) rename {hooks => src/components/hooks}/useTheme.js (100%) rename {hooks => src/components/hooks}/useTimezone.js (100%) rename {hooks => src/components/hooks}/useUser.js (100%) rename {hooks => src/components/hooks}/useWebsite.js (100%) rename {hooks => src/components/hooks}/useWebsiteReports.js (94%) rename {components => src/components}/icons.ts (98%) rename {components => src/components}/input/DateFilter.js (96%) rename {components => src/components}/input/LanguageButton.js (96%) rename {components => src/components}/input/LanguageButton.module.css (100%) rename {components => src/components}/input/LogoutButton.js (84%) rename {components => src/components}/input/MonthSelect.js (97%) rename {components => src/components}/input/MonthSelect.module.css (100%) rename {components => src/components}/input/ProfileButton.js (88%) rename {components => src/components}/input/ProfileButton.module.css (100%) rename {components => src/components}/input/RefreshButton.js (86%) rename {components => src/components}/input/SettingsButton.js (94%) rename {components => src/components}/input/SettingsButton.module.css (100%) rename {components => src/components}/input/ThemeButton.js (95%) rename {components => src/components}/input/ThemeButton.module.css (100%) rename {components => src/components}/input/WebsiteDateFilter.js (76%) rename {components => src/components}/input/WebsiteDateFilter.module.css (100%) rename {components => src/components}/input/WebsiteSelect.js (87%) rename {components => src/components}/layout/AppLayout.js (92%) rename {components => src/components}/layout/AppLayout.module.css (100%) rename {components => src/components}/layout/Footer.js (100%) rename {components => src/components}/layout/Footer.module.css (100%) rename {components => src/components}/layout/Grid.js (100%) rename {components => src/components}/layout/Grid.module.css (100%) rename {components => src/components}/layout/Header.js (100%) rename {components => src/components}/layout/Header.module.css (100%) rename {components => src/components}/layout/NavBar.js (94%) rename {components => src/components}/layout/NavBar.module.css (100%) rename {components => src/components}/layout/NavGroup.js (100%) rename {components => src/components}/layout/NavGroup.module.css (100%) rename {components => src/components}/layout/Page.js (90%) rename {components => src/components}/layout/Page.module.css (100%) rename {components => src/components}/layout/PageHeader.js (100%) rename {components => src/components}/layout/PageHeader.module.css (100%) rename {components => src/components}/layout/ReportsLayout.js (83%) rename {components => src/components}/layout/ReportsLayout.module.css (100%) rename {components => src/components}/layout/SettingsLayout.js (88%) rename {components => src/components}/layout/SettingsLayout.module.css (100%) rename {components => src/components}/layout/ShareLayout.js (100%) rename {components => src/components}/layout/SideNav.js (100%) rename {components => src/components}/layout/SideNav.module.css (100%) rename {components => src/components}/messages.js (100%) rename {components => src/components}/metrics/ActiveUsers.js (89%) rename {components => src/components}/metrics/ActiveUsers.module.css (100%) rename {components => src/components}/metrics/BarChart.js (97%) rename {components => src/components}/metrics/BarChart.module.css (100%) rename {components => src/components}/metrics/BrowsersTable.js (89%) rename {components => src/components}/metrics/CitiesTable.js (86%) rename {components => src/components}/metrics/CountriesTable.js (88%) rename {components => src/components}/metrics/DataTable.js (98%) rename {components => src/components}/metrics/DataTable.module.css (100%) rename {components => src/components}/metrics/DatePickerForm.js (96%) rename {components => src/components}/metrics/DatePickerForm.module.css (100%) rename {components => src/components}/metrics/DevicesTable.js (90%) rename {components => src/components}/metrics/EventsChart.js (98%) rename {components => src/components}/metrics/EventsChart.module.css (100%) rename {components => src/components}/metrics/EventsTable.js (89%) rename {components => src/components}/metrics/FilterTags.js (92%) rename {components => src/components}/metrics/FilterTags.module.css (100%) rename {components => src/components}/metrics/LanguagesTable.js (81%) rename {components => src/components}/metrics/Legend.js (89%) rename {components => src/components}/metrics/Legend.module.css (100%) rename {components => src/components}/metrics/MetricCard.js (100%) rename {components => src/components}/metrics/MetricCard.module.css (100%) rename {components => src/components}/metrics/MetricsBar.js (100%) rename {components => src/components}/metrics/MetricsBar.module.css (100%) rename {components => src/components}/metrics/MetricsTable.js (90%) rename {components => src/components}/metrics/MetricsTable.module.css (100%) rename {components => src/components}/metrics/OSTable.js (93%) rename {components => src/components}/metrics/PagesTable.js (91%) rename {components => src/components}/metrics/PageviewsChart.js (90%) rename {components => src/components}/metrics/QueryParametersTable.js (96%) rename {components => src/components}/metrics/QueryParametersTable.module.css (100%) rename {components => src/components}/metrics/RealtimeChart.js (98%) rename {components => src/components}/metrics/ReferrersTable.js (93%) rename {components => src/components}/metrics/RegionsTable.js (87%) rename {components => src/components}/metrics/ScreenTable.js (87%) rename {components => src/components}/pages/console/TestConsole.js (99%) rename {components => src/components}/pages/console/TestConsole.module.css (100%) rename {components => src/components}/pages/dashboard/Dashboard.js (93%) rename {components => src/components}/pages/dashboard/DashboardEdit.js (98%) rename {components => src/components}/pages/dashboard/DashboardEdit.module.css (100%) rename {components => src/components}/pages/dashboard/DashboardSettingsButton.js (86%) rename {components => src/components}/pages/dashboard/DashboardSettingsButton.module.css (100%) rename {components => src/components}/pages/event-data/EventDataMetricsBar.js (94%) rename {components => src/components}/pages/event-data/EventDataMetricsBar.module.css (100%) rename {components => src/components}/pages/event-data/EventDataTable.js (94%) rename {components => src/components}/pages/event-data/EventDataValueTable.js (96%) rename {components => src/components}/pages/login/LoginForm.js (94%) rename {components => src/components}/pages/login/LoginForm.module.css (100%) rename {components => src/components}/pages/login/LoginLayout.js (86%) rename {components => src/components}/pages/login/LoginLayout.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeCountries.js (84%) rename {components => src/components}/pages/realtime/RealtimeCountries.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeHeader.js (95%) rename {components => src/components}/pages/realtime/RealtimeHeader.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeHome.js (90%) rename {components => src/components}/pages/realtime/RealtimeLog.js (96%) rename {components => src/components}/pages/realtime/RealtimeLog.module.css (100%) rename {components => src/components}/pages/realtime/RealtimePage.js (97%) rename {components => src/components}/pages/realtime/RealtimePage.module.css (100%) rename {components => src/components}/pages/realtime/RealtimeUrls.js (97%) rename {components => src/components}/pages/reports/BaseParameters.js (97%) rename {components => src/components}/pages/reports/FieldAddForm.js (100%) rename {components => src/components}/pages/reports/FieldAddForm.module.css (100%) rename {components => src/components}/pages/reports/FieldAggregateForm.js (96%) rename {components => src/components}/pages/reports/FieldFilterForm.js (96%) rename {components => src/components}/pages/reports/FieldFilterForm.module.css (100%) rename {components => src/components}/pages/reports/FieldSelectForm.js (94%) rename {components => src/components}/pages/reports/FieldSelectForm.module.css (100%) rename {components => src/components}/pages/reports/FilterSelectForm.js (96%) rename {components => src/components}/pages/reports/ParameterList.js (95%) rename {components => src/components}/pages/reports/ParameterList.module.css (100%) rename {components => src/components}/pages/reports/PopupForm.js (100%) rename {components => src/components}/pages/reports/PopupForm.module.css (100%) rename {components => src/components}/pages/reports/Report.js (92%) rename {components => src/components}/pages/reports/ReportBody.js (100%) rename {components => src/components}/pages/reports/ReportDetails.js (100%) rename {components => src/components}/pages/reports/ReportHeader.js (97%) rename {components => src/components}/pages/reports/ReportHeader.module.css (100%) rename {components => src/components}/pages/reports/ReportMenu.js (100%) rename {components => src/components}/pages/reports/ReportTemplates.js (97%) rename {components => src/components}/pages/reports/ReportTemplates.module.css (100%) rename {components => src/components}/pages/reports/ReportsPage.js (96%) rename {components => src/components}/pages/reports/ReportsTable.js (95%) rename {components => src/components}/pages/reports/event-data/EventDataParameters.js (98%) rename {components => src/components}/pages/reports/event-data/EventDataParameters.module.css (100%) rename {components => src/components}/pages/reports/event-data/EventDataReport.js (100%) rename {components => src/components}/pages/reports/event-data/EventDataTable.js (92%) rename {components => src/components}/pages/reports/funnel/FunnelChart.js (92%) rename {components => src/components}/pages/reports/funnel/FunnelChart.module.css (100%) rename {components => src/components}/pages/reports/funnel/FunnelParameters.js (98%) rename {components => src/components}/pages/reports/funnel/FunnelReport.js (100%) rename {components => src/components}/pages/reports/funnel/FunnelReport.module.css (100%) rename {components => src/components}/pages/reports/funnel/FunnelTable.js (90%) rename {components => src/components}/pages/reports/funnel/UrlAddForm.js (95%) rename {components => src/components}/pages/reports/funnel/UrlAddForm.module.css (100%) rename {components => src/components}/pages/reports/insights/InsightsParameters.js (98%) rename {components => src/components}/pages/reports/insights/InsightsParameters.module.css (100%) rename {components => src/components}/pages/reports/insights/InsightsReport.js (100%) rename {components => src/components}/pages/reports/insights/InsightsTable.js (95%) rename {components => src/components}/pages/reports/reports.module.css (100%) rename {components => src/components}/pages/reports/retention/RetentionParameters.js (96%) rename {components => src/components}/pages/reports/retention/RetentionReport.js (100%) rename {components => src/components}/pages/reports/retention/RetentionReport.module.css (100%) rename {components => src/components}/pages/reports/retention/RetentionTable.js (97%) rename {components => src/components}/pages/reports/retention/RetentionTable.module.css (100%) rename {components => src/components}/pages/settings/profile/DateRangeSetting.js (87%) rename {components => src/components}/pages/settings/profile/LanguageSetting.js (89%) rename {components => src/components}/pages/settings/profile/PasswordChangeButton.js (94%) rename {components => src/components}/pages/settings/profile/PasswordEditForm.js (95%) rename {components => src/components}/pages/settings/profile/ProfileDetails.js (91%) rename {components => src/components}/pages/settings/profile/ProfileSettings.js (87%) rename {components => src/components}/pages/settings/profile/ThemeSetting.js (93%) rename {components => src/components}/pages/settings/profile/ThemeSetting.module.css (100%) rename {components => src/components}/pages/settings/profile/TimezoneSetting.js (87%) rename {components => src/components}/pages/settings/teams/TeamAddForm.js (92%) rename {components => src/components}/pages/settings/teams/TeamAddWebsiteForm.js (95%) rename {components => src/components}/pages/settings/teams/TeamDeleteForm.js (90%) rename {components => src/components}/pages/settings/teams/TeamEditForm.js (95%) rename {components => src/components}/pages/settings/teams/TeamJoinForm.js (91%) rename {components => src/components}/pages/settings/teams/TeamLeaveForm.js (90%) rename {components => src/components}/pages/settings/teams/TeamMemberRemoveButton.js (88%) rename {components => src/components}/pages/settings/teams/TeamMembers.js (88%) rename {components => src/components}/pages/settings/teams/TeamMembersTable.js (93%) rename {components => src/components}/pages/settings/teams/TeamSettings.js (93%) rename {components => src/components}/pages/settings/teams/TeamWebsiteRemoveButton.js (87%) rename {components => src/components}/pages/settings/teams/TeamWebsites.js (92%) rename {components => src/components}/pages/settings/teams/TeamWebsitesTable.js (92%) rename {components => src/components}/pages/settings/teams/TeamsList.js (94%) rename {components => src/components}/pages/settings/teams/TeamsTable.js (95%) rename {components => src/components}/pages/settings/teams/WebsiteTags.js (100%) rename {components => src/components}/pages/settings/teams/WebsiteTags.module.css (100%) rename {components => src/components}/pages/settings/users/UserAddButton.js (92%) rename {components => src/components}/pages/settings/users/UserAddForm.js (95%) rename {components => src/components}/pages/settings/users/UserDeleteForm.js (91%) rename {components => src/components}/pages/settings/users/UserEditForm.js (95%) rename {components => src/components}/pages/settings/users/UserSettings.js (91%) rename {components => src/components}/pages/settings/users/UserWebsites.js (90%) rename {components => src/components}/pages/settings/users/UsersList.js (90%) rename {components => src/components}/pages/settings/users/UsersTable.js (93%) rename {components => src/components}/pages/settings/websites/ShareUrl.js (94%) rename {components => src/components}/pages/settings/websites/TrackingCode.js (85%) rename {components => src/components}/pages/settings/websites/WebsiteAddForm.js (93%) rename {components => src/components}/pages/settings/websites/WebsiteData.js (96%) rename {components => src/components}/pages/settings/websites/WebsiteDeleteForm.js (92%) rename {components => src/components}/pages/settings/websites/WebsiteEditForm.js (94%) rename {components => src/components}/pages/settings/websites/WebsiteResetForm.js (92%) rename {components => src/components}/pages/settings/websites/WebsiteSettings.js (95%) rename {components => src/components}/pages/settings/websites/WebsitesList.js (91%) rename {components => src/components}/pages/settings/websites/WebsitesTable.js (94%) rename {components => src/components}/pages/settings/websites/WebsitesTable.module.css (100%) rename {components => src/components}/pages/websites/WebsiteChart.js (98%) rename {components => src/components}/pages/websites/WebsiteChart.module.css (100%) rename {components => src/components}/pages/websites/WebsiteChartList.js (96%) rename {components => src/components}/pages/websites/WebsiteDetailsPage.js (93%) rename {components => src/components}/pages/websites/WebsiteEventData.js (94%) rename {components => src/components}/pages/websites/WebsiteEventData.module.css (100%) rename {components => src/components}/pages/websites/WebsiteEventDataPage.js (100%) rename {components => src/components}/pages/websites/WebsiteHeader.js (94%) rename {components => src/components}/pages/websites/WebsiteHeader.module.css (100%) rename {components => src/components}/pages/websites/WebsiteList.module.css (100%) rename {components => src/components}/pages/websites/WebsiteMenuView.js (96%) rename {components => src/components}/pages/websites/WebsiteMenuView.module.css (100%) rename {components => src/components}/pages/websites/WebsiteMetricsBar.js (99%) rename {components => src/components}/pages/websites/WebsiteMetricsBar.module.css (100%) rename {components => src/components}/pages/websites/WebsiteReportsPage.js (95%) rename {components => src/components}/pages/websites/WebsiteTableView.js (100%) rename {components => src/components}/pages/websites/WebsiteTableView.module.css (100%) rename {components => src/components}/pages/websites/WebsitesPage.js (93%) create mode 100644 src/index.ts rename {lang => src/lang}/am-ET.json (100%) rename {lang => src/lang}/ar-SA.json (100%) rename {lang => src/lang}/be-BY.json (100%) rename {lang => src/lang}/bn-BD.json (100%) rename {lang => src/lang}/ca-ES.json (100%) rename {lang => src/lang}/cs-CZ.json (100%) rename {lang => src/lang}/da-DK.json (100%) rename {lang => src/lang}/de-CH.json (100%) rename {lang => src/lang}/de-DE.json (100%) rename {lang => src/lang}/el-GR.json (100%) rename {lang => src/lang}/en-GB.json (100%) rename {lang => src/lang}/en-US.json (100%) rename {lang => src/lang}/es-ES.json (100%) rename {lang => src/lang}/es-MX.json (100%) rename {lang => src/lang}/fa-IR.json (100%) rename {lang => src/lang}/fi-FI.json (100%) rename {lang => src/lang}/fo-FO.json (100%) rename {lang => src/lang}/fr-FR.json (100%) rename {lang => src/lang}/ga-ES.json (100%) rename {lang => src/lang}/he-IL.json (100%) rename {lang => src/lang}/hi-IN.json (100%) rename {lang => src/lang}/hr-HR.json (100%) rename {lang => src/lang}/hu-HU.json (100%) rename {lang => src/lang}/id-ID.json (100%) rename {lang => src/lang}/it-IT.json (100%) rename {lang => src/lang}/ja-JP.json (100%) rename {lang => src/lang}/km-KH.json (100%) rename {lang => src/lang}/ko-KR.json (100%) rename {lang => src/lang}/lt-LT.json (100%) rename {lang => src/lang}/mn-MN.json (100%) rename {lang => src/lang}/ms-MY.json (100%) rename {lang => src/lang}/my-MM.json (100%) rename {lang => src/lang}/nb-NO.json (100%) rename {lang => src/lang}/nl-NL.json (100%) rename {lang => src/lang}/pl-PL.json (100%) rename {lang => src/lang}/pt-BR.json (100%) rename {lang => src/lang}/pt-PT.json (100%) rename {lang => src/lang}/ro-RO.json (100%) rename {lang => src/lang}/ru-RU.json (100%) rename {lang => src/lang}/si-LK.json (100%) rename {lang => src/lang}/sk-SK.json (100%) rename {lang => src/lang}/sl-SI.json (100%) rename {lang => src/lang}/sv-SE.json (100%) rename {lang => src/lang}/ta-IN.json (100%) rename {lang => src/lang}/th-TH.json (100%) rename {lang => src/lang}/tr-TR.json (100%) rename {lang => src/lang}/uk-UA.json (100%) rename {lang => src/lang}/ur-PK.json (100%) rename {lang => src/lang}/vi-VN.json (100%) rename {lang => src/lang}/zh-CN.json (100%) rename {lang => src/lang}/zh-TW.json (100%) rename {lib => src/lib}/auth.ts (100%) rename {lib => src/lib}/cache.ts (100%) rename {lib => src/lib}/charts.js (100%) rename {lib => src/lib}/clickhouse.ts (100%) rename {lib => src/lib}/client.ts (100%) rename {lib => src/lib}/constants.ts (100%) rename {lib => src/lib}/crypto.js (100%) rename {lib => src/lib}/data.ts (100%) rename {lib => src/lib}/date.js (100%) rename {lib => src/lib}/db.js (100%) rename {lib => src/lib}/detect.ts (100%) rename {lib => src/lib}/filters.js (100%) rename {lib => src/lib}/format.js (100%) rename {lib => src/lib}/kafka.ts (100%) rename {lib => src/lib}/lang.js (100%) rename {lib => src/lib}/load.ts (100%) rename {lib => src/lib}/middleware.ts (100%) rename {lib => src/lib}/prisma.ts (100%) rename {lib => src/lib}/query.ts (100%) rename {lib => src/lib}/session.ts (100%) rename {lib => src/lib}/sql.ts (100%) rename {lib => src/lib}/types.ts (100%) rename {lib => src/lib}/yup.ts (100%) rename {pages => src/pages}/404.js (90%) rename {pages => src/pages}/_app.js (96%) rename {pages => src/pages}/api/auth/login.ts (100%) rename {pages => src/pages}/api/auth/logout.ts (100%) rename {pages => src/pages}/api/auth/sso.ts (100%) rename {pages => src/pages}/api/auth/verify.ts (100%) rename {pages => src/pages}/api/config.ts (100%) rename {pages => src/pages}/api/event-data/events.ts (100%) rename {pages => src/pages}/api/event-data/fields.ts (100%) rename {pages => src/pages}/api/event-data/stats.ts (100%) rename {pages => src/pages}/api/heartbeat.ts (100%) rename {pages => src/pages}/api/me/index.ts (100%) rename {pages => src/pages}/api/me/password.ts (100%) rename {pages => src/pages}/api/me/teams.ts (100%) rename {pages => src/pages}/api/me/websites.ts (100%) rename {pages => src/pages}/api/realtime/[id].ts (100%) rename {pages => src/pages}/api/reports/[id].ts (100%) rename {pages => src/pages}/api/reports/funnel.ts (100%) rename {pages => src/pages}/api/reports/index.ts (100%) rename {pages => src/pages}/api/reports/insights.ts (100%) rename {pages => src/pages}/api/reports/retention.ts (100%) rename {pages => src/pages}/api/scripts/telemetry.js (100%) rename {pages => src/pages}/api/send.ts (100%) rename {pages => src/pages}/api/share/[id].ts (100%) rename {pages => src/pages}/api/teams/[id]/index.ts (100%) rename {pages => src/pages}/api/teams/[id]/users/[userId].ts (100%) rename {pages => src/pages}/api/teams/[id]/users/index.ts (100%) rename {pages => src/pages}/api/teams/[id]/websites/[websiteId].ts (100%) rename {pages => src/pages}/api/teams/[id]/websites/index.ts (100%) rename {pages => src/pages}/api/teams/index.ts (100%) rename {pages => src/pages}/api/teams/join.ts (100%) rename {pages => src/pages}/api/users/[id]/index.ts (100%) rename {pages => src/pages}/api/users/[id]/teams.ts (100%) rename {pages => src/pages}/api/users/[id]/usage.ts (100%) rename {pages => src/pages}/api/users/[id]/websites.ts (100%) rename {pages => src/pages}/api/users/index.ts (100%) rename {pages => src/pages}/api/websites/[id]/active.ts (100%) rename {pages => src/pages}/api/websites/[id]/daterange.ts (100%) rename {pages => src/pages}/api/websites/[id]/events.ts (100%) rename {pages => src/pages}/api/websites/[id]/index.ts (100%) rename {pages => src/pages}/api/websites/[id]/metrics.ts (100%) rename {pages => src/pages}/api/websites/[id]/pageviews.ts (100%) rename {pages => src/pages}/api/websites/[id]/reports.ts (100%) rename {pages => src/pages}/api/websites/[id]/reset.ts (100%) rename {pages => src/pages}/api/websites/[id]/stats.ts (100%) rename {pages => src/pages}/api/websites/[id]/values.ts (100%) rename {pages => src/pages}/api/websites/index.ts (100%) rename {pages => src/pages}/console/[[...id]].js (100%) rename {pages => src/pages}/dashboard/index.js (85%) rename {pages => src/pages}/index.js (100%) rename {pages => src/pages}/login.js (100%) rename {pages => src/pages}/logout.js (93%) rename {pages => src/pages}/reports/[id].js (92%) rename {pages => src/pages}/reports/create.js (87%) rename {pages => src/pages}/reports/funnel.js (86%) rename {pages => src/pages}/reports/index.js (86%) rename {pages => src/pages}/reports/insights.js (88%) rename {pages => src/pages}/reports/retention.js (86%) rename {pages => src/pages}/settings/profile/index.js (89%) rename {pages => src/pages}/settings/teams/[id].js (93%) rename {pages => src/pages}/settings/teams/index.js (91%) rename {pages => src/pages}/settings/users/[id].js (93%) rename {pages => src/pages}/settings/users/index.js (91%) rename {pages => src/pages}/settings/websites/[id].js (93%) rename {pages => src/pages}/settings/websites/index.js (92%) rename {pages => src/pages}/share/[...id].js (89%) rename {pages => src/pages}/sso.js (100%) rename {pages => src/pages}/websites/[id]/event-data.js (89%) rename {pages => src/pages}/websites/[id]/index.js (89%) rename {pages => src/pages}/websites/[id]/realtime.js (100%) rename {pages => src/pages}/websites/[id]/reports.js (100%) rename {pages => src/pages}/websites/index.js (84%) rename {queries => src/queries}/admin/report.ts (100%) rename {queries => src/queries}/admin/team.ts (100%) rename {queries => src/queries}/admin/teamUser.ts (100%) rename {queries => src/queries}/admin/teamWebsite.ts (100%) rename {queries => src/queries}/admin/user.ts (100%) rename {queries => src/queries}/admin/website.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataEvents.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataFields.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataStats.ts (100%) rename {queries => src/queries}/analytics/eventData/getEventDataUsage.ts (100%) rename {queries => src/queries}/analytics/eventData/saveEventData.ts (100%) rename {queries => src/queries}/analytics/events/getEventMetrics.ts (100%) rename {queries => src/queries}/analytics/events/getEventUsage.ts (100%) rename {queries => src/queries}/analytics/events/getEvents.ts (100%) rename {queries => src/queries}/analytics/events/saveEvent.ts (100%) rename {queries => src/queries}/analytics/getActiveVisitors.ts (100%) rename {queries => src/queries}/analytics/getRealtimeData.ts (100%) rename {queries => src/queries}/analytics/getValues.ts (100%) rename {queries => src/queries}/analytics/getWebsiteDateRange.ts (100%) rename {queries => src/queries}/analytics/getWebsiteStats.ts (100%) rename {queries => src/queries}/analytics/pageviews/getPageviewMetrics.ts (100%) rename {queries => src/queries}/analytics/pageviews/getPageviewStats.ts (100%) rename {queries => src/queries}/analytics/reports/getFunnel.ts (100%) rename {queries => src/queries}/analytics/reports/getInsights.ts (100%) rename {queries => src/queries}/analytics/reports/getRetention.ts (100%) rename {queries => src/queries}/analytics/sessions/createSession.ts (100%) rename {queries => src/queries}/analytics/sessions/getSession.ts (100%) rename {queries => src/queries}/analytics/sessions/getSessionMetrics.ts (100%) rename {queries => src/queries}/analytics/sessions/getSessionStats.ts (100%) rename {queries => src/queries}/analytics/sessions/getSessions.ts (100%) rename {queries => src/queries}/analytics/sessions/saveSessionData.ts (100%) rename {queries => src/queries}/index.js (100%) rename {store => src/store}/app.js (100%) rename {store => src/store}/dashboard.js (100%) rename {store => src/store}/queries.js (100%) rename {store => src/store}/version.js (100%) rename {store => src/store}/websites.ts (100%) rename {styles => src/styles}/index.css (100%) rename {styles => src/styles}/locale.css (100%) rename {styles => src/styles}/variables.css (100%) rename {tracker => src/tracker}/index.d.ts (100%) rename {tracker => src/tracker}/index.js (100%) diff --git a/.eslintrc.json b/.eslintrc.json index f6d90cca..a77ed5bd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,22 +19,21 @@ "plugin:@typescript-eslint/recommended", "next" ], - "plugins": ["@typescript-eslint", "prettier"], "settings": { "import/resolver": { "alias": { "map": [ - ["assets", "./assets"], - ["components", "./components"], + ["assets", "./src/assets"], + ["components", "./src/components"], ["db", "./db"], - ["hooks", "./hooks"], - ["lang", "./lang"], - ["lib", "./lib"], + ["hooks", "./src/components/hooks"], + ["lang", "./src/lang"], + ["lib", "./src/lib"], ["public", "./public"], - ["queries", "./queries"], - ["store", "./store"], - ["styles", "./styles"] + ["queries", "./src/queries"], + ["store", "./src/store"], + ["styles", "./src/styles"] ], "extensions": [".ts", ".tsx", ".js", ".jsx", ".json"] } diff --git a/jsconfig.json b/jsconfig.json index b639b0f8..738e8a46 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { - "baseUrl": "." + "baseUrl": "./src" } -} \ No newline at end of file +} diff --git a/package.components.json b/package.components.json new file mode 100644 index 00000000..4596caa2 --- /dev/null +++ b/package.components.json @@ -0,0 +1,10 @@ +{ + "name": "@umami/components", + "version": "0.1.0", + "description": "Umami React components.", + "author": "Mike Cao ", + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts" +} diff --git a/package.json b/package.json index e1361d20..4f1eaa79 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "start-env": "node scripts/start-env.js", "start-server": "node server.js", "build-app": "next build", - "build-tracker": "rollup -c rollup.tracker.config.js", + "build-components": "rollup -c rollup.components.config.mjs", + "build-tracker": "rollup -c rollup.tracker.config.mjs", "build-db": "npm-run-all copy-db-files build-db-client", "build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names", "build-geo": "node scripts/build-geo.js", @@ -115,13 +116,16 @@ "@formatjs/cli": "^4.2.29", "@netlify/plugin-nextjs": "^4.27.3", "@rollup/plugin-alias": "^5.0.0", - "@rollup/plugin-buble": "^0.21.3", - "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-buble": "^1.0.2", + "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.2", - "@rollup/plugin-replace": "^4.0.0", - "@svgr/rollup": "^7.0.0", + "@rollup/plugin-node-resolve": "^15.2.0", + "@rollup/plugin-replace": "^5.0.2", + "@svgr/rollup": "^8.1.0", "@svgr/webpack": "^6.2.1", + "@types/node": "^18.11.9", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", "@typescript-eslint/eslint-plugin": "^5.50.0", "@typescript-eslint/parser": "^5.50.0", "cross-env": "^7.0.3", @@ -143,11 +147,12 @@ "prettier": "^2.6.2", "prisma": "5.0.0", "prompts": "2.4.2", - "rollup": "^2.70.1", + "rollup": "^3.28.0", + "rollup-plugin-copy": "^3.4.0", "rollup-plugin-delete": "^2.0.0", - "rollup-plugin-dts": "^5.3.0", + "rollup-plugin-dts": "^6.0.0", "rollup-plugin-esbuild": "^5.0.0", - "rollup-plugin-node-externals": "^5.1.2", + "rollup-plugin-node-externals": "^6.1.1", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", "stylelint": "^15.10.1", @@ -156,6 +161,6 @@ "stylelint-config-recommended": "^9.0.0", "tar": "^6.1.2", "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "typescript": "^5.1.6" } } diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs new file mode 100644 index 00000000..5c8722b4 --- /dev/null +++ b/rollup.components.config.mjs @@ -0,0 +1,99 @@ +import path from 'path'; +import crypto from 'crypto'; +import resolve from '@rollup/plugin-node-resolve'; +import alias from '@rollup/plugin-alias'; +import json from '@rollup/plugin-json'; +import postcss from 'rollup-plugin-postcss'; +import copy from 'rollup-plugin-copy'; +import del from 'rollup-plugin-delete'; +import nodeExternals from 'rollup-plugin-node-externals'; +import esbuild from 'rollup-plugin-esbuild'; +import dts from 'rollup-plugin-dts'; +import svgr from '@svgr/rollup'; + +const md5 = str => crypto.createHash('md5').update(str).digest('hex'); + +const customResolver = resolve({ + extensions: ['.js', '.jsx', '.ts', '.tsx'], +}); + +const aliasConfig = { + entries: [ + { find: /^components/, replacement: path.resolve('./src/components') }, + { find: /^hooks/, replacement: path.resolve('./src/hooks') }, + { find: /^lib/, replacement: path.resolve('./src/lib') }, + { find: /^store/, replacement: path.resolve('./src/store') }, + { find: /^public/, replacement: path.resolve('./public') }, + { find: /^assets/, replacement: path.resolve('./src/assets') }, + ], + customResolver, +}; + +const external = [ + 'react', + 'react-dom', + 'react/jsx-runtime', + 'react-intl', + 'react-basics', + 'classnames', + 'next', +]; + +const jsBundle = { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.js', + format: 'cjs', + sourcemap: true, + }, + { + file: 'dist/index.mjs', + format: 'es', + sourcemap: true, + }, + ], + plugins: [ + del({ targets: 'dist/*', runOnce: true }), + copy({ targets: [{ src: './package.components.json', dest: 'dist', rename: 'package.json' }] }), + postcss({ + config: false, + extract: 'styles.css', + sourceMap: true, + minimize: true, + modules: { + generateScopedName: function (name, filename, css) { + const file = path.basename(filename, '.css').replace('.module', ''); + const hash = Buffer.from(md5(`${name}:${filename}:${css}`)) + .toString('base64') + .substring(0, 5); + + return `${file}-${name}--${hash}`; + }, + }, + }), + svgr({ icon: true }), + nodeExternals(), + json(), + alias(aliasConfig), + esbuild({ + target: 'es6', + jsx: 'transform', + loaders: { + '.js': 'jsx', + }, + }), + ], +}; + +const dtsBundle = { + input: 'src/index.ts', + output: { + file: 'dist/index.d.ts', + format: 'es', + }, + plugins: [alias(aliasConfig), nodeExternals(), json(), dts()], + external: [/\.css/], +}; + +export default [jsBundle, dtsBundle]; diff --git a/rollup.tracker.config.js b/rollup.tracker.config.mjs similarity index 93% rename from rollup.tracker.config.js rename to rollup.tracker.config.mjs index f4e7223c..465e1af3 100644 --- a/rollup.tracker.config.js +++ b/rollup.tracker.config.mjs @@ -4,7 +4,7 @@ import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; export default { - input: 'tracker/index.js', + input: 'src/tracker/index.js', output: { file: 'public/script.js', format: 'iife', diff --git a/scripts/check-lang.js b/scripts/check-lang.js index e5a0bf09..a1b60431 100644 --- a/scripts/check-lang.js +++ b/scripts/check-lang.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); -const messages = require('../lang/en-US.json'); +const messages = require('../src/lang/en-US.json'); const ignore = require('../lang-ignore.json'); const dir = path.resolve(__dirname, '../lang'); diff --git a/assets/add-user.svg b/src/assets/add-user.svg similarity index 100% rename from assets/add-user.svg rename to src/assets/add-user.svg diff --git a/assets/bar-chart.svg b/src/assets/bar-chart.svg similarity index 100% rename from assets/bar-chart.svg rename to src/assets/bar-chart.svg diff --git a/assets/bars.svg b/src/assets/bars.svg similarity index 100% rename from assets/bars.svg rename to src/assets/bars.svg diff --git a/assets/bolt.svg b/src/assets/bolt.svg similarity index 100% rename from assets/bolt.svg rename to src/assets/bolt.svg diff --git a/assets/calendar.svg b/src/assets/calendar.svg similarity index 100% rename from assets/calendar.svg rename to src/assets/calendar.svg diff --git a/assets/clock.svg b/src/assets/clock.svg similarity index 100% rename from assets/clock.svg rename to src/assets/clock.svg diff --git a/assets/dashboard.svg b/src/assets/dashboard.svg similarity index 100% rename from assets/dashboard.svg rename to src/assets/dashboard.svg diff --git a/assets/expand.svg b/src/assets/expand.svg similarity index 100% rename from assets/expand.svg rename to src/assets/expand.svg diff --git a/assets/eye.svg b/src/assets/eye.svg similarity index 100% rename from assets/eye.svg rename to src/assets/eye.svg diff --git a/assets/funnel.svg b/src/assets/funnel.svg similarity index 100% rename from assets/funnel.svg rename to src/assets/funnel.svg diff --git a/assets/gear.svg b/src/assets/gear.svg similarity index 100% rename from assets/gear.svg rename to src/assets/gear.svg diff --git a/assets/globe.svg b/src/assets/globe.svg similarity index 100% rename from assets/globe.svg rename to src/assets/globe.svg diff --git a/assets/lightbulb.svg b/src/assets/lightbulb.svg similarity index 100% rename from assets/lightbulb.svg rename to src/assets/lightbulb.svg diff --git a/assets/link.svg b/src/assets/link.svg similarity index 100% rename from assets/link.svg rename to src/assets/link.svg diff --git a/assets/lock.svg b/src/assets/lock.svg similarity index 100% rename from assets/lock.svg rename to src/assets/lock.svg diff --git a/assets/logo.svg b/src/assets/logo.svg similarity index 100% rename from assets/logo.svg rename to src/assets/logo.svg diff --git a/assets/magnet.svg b/src/assets/magnet.svg similarity index 100% rename from assets/magnet.svg rename to src/assets/magnet.svg diff --git a/assets/moon.svg b/src/assets/moon.svg similarity index 100% rename from assets/moon.svg rename to src/assets/moon.svg diff --git a/assets/nodes.svg b/src/assets/nodes.svg similarity index 100% rename from assets/nodes.svg rename to src/assets/nodes.svg diff --git a/assets/overview.svg b/src/assets/overview.svg similarity index 100% rename from assets/overview.svg rename to src/assets/overview.svg diff --git a/assets/profile.svg b/src/assets/profile.svg similarity index 100% rename from assets/profile.svg rename to src/assets/profile.svg diff --git a/assets/redo.svg b/src/assets/redo.svg similarity index 100% rename from assets/redo.svg rename to src/assets/redo.svg diff --git a/assets/reports.svg b/src/assets/reports.svg similarity index 100% rename from assets/reports.svg rename to src/assets/reports.svg diff --git a/assets/sun.svg b/src/assets/sun.svg similarity index 100% rename from assets/sun.svg rename to src/assets/sun.svg diff --git a/assets/user.svg b/src/assets/user.svg similarity index 100% rename from assets/user.svg rename to src/assets/user.svg diff --git a/assets/users.svg b/src/assets/users.svg similarity index 100% rename from assets/users.svg rename to src/assets/users.svg diff --git a/assets/visitor.svg b/src/assets/visitor.svg similarity index 100% rename from assets/visitor.svg rename to src/assets/visitor.svg diff --git a/assets/website.svg b/src/assets/website.svg similarity index 100% rename from assets/website.svg rename to src/assets/website.svg diff --git a/components/common/ConfirmDeleteForm.js b/src/components/common/ConfirmDeleteForm.js similarity index 93% rename from components/common/ConfirmDeleteForm.js rename to src/components/common/ConfirmDeleteForm.js index 3496a305..fed618da 100644 --- a/components/common/ConfirmDeleteForm.js +++ b/src/components/common/ConfirmDeleteForm.js @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Button, LoadingButton, Form, FormButtons } from 'react-basics'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function ConfirmDeleteForm({ name, onConfirm, onClose }) { const [loading, setLoading] = useState(false); diff --git a/components/common/Empty.js b/src/components/common/Empty.js similarity index 86% rename from components/common/Empty.js rename to src/components/common/Empty.js index 95681b16..c0be761a 100644 --- a/components/common/Empty.js +++ b/src/components/common/Empty.js @@ -1,6 +1,6 @@ import classNames from 'classnames'; import styles from './Empty.module.css'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function Empty({ message, className }) { const { formatMessage, messages } = useMessages(); diff --git a/components/common/Empty.module.css b/src/components/common/Empty.module.css similarity index 100% rename from components/common/Empty.module.css rename to src/components/common/Empty.module.css diff --git a/components/common/EmptyPlaceholder.js b/src/components/common/EmptyPlaceholder.js similarity index 100% rename from components/common/EmptyPlaceholder.js rename to src/components/common/EmptyPlaceholder.js diff --git a/components/common/ErrorBoundary.js b/src/components/common/ErrorBoundary.js similarity index 93% rename from components/common/ErrorBoundary.js rename to src/components/common/ErrorBoundary.js index f97fd92c..32cedb39 100644 --- a/components/common/ErrorBoundary.js +++ b/src/components/common/ErrorBoundary.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { ErrorBoundary as Boundary } from 'react-error-boundary'; import { Button } from 'react-basics'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; import styles from './ErrorBoundry.module.css'; const logError = (error, info) => { diff --git a/components/common/ErrorBoundry.module.css b/src/components/common/ErrorBoundry.module.css similarity index 100% rename from components/common/ErrorBoundry.module.css rename to src/components/common/ErrorBoundry.module.css diff --git a/components/common/ErrorMessage.js b/src/components/common/ErrorMessage.js similarity index 88% rename from components/common/ErrorMessage.js rename to src/components/common/ErrorMessage.js index e2b22747..f8129c6b 100644 --- a/components/common/ErrorMessage.js +++ b/src/components/common/ErrorMessage.js @@ -1,6 +1,6 @@ import { Icon, Icons, Text } from 'react-basics'; import styles from './ErrorMessage.module.css'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function ErrorMessage() { const { formatMessage, messages } = useMessages(); diff --git a/components/common/ErrorMessage.module.css b/src/components/common/ErrorMessage.module.css similarity index 100% rename from components/common/ErrorMessage.module.css rename to src/components/common/ErrorMessage.module.css diff --git a/components/common/Favicon.js b/src/components/common/Favicon.js similarity index 100% rename from components/common/Favicon.js rename to src/components/common/Favicon.js diff --git a/components/common/Favicon.module.css b/src/components/common/Favicon.module.css similarity index 100% rename from components/common/Favicon.module.css rename to src/components/common/Favicon.module.css diff --git a/components/common/FilterButtons.js b/src/components/common/FilterButtons.js similarity index 100% rename from components/common/FilterButtons.js rename to src/components/common/FilterButtons.js diff --git a/components/common/FilterLink.js b/src/components/common/FilterLink.js similarity index 91% rename from components/common/FilterLink.js rename to src/components/common/FilterLink.js index 30cdc025..2a95e011 100644 --- a/components/common/FilterLink.js +++ b/src/components/common/FilterLink.js @@ -2,8 +2,8 @@ import { Icon, Icons } from 'react-basics'; import classNames from 'classnames'; import Link from 'next/link'; import { safeDecodeURI } from 'next-basics'; -import usePageQuery from 'hooks/usePageQuery'; -import useMessages from 'hooks/useMessages'; +import usePageQuery from 'components/hooks/usePageQuery'; +import useMessages from 'components/hooks/useMessages'; import styles from './FilterLink.module.css'; export function FilterLink({ id, value, label, externalUrl, children, className }) { diff --git a/components/common/FilterLink.module.css b/src/components/common/FilterLink.module.css similarity index 100% rename from components/common/FilterLink.module.css rename to src/components/common/FilterLink.module.css diff --git a/components/common/HamburgerButton.js b/src/components/common/HamburgerButton.js similarity index 93% rename from components/common/HamburgerButton.js rename to src/components/common/HamburgerButton.js index 48c80770..9feee67b 100644 --- a/components/common/HamburgerButton.js +++ b/src/components/common/HamburgerButton.js @@ -2,8 +2,8 @@ import { Button, Icon } from 'react-basics'; import { useState } from 'react'; import MobileMenu from './MobileMenu'; import Icons from 'components/icons'; -import useMessages from 'hooks/useMessages'; -import useConfig from 'hooks/useConfig'; +import useMessages from 'components/hooks/useMessages'; +import useConfig from 'components/hooks/useConfig'; export function HamburgerButton() { const { formatMessage, labels } = useMessages(); diff --git a/components/common/HamburgerButton.module.css b/src/components/common/HamburgerButton.module.css similarity index 100% rename from components/common/HamburgerButton.module.css rename to src/components/common/HamburgerButton.module.css diff --git a/components/common/HoverTooltip.js b/src/components/common/HoverTooltip.js similarity index 100% rename from components/common/HoverTooltip.js rename to src/components/common/HoverTooltip.js diff --git a/components/common/HoverTooltip.module.css b/src/components/common/HoverTooltip.module.css similarity index 100% rename from components/common/HoverTooltip.module.css rename to src/components/common/HoverTooltip.module.css diff --git a/components/common/LinkButton.js b/src/components/common/LinkButton.js similarity index 77% rename from components/common/LinkButton.js rename to src/components/common/LinkButton.js index 8c050147..54c7fa63 100644 --- a/components/common/LinkButton.js +++ b/src/components/common/LinkButton.js @@ -2,7 +2,7 @@ import Link from 'next/link'; import { Icon, Icons, Text } from 'react-basics'; import styles from './LinkButton.module.css'; -export default function LinkButton({ href, icon, children }) { +export function LinkButton({ href, icon, children }) { return ( {icon || } @@ -10,3 +10,5 @@ export default function LinkButton({ href, icon, children }) { ); } + +export default LinkButton; diff --git a/components/common/LinkButton.module.css b/src/components/common/LinkButton.module.css similarity index 100% rename from components/common/LinkButton.module.css rename to src/components/common/LinkButton.module.css diff --git a/components/common/MobileMenu.js b/src/components/common/MobileMenu.js similarity index 100% rename from components/common/MobileMenu.js rename to src/components/common/MobileMenu.js diff --git a/components/common/MobileMenu.module.css b/src/components/common/MobileMenu.module.css similarity index 100% rename from components/common/MobileMenu.module.css rename to src/components/common/MobileMenu.module.css diff --git a/components/common/Pager.js b/src/components/common/Pager.js similarity index 95% rename from components/common/Pager.js rename to src/components/common/Pager.js index aaeffbae..7a5e7ed5 100644 --- a/components/common/Pager.js +++ b/src/components/common/Pager.js @@ -1,6 +1,6 @@ import styles from './Pager.module.css'; import { Button, Flexbox, Icon, Icons } from 'react-basics'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function Pager({ page, pageSize, count, onPageChange }) { const { formatMessage, labels } = useMessages(); diff --git a/components/common/Pager.module.css b/src/components/common/Pager.module.css similarity index 100% rename from components/common/Pager.module.css rename to src/components/common/Pager.module.css diff --git a/components/common/SettingsTable.js b/src/components/common/SettingsTable.js similarity index 98% rename from components/common/SettingsTable.js rename to src/components/common/SettingsTable.js index eb7a6411..2df3b391 100644 --- a/components/common/SettingsTable.js +++ b/src/components/common/SettingsTable.js @@ -1,5 +1,5 @@ import Empty from 'components/common/Empty'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; import { useState } from 'react'; import { SearchField, diff --git a/components/common/SettingsTable.module.css b/src/components/common/SettingsTable.module.css similarity index 100% rename from components/common/SettingsTable.module.css rename to src/components/common/SettingsTable.module.css diff --git a/components/common/UpdateNotice.js b/src/components/common/UpdateNotice.js similarity index 96% rename from components/common/UpdateNotice.js rename to src/components/common/UpdateNotice.js index bef6be98..e3edc70c 100644 --- a/components/common/UpdateNotice.js +++ b/src/components/common/UpdateNotice.js @@ -4,7 +4,7 @@ import { setItem } from 'next-basics'; import useStore, { checkVersion } from 'store/version'; import { REPO_URL, VERSION_CHECK } from 'lib/constants'; import styles from './UpdateNotice.module.css'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; import { useRouter } from 'next/router'; export function UpdateNotice({ user, config }) { diff --git a/components/common/UpdateNotice.module.css b/src/components/common/UpdateNotice.module.css similarity index 100% rename from components/common/UpdateNotice.module.css rename to src/components/common/UpdateNotice.module.css diff --git a/components/common/WorldMap.js b/src/components/common/WorldMap.js similarity index 94% rename from components/common/WorldMap.js rename to src/components/common/WorldMap.js index 9c91e4a4..b593099b 100644 --- a/components/common/WorldMap.js +++ b/src/components/common/WorldMap.js @@ -5,9 +5,9 @@ import classNames from 'classnames'; import { colord } from 'colord'; import HoverTooltip from 'components/common/HoverTooltip'; import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants'; -import useTheme from 'hooks/useTheme'; -import useCountryNames from 'hooks/useCountryNames'; -import useLocale from 'hooks/useLocale'; +import useTheme from 'components/hooks/useTheme'; +import useCountryNames from 'components/hooks/useCountryNames'; +import useLocale from 'components/hooks/useLocale'; import { formatLongNumber } from 'lib/format'; import { percentFilter } from 'lib/filters'; import styles from './WorldMap.module.css'; diff --git a/components/common/WorldMap.module.css b/src/components/common/WorldMap.module.css similarity index 100% rename from components/common/WorldMap.module.css rename to src/components/common/WorldMap.module.css diff --git a/components/declarations.d.ts b/src/components/declarations.d.ts similarity index 100% rename from components/declarations.d.ts rename to src/components/declarations.d.ts diff --git a/hooks/index.js b/src/components/hooks/index.js similarity index 100% rename from hooks/index.js rename to src/components/hooks/index.js diff --git a/hooks/useApi.ts b/src/components/hooks/useApi.ts similarity index 100% rename from hooks/useApi.ts rename to src/components/hooks/useApi.ts diff --git a/hooks/useApiFilter.ts b/src/components/hooks/useApiFilter.ts similarity index 100% rename from hooks/useApiFilter.ts rename to src/components/hooks/useApiFilter.ts diff --git a/hooks/useConfig.js b/src/components/hooks/useConfig.js similarity index 91% rename from hooks/useConfig.js rename to src/components/hooks/useConfig.js index 2dead15a..6b37c87b 100644 --- a/hooks/useConfig.js +++ b/src/components/hooks/useConfig.js @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import useStore, { setConfig } from 'store/app'; -import useApi from 'hooks/useApi'; +import useApi from 'components/hooks/useApi'; let loading = false; diff --git a/hooks/useCountryNames.js b/src/components/hooks/useCountryNames.js similarity index 100% rename from hooks/useCountryNames.js rename to src/components/hooks/useCountryNames.js diff --git a/hooks/useDateRange.js b/src/components/hooks/useDateRange.js similarity index 100% rename from hooks/useDateRange.js rename to src/components/hooks/useDateRange.js diff --git a/hooks/useDocumentClick.js b/src/components/hooks/useDocumentClick.js similarity index 100% rename from hooks/useDocumentClick.js rename to src/components/hooks/useDocumentClick.js diff --git a/hooks/useEscapeKey.js b/src/components/hooks/useEscapeKey.js similarity index 100% rename from hooks/useEscapeKey.js rename to src/components/hooks/useEscapeKey.js diff --git a/hooks/useFilters.js b/src/components/hooks/useFilters.js similarity index 97% rename from hooks/useFilters.js rename to src/components/hooks/useFilters.js index 089f2ee8..e1a9a885 100644 --- a/hooks/useFilters.js +++ b/src/components/hooks/useFilters.js @@ -1,4 +1,4 @@ -import { useMessages } from 'hooks'; +import { useMessages } from './useMessages'; import { OPERATORS } from 'lib/constants'; export function useFilters() { diff --git a/hooks/useForceUpdate.js b/src/components/hooks/useForceUpdate.js similarity index 100% rename from hooks/useForceUpdate.js rename to src/components/hooks/useForceUpdate.js diff --git a/hooks/useFormat.js b/src/components/hooks/useFormat.js similarity index 100% rename from hooks/useFormat.js rename to src/components/hooks/useFormat.js diff --git a/hooks/useLanguageNames.js b/src/components/hooks/useLanguageNames.js similarity index 100% rename from hooks/useLanguageNames.js rename to src/components/hooks/useLanguageNames.js diff --git a/hooks/useLocale.js b/src/components/hooks/useLocale.js similarity index 96% rename from hooks/useLocale.js rename to src/components/hooks/useLocale.js index 86ca9904..6353b033 100644 --- a/hooks/useLocale.js +++ b/src/components/hooks/useLocale.js @@ -4,7 +4,7 @@ import { httpGet, setItem } from 'next-basics'; import { LOCALE_CONFIG } from 'lib/constants'; import { getDateLocale, getTextDirection } from 'lib/lang'; import useStore, { setLocale } from 'store/app'; -import useForceUpdate from 'hooks/useForceUpdate'; +import useForceUpdate from 'components/hooks/useForceUpdate'; import enUS from 'public/intl/messages/en-US.json'; const messages = { diff --git a/hooks/useMessages.js b/src/components/hooks/useMessages.js similarity index 100% rename from hooks/useMessages.js rename to src/components/hooks/useMessages.js diff --git a/hooks/usePageQuery.js b/src/components/hooks/usePageQuery.js similarity index 100% rename from hooks/usePageQuery.js rename to src/components/hooks/usePageQuery.js diff --git a/hooks/useReport.js b/src/components/hooks/useReport.js similarity index 100% rename from hooks/useReport.js rename to src/components/hooks/useReport.js diff --git a/hooks/useReports.js b/src/components/hooks/useReports.js similarity index 94% rename from hooks/useReports.js rename to src/components/hooks/useReports.js index 932fa6dc..d9292aeb 100644 --- a/hooks/useReports.js +++ b/src/components/hooks/useReports.js @@ -1,6 +1,6 @@ import { useState } from 'react'; import useApi from './useApi'; -import useApiFilter from 'hooks/useApiFilter'; +import useApiFilter from 'components/hooks/useApiFilter'; export function useReports() { const [modified, setModified] = useState(Date.now()); diff --git a/hooks/useRequireLogin.js b/src/components/hooks/useRequireLogin.js similarity index 84% rename from hooks/useRequireLogin.js rename to src/components/hooks/useRequireLogin.js index 3a95c988..82a6d220 100644 --- a/hooks/useRequireLogin.js +++ b/src/components/hooks/useRequireLogin.js @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useRouter } from 'next/router'; -import useApi from 'hooks/useApi'; -import useUser from 'hooks/useUser'; +import useApi from 'components/hooks/useApi'; +import useUser from 'components/hooks/useUser'; export function useRequireLogin() { const router = useRouter(); diff --git a/hooks/useShareToken.js b/src/components/hooks/useShareToken.js similarity index 100% rename from hooks/useShareToken.js rename to src/components/hooks/useShareToken.js diff --git a/hooks/useSticky.js b/src/components/hooks/useSticky.js similarity index 100% rename from hooks/useSticky.js rename to src/components/hooks/useSticky.js diff --git a/hooks/useTheme.js b/src/components/hooks/useTheme.js similarity index 100% rename from hooks/useTheme.js rename to src/components/hooks/useTheme.js diff --git a/hooks/useTimezone.js b/src/components/hooks/useTimezone.js similarity index 100% rename from hooks/useTimezone.js rename to src/components/hooks/useTimezone.js diff --git a/hooks/useUser.js b/src/components/hooks/useUser.js similarity index 100% rename from hooks/useUser.js rename to src/components/hooks/useUser.js diff --git a/hooks/useWebsite.js b/src/components/hooks/useWebsite.js similarity index 100% rename from hooks/useWebsite.js rename to src/components/hooks/useWebsite.js diff --git a/hooks/useWebsiteReports.js b/src/components/hooks/useWebsiteReports.js similarity index 94% rename from hooks/useWebsiteReports.js rename to src/components/hooks/useWebsiteReports.js index 3b7ec415..c637bc76 100644 --- a/hooks/useWebsiteReports.js +++ b/src/components/hooks/useWebsiteReports.js @@ -1,6 +1,6 @@ import { useState } from 'react'; import useApi from './useApi'; -import useApiFilter from 'hooks/useApiFilter'; +import useApiFilter from 'components/hooks/useApiFilter'; export function useWebsiteReports(websiteId) { const [modified, setModified] = useState(Date.now()); diff --git a/components/icons.ts b/src/components/icons.ts similarity index 98% rename from components/icons.ts rename to src/components/icons.ts index 01d7caf5..8eb1f8b0 100644 --- a/components/icons.ts +++ b/src/components/icons.ts @@ -22,7 +22,7 @@ import User from 'assets/user.svg'; import Users from 'assets/users.svg'; import Visitor from 'assets/visitor.svg'; -const icons = { +const icons: any = { ...Icons, AddUser, Bars, diff --git a/components/input/DateFilter.js b/src/components/input/DateFilter.js similarity index 96% rename from components/input/DateFilter.js rename to src/components/input/DateFilter.js index af4b69dd..ffbcff69 100644 --- a/components/input/DateFilter.js +++ b/src/components/input/DateFilter.js @@ -2,10 +2,10 @@ import { useState } from 'react'; import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; -import useLocale from 'hooks/useLocale'; +import useLocale from 'components/hooks/useLocale'; import { formatDate } from 'lib/date'; import Icons from 'components/icons'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function DateFilter({ value, diff --git a/components/input/LanguageButton.js b/src/components/input/LanguageButton.js similarity index 96% rename from components/input/LanguageButton.js rename to src/components/input/LanguageButton.js index d4c1cbc3..3c0d0cd6 100644 --- a/components/input/LanguageButton.js +++ b/src/components/input/LanguageButton.js @@ -1,7 +1,7 @@ import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics'; import classNames from 'classnames'; import { languages } from 'lib/lang'; -import useLocale from 'hooks/useLocale'; +import useLocale from 'components/hooks/useLocale'; import Icons from 'components/icons'; import styles from './LanguageButton.module.css'; diff --git a/components/input/LanguageButton.module.css b/src/components/input/LanguageButton.module.css similarity index 100% rename from components/input/LanguageButton.module.css rename to src/components/input/LanguageButton.module.css diff --git a/components/input/LogoutButton.js b/src/components/input/LogoutButton.js similarity index 84% rename from components/input/LogoutButton.js rename to src/components/input/LogoutButton.js index 4a15cd68..2b04a78a 100644 --- a/components/input/LogoutButton.js +++ b/src/components/input/LogoutButton.js @@ -1,11 +1,11 @@ import { Button, Icon, Icons, TooltipPopup } from 'react-basics'; import Link from 'next/link'; -import useMessages from 'hooks/useMessages'; +import useMessages from 'components/hooks/useMessages'; export function LogoutButton({ tooltipPosition = 'top' }) { const { formatMessage, labels } = useMessages(); return ( - + + + + + ); } diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index ad68a9fa..c625e239 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -110,8 +110,8 @@ export function WebsiteMetricsBar({ websiteId, sticky }) {
- +
diff --git a/src/lib/date.js b/src/lib/date.js index 49bff897..02f6053d 100644 --- a/src/lib/date.js +++ b/src/lib/date.js @@ -29,9 +29,19 @@ import { max, min, isDate, + subWeeks, } from 'date-fns'; import { getDateLocale } from 'lib/lang'; +export const TIME_UNIT = { + minute: 'minute', + hour: 'hour', + day: 'day', + week: 'week', + month: 'month', + year: 'year', +}; + const dateFuncs = { minute: [differenceInMinutes, addMinutes, startOfMinute], hour: [differenceInHours, addHours, startOfHour], @@ -81,6 +91,7 @@ export function parseDateRange(value, locale = 'en-US') { if (!match) return null; const { num, unit } = match.groups; + const selectedUnit = { num, unit }; if (+num === 1) { switch (unit) { @@ -90,6 +101,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfDay(now), unit: 'hour', value, + selectedUnit, }; case 'week': return { @@ -97,6 +109,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfWeek(now, { locale: dateLocale }), unit: 'day', value, + selectedUnit, }; case 'month': return { @@ -104,6 +117,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfMonth(now), unit: 'day', value, + selectedUnit, }; case 'year': return { @@ -111,6 +125,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfYear(now), unit: 'month', value, + selectedUnit, }; } } @@ -123,6 +138,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subDays(endOfDay(now), 1), unit: 'hour', value, + selectedUnit, }; case 'week': return { @@ -130,6 +146,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subDays(endOfWeek(now, { locale: dateLocale }), 1), unit: 'day', value, + selectedUnit, }; case 'month': return { @@ -137,6 +154,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subMonths(endOfMonth(now), 1), unit: 'day', value, + selectedUnit, }; case 'year': return { @@ -144,6 +162,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: subYears(endOfYear(now), 1), unit: 'month', value, + selectedUnit, }; } } @@ -155,6 +174,7 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfDay(now), unit, value, + selectedUnit, }; case 'hour': return { @@ -162,6 +182,46 @@ export function parseDateRange(value, locale = 'en-US') { endDate: endOfHour(now), unit, value, + selectedUnit, + }; + } +} + +export function incrementDateRange(value, increment) { + const { startDate, endDate, selectedUnit } = value; + + const { num, unit } = selectedUnit; + + const sub = num * increment; + + switch (unit) { + case 'day': + return { + ...value, + startDate: subDays(startDate, sub), + endDate: subDays(endDate, sub), + value: 'range', + }; + case 'week': + return { + ...value, + startDate: subWeeks(startDate, sub), + endDate: subWeeks(endDate, sub), + value: 'range', + }; + case 'month': + return { + ...value, + startDate: subMonths(startDate, sub), + endDate: subMonths(endDate, sub), + value: 'range', + }; + case 'year': + return { + ...value, + startDate: subYears(startDate, sub), + endDate: subYears(endDate, sub), + value: 'range', }; } } @@ -237,7 +297,7 @@ export function getDateLength(startDate, endDate, unit) { return diff(endDate, startDate) + 1; } -export const customFormats = { +export const CUSTOM_FORMATS = { 'en-US': { p: 'ha', pp: 'h:mm:ss', @@ -252,7 +312,7 @@ export const customFormats = { export function formatDate(date, str, locale = 'en-US') { return format( typeof date === 'string' ? new Date(date) : date, - customFormats?.[locale]?.[str] || str, + CUSTOM_FORMATS?.[locale]?.[str] || str, { locale: getDateLocale(locale), }, diff --git a/src/lib/types.ts b/src/lib/types.ts index 3f3839a4..e1e9da29 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -12,9 +12,12 @@ import { WEBSITE_FILTER_TYPES, } from './constants'; import * as yup from 'yup'; +import { TIME_UNIT } from './date'; type ObjectValues = T[keyof T]; +export type TimeUnit = ObjectValues; + export type CollectionType = ObjectValues; export type Role = ObjectValues; export type EventType = ObjectValues; @@ -181,6 +184,8 @@ export interface DateRange { startDate: Date; endDate: Date; value: string; + unit?: TimeUnit; + selectedUnit?: TimeUnit; } export interface QueryFilters { diff --git a/tsconfig.json b/tsconfig.json index 37457b40..78b225f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "baseUrl": "./src", "strictNullChecks": false, "noEmit": true, - "jsx": "preserve" + "jsx": "preserve", + "incremental": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] From 51014f6ce62e07fb8da45e8f1b862821894069f2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 21 Aug 2023 19:32:17 -0700 Subject: [PATCH 12/32] Fixed path alias. --- jsconfig.json | 5 ++++- src/components/hooks/useCountryNames.js | 2 +- src/components/hooks/useLanguageNames.js | 2 +- src/components/hooks/useLocale.js | 2 +- src/components/metrics/RegionsTable.js | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/jsconfig.json b/jsconfig.json index 738e8a46..f8124a20 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { - "baseUrl": "./src" + "baseUrl": "./src", + "paths": { + "public/*": ["./public/*"] + } } } diff --git a/src/components/hooks/useCountryNames.js b/src/components/hooks/useCountryNames.js index cd46ee44..51cabf34 100644 --- a/src/components/hooks/useCountryNames.js +++ b/src/components/hooks/useCountryNames.js @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { httpGet } from 'next-basics'; -import enUS from '../../../public/intl/country/en-US.json'; +import enUS from 'public/intl/country/en-US.json'; const countryNames = { 'en-US': enUS, diff --git a/src/components/hooks/useLanguageNames.js b/src/components/hooks/useLanguageNames.js index b684667a..afcb0ba6 100644 --- a/src/components/hooks/useLanguageNames.js +++ b/src/components/hooks/useLanguageNames.js @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { httpGet } from 'next-basics'; -import enUS from '../../../public/intl/country/en-US.json'; +import enUS from 'public/intl/country/en-US.json'; const languageNames = { 'en-US': enUS, diff --git a/src/components/hooks/useLocale.js b/src/components/hooks/useLocale.js index d84e1b4b..1374af81 100644 --- a/src/components/hooks/useLocale.js +++ b/src/components/hooks/useLocale.js @@ -5,7 +5,7 @@ import { LOCALE_CONFIG } from 'lib/constants'; import { getDateLocale, getTextDirection } from 'lib/lang'; import useStore, { setLocale } from 'store/app'; import useForceUpdate from 'components/hooks/useForceUpdate'; -import enUS from '../../../public/intl/country/en-US.json'; +import enUS from 'public/intl/country/en-US.json'; const messages = { 'en-US': enUS, diff --git a/src/components/metrics/RegionsTable.js b/src/components/metrics/RegionsTable.js index eee57a14..2e260e41 100644 --- a/src/components/metrics/RegionsTable.js +++ b/src/components/metrics/RegionsTable.js @@ -5,7 +5,7 @@ import useLocale from 'components/hooks/useLocale'; import useMessages from 'components/hooks/useMessages'; import useCountryNames from 'components/hooks/useCountryNames'; import MetricsTable from './MetricsTable'; -import regions from '../../../public/iso-3166-2.json'; +import regions from 'public/iso-3166-2.json'; export function RegionsTable({ websiteId, ...props }) { const { locale } = useLocale(); From f62d23c9bc888da7a1f029efa62f2603dbe4418f Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 22 Aug 2023 03:36:49 -0700 Subject: [PATCH 13/32] Added alias for public folder. --- jsconfig.json | 5 +---- next.config.js | 3 +++ tsconfig.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jsconfig.json b/jsconfig.json index f8124a20..738e8a46 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,8 +1,5 @@ { "compilerOptions": { - "baseUrl": "./src", - "paths": { - "public/*": ["./public/*"] - } + "baseUrl": "./src" } } diff --git a/next.config.js b/next.config.js index 2165a6e0..4ab77510 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ require('dotenv').config(); +const path = require('path'); const pkg = require('./package.json'); const contentSecurityPolicy = ` @@ -92,6 +93,8 @@ const config = { use: ['@svgr/webpack'], }); + config.resolve.alias['public'] = path.resolve('./public'); + return config; }, async headers() { diff --git a/tsconfig.json b/tsconfig.json index 78b225f4..0faf5fbc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "strictNullChecks": false, "noEmit": true, "jsx": "preserve", - "incremental": true + "incremental": false }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] From 84236c0cd9ef3599aab00bd3e8a6647e26c12827 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 22 Aug 2023 12:32:36 -0700 Subject: [PATCH 14/32] Fix date filter. --- src/components/input/WebsiteDateFilter.js | 29 +++++++++++++---------- src/lib/date.js | 7 ++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.js index e56eba4f..db8d141a 100644 --- a/src/components/input/WebsiteDateFilter.js +++ b/src/components/input/WebsiteDateFilter.js @@ -9,7 +9,8 @@ export function WebsiteDateFilter({ websiteId }) { const [dateRange, setDateRange] = useDateRange(websiteId); const { value, startDate, endDate, selectedUnit } = dateRange; - const isFutureDate = isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); + const isFutureDate = + value !== 'all' && isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); const handleChange = async value => { setDateRange(value); @@ -32,19 +33,21 @@ export function WebsiteDateFilter({ websiteId }) { onChange={handleChange} showAllTime={true} /> - - + {value !== 'all' && ( + + - - + + + )} ); } diff --git a/src/lib/date.js b/src/lib/date.js index 02f6053d..14f0e13c 100644 --- a/src/lib/date.js +++ b/src/lib/date.js @@ -195,6 +195,13 @@ export function incrementDateRange(value, increment) { const sub = num * increment; switch (unit) { + case 'hour': + return { + ...value, + startDate: subHours(startDate, sub), + endDate: subHours(endDate, sub), + value: 'range', + }; case 'day': return { ...value, From 280f6a9113877d189e89c3ff45e0050b029808df Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 22 Aug 2023 15:37:22 -0700 Subject: [PATCH 15/32] Add grant to create website/team. --- src/lib/auth.ts | 23 ++++++++++++++++++++--- src/lib/middleware.ts | 10 ++++++++-- src/lib/types.ts | 3 +++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 10f7fbca..a93f89c7 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -4,11 +4,12 @@ import debug from 'debug'; import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics'; -import { getTeamUser, getTeamWebsite, findTeamWebsiteByUserId } from 'queries'; +import { findTeamWebsiteByUserId, getTeamUser, getTeamWebsite, getWebsitesByUserId } from 'queries'; import { loadWebsite } from './load'; import { Auth } from './types'; const log = debug('umami:auth'); +const cloudMode = process.env.CLOUD_MODE; export async function setAuthKey(user, expire = 0) { const authKey = `auth:${getRandomChars(32)}`; @@ -57,7 +58,15 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri return !!(await findTeamWebsiteByUserId(websiteId, user.id)); } -export async function canCreateWebsite({ user }: Auth) { +export async function canCreateWebsite({ user, grant }: Auth) { + if (cloudMode) { + if (grant.find(a => a === PERMISSIONS.websiteCreate)) { + return true; + } + + return (await getWebsitesByUserId(user.id)).count < Number(process.env.WEBSITE_LIMIT); + } + if (user.isAdmin) { return true; } @@ -109,7 +118,15 @@ export async function canDeleteReport(auth: Auth, report: Report) { return canUpdateReport(auth, report); } -export async function canCreateTeam({ user }: Auth) { +export async function canCreateTeam({ user, grant }: Auth) { + if (cloudMode) { + if (grant.find(a => a === PERMISSIONS.teamCreate)) { + return true; + } + + return false; + } + if (user.isAdmin) { return true; } diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 0cb0cb88..18f6cc46 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -51,7 +51,7 @@ export const useAuth = createMiddleware(async (req, res, next) => { const shareToken = await parseShareToken(req); let user = null; - const { userId, authKey } = payload || {}; + const { userId, authKey, grant } = payload || {}; if (isUuid(userId)) { user = await getUserById(userId); @@ -72,7 +72,13 @@ export const useAuth = createMiddleware(async (req, res, next) => { user.isAdmin = user.role === ROLES.admin; } - (req as any).auth = { user, token, shareToken, authKey }; + (req as any).auth = { + user, + grant, + token, + shareToken, + authKey, + }; next(); }); diff --git a/src/lib/types.ts b/src/lib/types.ts index e1e9da29..3685753e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -4,6 +4,7 @@ import { DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, + PERMISSIONS, REPORT_FILTER_TYPES, REPORT_TYPES, ROLES, @@ -17,6 +18,7 @@ import { TIME_UNIT } from './date'; type ObjectValues = T[keyof T]; export type TimeUnit = ObjectValues; +export type Permission = ObjectValues; export type CollectionType = ObjectValues; export type Role = ObjectValues; @@ -78,6 +80,7 @@ export interface Auth { role: string; isAdmin: boolean; }; + grant?: Permission[]; shareToken?: { websiteId: string; }; From c5345b01bb756c6055d529f0bc5e61db47a887a2 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 10:25:57 -0700 Subject: [PATCH 16/32] Update tsconfig. --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 0faf5fbc..9d860a22 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,6 @@ "jsx": "preserve", "incremental": false }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], "exclude": ["node_modules"] } From f794b5674b3b7d6124515fb4f5f090add29f30a2 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 11:26:41 -0700 Subject: [PATCH 17/32] Update team/website permission check. --- src/lib/auth.ts | 8 ++++---- src/lib/{date.js => date.ts} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename src/lib/{date.js => date.ts} (100%) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index a93f89c7..4a42d85d 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -4,7 +4,7 @@ import debug from 'debug'; import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics'; -import { findTeamWebsiteByUserId, getTeamUser, getTeamWebsite, getWebsitesByUserId } from 'queries'; +import { findTeamWebsiteByUserId, getTeamUser, getTeamWebsite } from 'queries'; import { loadWebsite } from './load'; import { Auth } from './types'; @@ -60,11 +60,11 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri export async function canCreateWebsite({ user, grant }: Auth) { if (cloudMode) { - if (grant.find(a => a === PERMISSIONS.websiteCreate)) { + if (grant?.find(a => a === PERMISSIONS.websiteCreate)) { return true; } - return (await getWebsitesByUserId(user.id)).count < Number(process.env.WEBSITE_LIMIT); + return false; } if (user.isAdmin) { @@ -120,7 +120,7 @@ export async function canDeleteReport(auth: Auth, report: Report) { export async function canCreateTeam({ user, grant }: Auth) { if (cloudMode) { - if (grant.find(a => a === PERMISSIONS.teamCreate)) { + if (grant?.find(a => a === PERMISSIONS.teamCreate)) { return true; } diff --git a/src/lib/date.js b/src/lib/date.ts similarity index 100% rename from src/lib/date.js rename to src/lib/date.ts From d43ab3e5593572895c2d6c443291fd4073f1ec9e Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 11:55:45 -0700 Subject: [PATCH 18/32] Roll back session insert. --- src/lib/session.ts | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/lib/session.ts b/src/lib/session.ts index 5eb7398a..85c173c5 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,12 +1,27 @@ -import { secret, uuid, isUuid } from 'lib/crypto'; +import { isUuid, secret, uuid } from 'lib/crypto'; import { getClientInfo, getJsonBody } from 'lib/detect'; import { parseToken } from 'next-basics'; import { CollectRequestBody, NextApiRequestCollect } from 'pages/api/send'; import { createSession } from 'queries'; import cache from './cache'; +import clickhouse from './clickhouse'; import { loadSession, loadWebsite } from './load'; -export async function findSession(req: NextApiRequestCollect) { +export async function findSession(req: NextApiRequestCollect): Promise<{ + id: any; + websiteId: string; + hostname: string; + browser: string; + os: any; + device: string; + screen: string; + language: string; + country: any; + subdivision1: any; + subdivision2: any; + city: any; + ownerId: string; +}> { const { payload } = getJsonBody(req); if (!payload) { @@ -53,6 +68,25 @@ export async function findSession(req: NextApiRequestCollect) { const sessionId = uuid(websiteId, hostname, ip, userAgent); + // Clickhouse does not require session lookup + if (clickhouse.enabled) { + return { + id: sessionId, + websiteId, + hostname, + browser, + os: os as any, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + ownerId: website.userId, + }; + } + // Find session let session = await loadSession(sessionId); From 06de67ec55821051480f5ca1ad3e612cb0cb7809 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 12:17:32 -0700 Subject: [PATCH 19/32] Add 1 day cache limit to user/website/session --- src/lib/cache.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/lib/cache.ts b/src/lib/cache.ts index bc46c23d..c54eda2e 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -2,17 +2,20 @@ import { User, Website } from '@prisma/client'; import redis from '@umami/redis-client'; import { getSession, getUserById, getWebsiteById } from '../queries'; -const { fetchObject, storeObject, deleteObject } = redis; +const { fetchObject, storeObject, deleteObject, expire } = redis; async function fetchWebsite(id): Promise { - return fetchObject(`website:${id}`, () => getWebsiteById(id)); + return fetchObject(`website:${id}`, () => getWebsiteById(id), 86400); } async function storeWebsite(data) { const { id } = data; const key = `website:${id}`; - return storeObject(key, data); + const obj = await storeObject(key, data); + await expire(key, 86400); + + return obj; } async function deleteWebsite(id) { @@ -20,14 +23,17 @@ async function deleteWebsite(id) { } async function fetchUser(id): Promise { - return fetchObject(`user:${id}`, () => getUserById(id, { includePassword: true })); + return fetchObject(`user:${id}`, () => getUserById(id, { includePassword: true }), 86400); } async function storeUser(data) { const { id } = data; const key = `user:${id}`; - return storeObject(key, data); + const obj = await storeObject(key, data); + await expire(key, 86400); + + return obj; } async function deleteUser(id) { @@ -35,14 +41,17 @@ async function deleteUser(id) { } async function fetchSession(id) { - return fetchObject(`session:${id}`, () => getSession(id)); + return fetchObject(`session:${id}`, () => getSession(id), 86400); } async function storeSession(data) { const { id } = data; const key = `session:${id}`; - return storeObject(key, data); + const obj = await storeObject(key, data); + await expire(key, 86400); + + return obj; } async function deleteSession(id) { From 89db57a38091f44c8e077c1f43b89deee3cb21b9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 23 Aug 2023 12:50:18 -0700 Subject: [PATCH 20/32] Updated component library build. --- package.components.json | 21 ++- package.json | 4 +- rollup.components.config.mjs | 7 +- ...{useRequireLogin.js => useRequireLogin.ts} | 6 +- .../hooks/{useUser.js => useUser.ts} | 0 src/components/input/WebsiteDateFilter.js | 8 +- .../pages/settings/teams/TeamWebsitesTable.js | 3 +- .../settings/websites/WebsiteSettings.js | 4 +- .../pages/settings/websites/WebsitesTable.js | 5 +- src/index.ts | 25 +++ src/lib/middleware.ts | 2 +- src/store/version.js | 2 +- src/store/websites.ts | 2 +- yarn.lock | 156 ++++++++++-------- 14 files changed, 146 insertions(+), 99 deletions(-) rename src/components/hooks/{useRequireLogin.js => useRequireLogin.ts} (71%) rename src/components/hooks/{useUser.js => useUser.ts} (100%) diff --git a/package.components.json b/package.components.json index 4596caa2..feb3fc2e 100644 --- a/package.components.json +++ b/package.components.json @@ -1,10 +1,23 @@ { "name": "@umami/components", - "version": "0.1.0", + "version": "0.11.0", "description": "Umami React components.", "author": "Mike Cao ", "license": "MIT", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts" + "type": "module", + "main": "./index.js", + "types": "./index.d.ts", + "peerDependencies": { + "@tanstack/react-query": "^4.33.0", + "classnames": "^2.3.1", + "colord": "^2.9.2", + "immer": "^9.0.12", + "moment-timezone": "^0.5.35", + "next": "^13.4.0", + "next-basics": "^0.36.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-intl": "^5.24.7", + "zustand": "^4.3.8" + } } diff --git a/package.json b/package.json index 118c9269..1ff1730d 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "dependencies": { "@fontsource/inter": "^4.5.15", "@prisma/client": "5.0.0", - "@tanstack/react-query": "^4.16.1", + "@tanstack/react-query": "^4.33.0", "@umami/prisma-client": "^0.2.0", "@umami/redis-client": "^0.5.0", "chalk": "^4.1.1", @@ -90,7 +90,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.3.1", + "next": "13.4.19", "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs index 5c8722b4..a0b8efbd 100644 --- a/rollup.components.config.mjs +++ b/rollup.components.config.mjs @@ -44,11 +44,6 @@ const jsBundle = { output: [ { file: 'dist/index.js', - format: 'cjs', - sourcemap: true, - }, - { - file: 'dist/index.mjs', format: 'es', sourcemap: true, }, @@ -78,7 +73,7 @@ const jsBundle = { alias(aliasConfig), esbuild({ target: 'es6', - jsx: 'transform', + jsx: 'automatic', loaders: { '.js': 'jsx', }, diff --git a/src/components/hooks/useRequireLogin.js b/src/components/hooks/useRequireLogin.ts similarity index 71% rename from src/components/hooks/useRequireLogin.js rename to src/components/hooks/useRequireLogin.ts index 82a6d220..950bb60a 100644 --- a/src/components/hooks/useRequireLogin.js +++ b/src/components/hooks/useRequireLogin.ts @@ -3,7 +3,7 @@ import { useRouter } from 'next/router'; import useApi from 'components/hooks/useApi'; import useUser from 'components/hooks/useUser'; -export function useRequireLogin() { +export function useRequireLogin(handler: (data?: object) => void) { const router = useRouter(); const { get } = useApi(); const { user, setUser } = useUser(); @@ -11,9 +11,9 @@ export function useRequireLogin() { useEffect(() => { async function loadUser() { try { - const { user } = await get('/auth/verify'); + const data = await get('/auth/verify'); - setUser(user); + setUser(typeof handler === 'function' ? handler(data) : (data as any)?.user); } catch { await router.push('/login'); } diff --git a/src/components/hooks/useUser.js b/src/components/hooks/useUser.ts similarity index 100% rename from src/components/hooks/useUser.js rename to src/components/hooks/useUser.ts diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.js index db8d141a..5ab19e60 100644 --- a/src/components/input/WebsiteDateFilter.js +++ b/src/components/input/WebsiteDateFilter.js @@ -12,14 +12,12 @@ export function WebsiteDateFilter({ websiteId }) { const isFutureDate = value !== 'all' && isAfter(incrementDateRange(dateRange, -1).startDate, new Date()); - const handleChange = async value => { + const handleChange = value => { setDateRange(value); }; - const handleIncrement = async value => { - const newValue = incrementDateRange(dateRange, value); - - setDateRange(newValue); + const handleIncrement = value => { + setDateRange(incrementDateRange(dateRange, value)); }; return ( diff --git a/src/components/pages/settings/teams/TeamWebsitesTable.js b/src/components/pages/settings/teams/TeamWebsitesTable.js index 848f8207..5ce08f35 100644 --- a/src/components/pages/settings/teams/TeamWebsitesTable.js +++ b/src/components/pages/settings/teams/TeamWebsitesTable.js @@ -4,7 +4,6 @@ import Link from 'next/link'; import { Button, Icon, Icons, Text } from 'react-basics'; import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton'; import SettingsTable from 'components/common/SettingsTable'; -import useConfig from 'components/hooks/useConfig'; export function TeamWebsitesTable({ data = [], @@ -13,9 +12,9 @@ export function TeamWebsitesTable({ onFilterChange, onPageChange, onPageSizeChange, + openExternal = false, }) { const { formatMessage, labels } = useMessages(); - const { openExternal } = useConfig(); const { user } = useUser(); const columns = [ diff --git a/src/components/pages/settings/websites/WebsiteSettings.js b/src/components/pages/settings/websites/WebsiteSettings.js index cdd0fe04..ac8cd87c 100644 --- a/src/components/pages/settings/websites/WebsiteSettings.js +++ b/src/components/pages/settings/websites/WebsiteSettings.js @@ -10,12 +10,10 @@ import TrackingCode from 'components/pages/settings/websites/TrackingCode'; import ShareUrl from 'components/pages/settings/websites/ShareUrl'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; -import useConfig from 'components/hooks/useConfig'; -export function WebsiteSettings({ websiteId }) { +export function WebsiteSettings({ websiteId, openExternal = false }) { const router = useRouter(); const { formatMessage, labels, messages } = useMessages(); - const { openExternal } = useConfig(); const { get, useQuery } = useApi(); const { showToast } = useToasts(); const { data, isLoading } = useQuery( diff --git a/src/components/pages/settings/websites/WebsitesTable.js b/src/components/pages/settings/websites/WebsitesTable.js index d35da757..7fa50716 100644 --- a/src/components/pages/settings/websites/WebsitesTable.js +++ b/src/components/pages/settings/websites/WebsitesTable.js @@ -3,7 +3,6 @@ import { Button, Text, Icon, Icons } from 'react-basics'; import SettingsTable from 'components/common/SettingsTable'; import Empty from 'components/common/Empty'; import useMessages from 'components/hooks/useMessages'; -import useConfig from 'components/hooks/useConfig'; import useUser from 'components/hooks/useUser'; export function WebsitesTable({ @@ -14,12 +13,12 @@ export function WebsitesTable({ onPageSizeChange, showTeam, showEditButton, + openExternal = false, }) { const { formatMessage, labels } = useMessages(); - const { openExternal } = useConfig(); const { user } = useUser(); - const showTable = data && (filterValue || data?.data.length !== 0); + const showTable = data && (filterValue || data?.data?.length !== 0); const teamColumns = [ { name: 'teamName', label: formatMessage(labels.teamName) }, diff --git a/src/index.ts b/src/index.ts index 6ca70afa..f2ef13ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,6 +64,31 @@ export * from 'components/layout/SettingsLayout'; export * from 'components/layout/ShareLayout'; export * from 'components/layout/SideNav'; */ + +export * from 'components/hooks/useApi'; +export * from 'components/hooks/useConfig'; +export * from 'components/hooks/useCountryNames'; +export * from 'components/hooks/useDateRange'; +export * from 'components/hooks/useDocumentClick'; +export * from 'components/hooks/useEscapeKey'; +export * from 'components/hooks/useFilters'; +export * from 'components/hooks/useForceUpdate'; +export * from 'components/hooks/useFormat'; +export * from 'components/hooks/useLanguageNames'; +export * from 'components/hooks/useLocale'; +export * from 'components/hooks/useMessages'; +export * from 'components/hooks/usePageQuery'; +export * from 'components/hooks/useReport'; +export * from 'components/hooks/useReports'; +export * from 'components/hooks/useRequireLogin'; +export * from 'components/hooks/useShareToken'; +export * from 'components/hooks/useSticky'; +export * from 'components/hooks/useTheme'; +export * from 'components/hooks/useTimezone'; +export * from 'components/hooks/useUser'; +export * from 'components/hooks/useWebsite'; +export * from 'components/hooks/useWebsiteReports'; + export * from 'components/pages/settings/teams/TeamAddForm'; export * from 'components/pages/settings/teams/TeamAddWebsiteForm'; export * from 'components/pages/settings/teams/TeamDeleteForm'; diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 18f6cc46..0efb9762 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -60,7 +60,7 @@ export const useAuth = createMiddleware(async (req, res, next) => { } if (process.env.NODE_ENV === 'development') { - log({ token, shareToken, payload, user }); + log({ token, shareToken, payload, user, grant }); } if (!user?.id && !shareToken) { diff --git a/src/store/version.js b/src/store/version.js index c232c7fa..3b5afaac 100644 --- a/src/store/version.js +++ b/src/store/version.js @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import produce from 'immer'; +import { produce } from 'immer'; import semver from 'semver'; import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from 'lib/constants'; import { getItem } from 'next-basics'; diff --git a/src/store/websites.ts b/src/store/websites.ts index 0d210af6..5d0eeccd 100644 --- a/src/store/websites.ts +++ b/src/store/websites.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import produce from 'immer'; +import { produce } from 'immer'; import { DateRange } from 'lib/types'; const store = create(() => ({})); diff --git a/yarn.lock b/yarn.lock index 5bec68b2..c20730f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1754,10 +1754,10 @@ slash "^3.0.0" tiny-glob "^0.2.9" -"@next/env@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1.tgz#589707043065f6b71d411ed9b8f1ffd057c0fd4a" - integrity sha512-EDtCoedIZC7JlUQ3uaQpSc4aVmyhbLHmQVALg7pFfQgOTjgSnn7mKtA0DiCMkYvvsx6aFb5octGMtWrOtGXW9A== +"@next/env@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3" + integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ== "@next/eslint-plugin-next@12.3.4": version "12.3.4" @@ -1766,50 +1766,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1.tgz#2c9719dd10a9cdf63bf50a7576b05dcf78999fe8" - integrity sha512-UXPtriEc/pBP8luSLSCZBcbzPeVv+SSjs9cH/KygTbhmACye8/OOXRZO13Z2Wq1G0gLmEAIHQAOuF+vafPd2lw== +"@next/swc-darwin-arm64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6" + integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ== -"@next/swc-darwin-x64@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1.tgz#0be90342c89e53a390ccd9bece15f7f5cd480049" - integrity sha512-lT36yYxosCfLtplFzJWgo0hrPu6/do8+msgM7oQkPeohDNdhjtjFUgOOwdSnPublLR6Mo2Ym4P/wl5OANuD2bw== +"@next/swc-darwin-x64@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a" + integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw== -"@next/swc-linux-arm64-gnu@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1.tgz#a7353265839f8b8569a346a444dc3ab3770d297e" - integrity sha512-wRb76nLWJhonH8s3kxC/1tFguEkeOPayIwe9mkaz1G/yeS3OrjeyKMJsb4+Kdg0zbTo53bNCOl59NNtDM7yyyw== +"@next/swc-linux-arm64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6" + integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg== -"@next/swc-linux-arm64-musl@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1.tgz#24552e6102c350e372f83f505a1d93c880551a50" - integrity sha512-qz3BzjJRZ16Iq/jrp+pjiYOc0jTjHlfmxQmZk9x/+5uhRP6/eWQSTAPVJ33BMo6oK5O5N4644OgTAbzXzorecg== +"@next/swc-linux-arm64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86" + integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA== -"@next/swc-linux-x64-gnu@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1.tgz#5f335a683b6eafa52307b12af97782993b6c45ff" - integrity sha512-6mgkLmwlyWlomQmpl21I3hxgqE5INoW4owTlcLpNsd1V4wP+J46BlI/5zV5KWWbzjfncIqzXoeGs5Eg+1GHODA== +"@next/swc-linux-x64-gnu@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1" + integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g== -"@next/swc-linux-x64-musl@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1.tgz#58e5aad6f97203a0788783f66324456c8f9cdb50" - integrity sha512-uqm5sielhQmKJM+qayIhgZv1KlS5pqTdQ99b+Z7hMWryXS96qE0DftTmMZowBcUL6x7s2vSXyH5wPtO1ON7LBg== +"@next/swc-linux-x64-musl@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8" + integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q== -"@next/swc-win32-arm64-msvc@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1.tgz#f8ed1badab57ed4503969758754e6fb0cf326753" - integrity sha512-WomIiTj/v3LevltlibNQKmvrOymNRYL+a0dp5R73IwPWN5FvXWwSELN/kiNALig/+T3luc4qHNTyvMCp9L6U5Q== +"@next/swc-win32-arm64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577" + integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw== -"@next/swc-win32-ia32-msvc@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1.tgz#7f599c8975b09ee5527cc49b9e5a4d13be50635a" - integrity sha512-M+PoH+0+q658wRUbs285RIaSTYnGBSTdweH/0CdzDgA6Q4rBM0sQs4DHmO3BPP0ltCO/vViIoyG7ks66XmCA5g== +"@next/swc-win32-ia32-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd" + integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA== -"@next/swc-win32-x64-msvc@13.3.1": - version "13.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1.tgz#192d43ab44ebb98bd4f5865d0e1d7ce62703182f" - integrity sha512-Sl1F4Vp5Z1rNXWZYqJwMuWRRol4bqOB6+/d7KqkgQ4AcafKPN1PZmpkCoxv4UFHtFNIB7EotnuIhtXu3zScicQ== +"@next/swc-win32-x64-msvc@13.4.19": + version "13.4.19" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce" + integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -2244,24 +2244,24 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" -"@swc/helpers@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.0.tgz#bf1d807b60f7290d0ec763feea7ccdeda06e85f1" - integrity sha512-SjY/p4MmECVVEWspzSRpQEM3sjR17sP8PbGxELWrT+YZMBfiUyt1MRUNjMV23zohwlG2HYtCQOsCwsTHguXkyg== +"@swc/helpers@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" + integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== dependencies: tslib "^2.4.0" -"@tanstack/query-core@4.32.0": - version "4.32.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.32.0.tgz#e0f4a830283612430450c13badd353766423f523" - integrity sha512-ei4IYwL2kmlKSlCw9WgvV7PpXi0MiswVwfQRxawhJA690zWO3dU49igaQ/UMTl+Jy9jj9dK5IKAYvbX7kUvviQ== +"@tanstack/query-core@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.33.0.tgz#7756da9a75a424e521622b1d84eb55b7a2b33715" + integrity sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g== -"@tanstack/react-query@^4.16.1": - version "4.32.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.32.0.tgz#701b45b149cfd4b54a68705f9100973db3ba5d5d" - integrity sha512-B8WUMcByYAH9500ENejDCATOmEZhqjtS9wsfiQ3BNa+s+yAynY8SESI8WWHhSqUmjd0pmCSFRP6BOUGSda3QXA== +"@tanstack/react-query@^4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.33.0.tgz#e927b0343a6ecaa948fee59e9ca98fe561062638" + integrity sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA== dependencies: - "@tanstack/query-core" "4.32.0" + "@tanstack/query-core" "4.33.0" use-sync-external-store "^1.2.0" "@trysound/sax@0.2.0": @@ -4982,6 +4982,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@7.1.7: version "7.1.7" resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" @@ -6450,27 +6455,29 @@ next-basics@^0.36.0: jsonwebtoken "^9.0.0" pure-rand "^6.0.2" -next@13.3.1: - version "13.3.1" - resolved "https://registry.yarnpkg.com/next/-/next-13.3.1.tgz#17625f7423db2e059d71b41bd9031756cf2b33bc" - integrity sha512-eByWRxPzKHs2oQz1yE41LX35umhz86ZSZ+mYyXBqn2IBi2hyUqxBA88avywdr4uyH+hCJczegGsDGWbzQA5Rqw== +next@13.4.19: + version "13.4.19" + resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f" + integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw== dependencies: - "@next/env" "13.3.1" - "@swc/helpers" "0.5.0" + "@next/env" "13.4.19" + "@swc/helpers" "0.5.1" busboy "1.6.0" caniuse-lite "^1.0.30001406" postcss "8.4.14" styled-jsx "5.1.1" + watchpack "2.4.0" + zod "3.21.4" optionalDependencies: - "@next/swc-darwin-arm64" "13.3.1" - "@next/swc-darwin-x64" "13.3.1" - "@next/swc-linux-arm64-gnu" "13.3.1" - "@next/swc-linux-arm64-musl" "13.3.1" - "@next/swc-linux-x64-gnu" "13.3.1" - "@next/swc-linux-x64-musl" "13.3.1" - "@next/swc-win32-arm64-msvc" "13.3.1" - "@next/swc-win32-ia32-msvc" "13.3.1" - "@next/swc-win32-x64-msvc" "13.3.1" + "@next/swc-darwin-arm64" "13.4.19" + "@next/swc-darwin-x64" "13.4.19" + "@next/swc-linux-arm64-gnu" "13.4.19" + "@next/swc-linux-arm64-musl" "13.4.19" + "@next/swc-linux-x64-gnu" "13.4.19" + "@next/swc-linux-x64-musl" "13.4.19" + "@next/swc-win32-arm64-msvc" "13.4.19" + "@next/swc-win32-ia32-msvc" "13.4.19" + "@next/swc-win32-x64-msvc" "13.4.19" nice-try@^1.0.4: version "1.0.5" @@ -9340,6 +9347,14 @@ vue@^3.2.23: "@vue/server-renderer" "3.2.36" "@vue/shared" "3.2.36" +watchpack@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + web-streams-polyfill@^3.0.3: version "3.2.1" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" @@ -9506,6 +9521,11 @@ yup@^0.32.11: property-expr "^2.0.4" toposort "^2.0.2" +zod@3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + zustand@^4.3.8: version "4.3.9" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.9.tgz#a7d4332bbd75dfd25c6848180b3df1407217f2ad" From 10f92d0178111ae13c3022bd38bec427e74551e0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 23 Aug 2023 13:10:12 -0700 Subject: [PATCH 21/32] Fixed tsconfig. --- tsconfig.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 9d860a22..08e376bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,11 +17,20 @@ "forceConsistentCasingInFileNames": true, "allowJs": true, "strict": true, - "baseUrl": "./src", "strictNullChecks": false, "noEmit": true, "jsx": "preserve", - "incremental": false + "incremental": false, + "baseUrl": ".", + "paths": { + "assets/*": ["./src/assets/*"], + "components/*": ["./src/components/*"], + "lib/*": ["./src/lib/*"], + "pages/*": ["./src/pages/*"], + "queries/*": ["./src/queries/*"], + "store/*": ["./src/store/*"], + "styles/*": ["./src/styles/*"] + } }, "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], "exclude": ["node_modules"] From d4be41a1217f61723d2e5fdff28d56c0a43b2536 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 14:39:38 -0700 Subject: [PATCH 22/32] Fixed tsconfig. --- tsconfig.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 08e376bd..71094dd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,15 +21,15 @@ "noEmit": true, "jsx": "preserve", "incremental": false, - "baseUrl": ".", + "baseUrl": "./src", "paths": { - "assets/*": ["./src/assets/*"], - "components/*": ["./src/components/*"], - "lib/*": ["./src/lib/*"], - "pages/*": ["./src/pages/*"], - "queries/*": ["./src/queries/*"], - "store/*": ["./src/store/*"], - "styles/*": ["./src/styles/*"] + "assets/*": ["./assets/*"], + "components/*": ["./components/*"], + "lib/*": ["./lib/*"], + "pages/*": ["./pages/*"], + "queries/*": ["./queries/*"], + "store/*": ["./store/*"], + "styles/*": ["./styles/*"] } }, "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], From 9180a7008b039bddff899da1acb721b8704f02c8 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 23 Aug 2023 16:24:14 -0700 Subject: [PATCH 23/32] Fix insights validation. --- src/pages/api/reports/insights.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts index 04e51d4c..d10eba3f 100644 --- a/src/pages/api/reports/insights.ts +++ b/src/pages/api/reports/insights.ts @@ -12,8 +12,8 @@ export interface InsightsRequestBody { startDate: string; endDate: string; }; - fields: { name: string; type: string; value: string }[]; - filters: string[]; + fields: { name: string; type: string; label: string }[]; + filters: { name: string; type: string; filter: string; value: string }[]; groups: { name: string; type: string }[]; } @@ -33,12 +33,23 @@ const schema = { yup.object().shape({ name: yup.string().required(), type: yup.string().required(), + label: yup.string().required(), + }), + ) + .min(1) + .required(), + filters: yup + .array() + .of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + filter: 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(), From 2683ff278a59813cff7f4ff6f022c48b044699fe Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 24 Aug 2023 12:55:15 -0700 Subject: [PATCH 24/32] Fix event-data calls. --- src/pages/api/event-data/events.ts | 4 ++-- src/pages/api/event-data/stats.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/api/event-data/events.ts b/src/pages/api/event-data/events.ts index da0afc65..1d1d3787 100644 --- a/src/pages/api/event-data/events.ts +++ b/src/pages/api/event-data/events.ts @@ -10,7 +10,7 @@ export interface EventDataFieldsRequestQuery { websiteId: string; startAt: string; endAt: string; - event: string; + event?: string; } const schema = { @@ -18,7 +18,7 @@ const schema = { websiteId: yup.string().uuid().required(), startAt: yup.number().integer().required(), endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - event: yup.string().required(), + event: yup.string(), }), }; diff --git a/src/pages/api/event-data/stats.ts b/src/pages/api/event-data/stats.ts index b7b70dbf..7f694bc6 100644 --- a/src/pages/api/event-data/stats.ts +++ b/src/pages/api/event-data/stats.ts @@ -3,6 +3,7 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventDataStats } from 'queries/index'; import * as yup from 'yup'; export interface EventDataStatsRequestQuery { From fb78202139fe3d62ead5388d32128c7ca5e496a3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 10:45:59 -0700 Subject: [PATCH 25/32] Remove mandatory validation. --- src/lib/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 0efb9762..edf3e929 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -87,7 +87,7 @@ export const useValidate = createMiddleware(async (req: any, res, next) => { try { const { yup } = req as NextApiRequestQueryBody; - yup[req.method].validateSync({ ...req.query, ...req.body }); + yup[req.method] && yup[req.method].validateSync({ ...req.query, ...req.body }); } catch (e: any) { return badRequest(res, e.message); } From 1a47f594c2934ba28f6888d088f0b205f9904bf7 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 11:03:58 -0700 Subject: [PATCH 26/32] Add dashboard filter. --- src/components/messages.js | 1 + .../pages/reports/FieldFilterForm.js | 36 +++++++---- .../pages/reports/FilterSelectForm.js | 3 +- .../pages/websites/WebsiteMetricsBar.js | 61 +++++++++++++++++-- src/pages/api/websites/[id]/index.ts | 1 + 5 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/components/messages.js b/src/components/messages.js index ff619945..f52ed5c5 100644 --- a/src/components/messages.js +++ b/src/components/messages.js @@ -140,6 +140,7 @@ export const labels = defineMessages({ description: { id: 'label.description', defaultMessage: 'Description' }, untitled: { id: 'label.untitled', defaultMessage: 'Untitled' }, type: { id: 'label.type', defaultMessage: 'Type' }, + filter: { id: 'label.filter', defaultMessage: 'Filter' }, filters: { id: 'label.filters', defaultMessage: 'Filters' }, breakdown: { id: 'label.breakdown', defaultMessage: 'Breakdown' }, true: { id: 'label.true', defaultMessage: 'True' }, diff --git a/src/components/pages/reports/FieldFilterForm.js b/src/components/pages/reports/FieldFilterForm.js index 96e96697..01efed3f 100644 --- a/src/components/pages/reports/FieldFilterForm.js +++ b/src/components/pages/reports/FieldFilterForm.js @@ -3,7 +3,14 @@ import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics'; import { useMessages, useFilters, useFormat } from 'components/hooks'; import styles from './FieldFilterForm.module.css'; -export default function FieldFilterForm({ name, label, type, values, onSelect }) { +export default function FieldFilterForm({ + name, + label, + type, + values, + onSelect, + includeOnlyEquals, +}) { const { formatMessage, labels } = useMessages(); const [filter, setFilter] = useState('eq'); const [value, setValue] = useState(); @@ -27,17 +34,19 @@ export default function FieldFilterForm({ name, label, type, values, onSelect })
- - {({ value, label }) => { - return {label}; - }} - + {!includeOnlyEquals && ( + + {({ value, label }) => { + return {label}; + }} + + )} {value => { return {formatValue(value, name)}; diff --git a/src/components/pages/reports/FilterSelectForm.js b/src/components/pages/reports/FilterSelectForm.js index 274a00ea..8e02930e 100644 --- a/src/components/pages/reports/FilterSelectForm.js +++ b/src/components/pages/reports/FilterSelectForm.js @@ -18,7 +18,7 @@ function useValues(websiteId, type) { return { data, error, isLoading }; } -export default function FilterSelectForm({ websiteId, items, onSelect }) { +export default function FilterSelectForm({ websiteId, items, onSelect, includeOnlyEquals }) { const [field, setField] = useState(); const { data, isLoading } = useValues(websiteId, field?.name); @@ -37,6 +37,7 @@ export default function FilterSelectForm({ websiteId, items, onSelect }) { type={field?.type} values={data} onSelect={onSelect} + includeOnlyEquals={includeOnlyEquals} /> ); } diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index c625e239..5297dfcd 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -1,20 +1,25 @@ import classNames from 'classnames'; -import { Row, Column } from 'react-basics'; -import { formatShortTime } from 'lib/format'; -import MetricCard from 'components/metrics/MetricCard'; +import { useApi, useDateRange, useMessages, usePageQuery, useSticky } from 'components/hooks'; import RefreshButton from 'components/input/RefreshButton'; import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; +import MetricCard from 'components/metrics/MetricCard'; import MetricsBar from 'components/metrics/MetricsBar'; -import { useApi, useDateRange, usePageQuery, useMessages, useSticky } from 'components/hooks'; +import FilterSelectForm from 'components/pages/reports/FilterSelectForm'; +import PopupForm from 'components/pages/reports/PopupForm'; +import { formatShortTime } from 'lib/format'; +import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row, TooltipPopup } from 'react-basics'; import styles from './WebsiteMetricsBar.module.css'; export function WebsiteMetricsBar({ websiteId, sticky }) { const { formatMessage, labels } = useMessages(); + const { get, useQuery } = useApi(); const [dateRange] = useDateRange(websiteId); const { startDate, endDate, modified } = dateRange; const { ref, isSticky } = useSticky({ enabled: sticky }); const { + resolveUrl, + router, query: { url, referrer, title, os, browser, device, country, region, city }, } = usePageQuery(); @@ -39,6 +44,17 @@ export function WebsiteMetricsBar({ websiteId, sticky }) { }), ); + const fieldOptions = [ + { name: 'url', type: 'string', label: formatMessage(labels.url) }, + { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) }, + { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, + { name: 'os', type: 'string', label: formatMessage(labels.os) }, + { name: 'device', type: 'string', label: formatMessage(labels.device) }, + { name: 'country', type: 'string', label: formatMessage(labels.country) }, + { name: 'region', type: 'string', label: formatMessage(labels.region) }, + { name: 'city', type: 'string', label: formatMessage(labels.city) }, + ]; + const { pageviews, uniques, bounces, totaltime } = data || {}; const num = Math.min(data && uniques.value, data && bounces.value); const diffs = data && { @@ -48,6 +64,42 @@ export function WebsiteMetricsBar({ websiteId, sticky }) { totaltime: totaltime.value - totaltime.change, }; + const handleAddFilter = ({ name, value }) => { + router.push(resolveUrl({ [name]: value })); + }; + + const WebsiteFilterButton = () => { + return ( + + + + + + {close => { + return ( + + { + handleAddFilter(value); + close(); + }} + includeOnlyEquals={true} + /> + + ); + }} + + + ); + }; + return (
+
diff --git a/src/pages/api/websites/[id]/index.ts b/src/pages/api/websites/[id]/index.ts index 597568de..0e5aacce 100644 --- a/src/pages/api/websites/[id]/index.ts +++ b/src/pages/api/websites/[id]/index.ts @@ -23,6 +23,7 @@ const schema = { id: yup.string().uuid().required(), }), }; + export default async ( req: NextApiRequestQueryBody, res: NextApiResponse, From fe56ed35d5cd736776d8e6e2644fd09ae3afffc3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 11:21:16 -0700 Subject: [PATCH 27/32] Hide filter. --- src/components/pages/websites/WebsiteChartList.js | 2 +- src/components/pages/websites/WebsiteMetricsBar.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/components/pages/websites/WebsiteChartList.js index f6f3f765..56cbe157 100644 --- a/src/components/pages/websites/WebsiteChartList.js +++ b/src/components/pages/websites/WebsiteChartList.js @@ -40,7 +40,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) { - + {showCharts && }
) : null; diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/components/pages/websites/WebsiteMetricsBar.js index 5297dfcd..35605804 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/components/pages/websites/WebsiteMetricsBar.js @@ -10,7 +10,7 @@ import { formatShortTime } from 'lib/format'; import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row, TooltipPopup } from 'react-basics'; import styles from './WebsiteMetricsBar.module.css'; -export function WebsiteMetricsBar({ websiteId, sticky }) { +export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); @@ -162,7 +162,7 @@ export function WebsiteMetricsBar({ websiteId, sticky }) {
- + {showFilter && }
From c67deb68e6e44904b838c1ccc4d17a400e18898f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 11:33:30 -0700 Subject: [PATCH 28/32] Remove enddate check. --- src/pages/api/realtime/[id].ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/api/realtime/[id].ts b/src/pages/api/realtime/[id].ts index ab7bb406..5b1e1e05 100644 --- a/src/pages/api/realtime/[id].ts +++ b/src/pages/api/realtime/[id].ts @@ -11,12 +11,10 @@ export interface RealtimeRequestQuery { 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(), + startAt: yup.number().integer().required(), }), }; From 30c32dca7d9a35ac48f31bd127a20631a82d0da7 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 25 Aug 2023 11:34:59 -0700 Subject: [PATCH 29/32] remove required yup filters on insights report --- src/pages/api/reports/insights.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts index d10eba3f..4d17c922 100644 --- a/src/pages/api/reports/insights.ts +++ b/src/pages/api/reports/insights.ts @@ -38,18 +38,14 @@ const schema = { ) .min(1) .required(), - filters: yup - .array() - .of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - filter: yup.string().required(), - value: yup.string().required(), - }), - ) - .min(1) - .required(), + filters: yup.array().of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + filter: yup.string().required(), + value: yup.string().required(), + }), + ), groups: yup.array().of( yup.object().shape({ name: yup.string().required(), From 75861bda61836a31e07bd3b25f3e73b7f2be58eb Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 13:01:48 -0700 Subject: [PATCH 30/32] Fix insight. --- src/pages/api/reports/insights.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts index d10eba3f..4d17c922 100644 --- a/src/pages/api/reports/insights.ts +++ b/src/pages/api/reports/insights.ts @@ -38,18 +38,14 @@ const schema = { ) .min(1) .required(), - filters: yup - .array() - .of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - filter: yup.string().required(), - value: yup.string().required(), - }), - ) - .min(1) - .required(), + filters: yup.array().of( + yup.object().shape({ + name: yup.string().required(), + type: yup.string().required(), + filter: yup.string().required(), + value: yup.string().required(), + }), + ), groups: yup.array().of( yup.object().shape({ name: yup.string().required(), From 6d0b3934ebcf313d1b8784cddfd4dfc31d994c85 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 25 Aug 2023 13:32:24 -0700 Subject: [PATCH 31/32] Fix LoadingButton isLoading property --- src/components/common/ConfirmDeleteForm.js | 2 +- src/components/input/RefreshButton.js | 2 +- src/components/pages/reports/ReportHeader.js | 2 +- .../pages/settings/teams/TeamMemberRemoveButton.js | 6 +++++- .../pages/settings/teams/TeamWebsiteRemoveButton.js | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/common/ConfirmDeleteForm.js b/src/components/common/ConfirmDeleteForm.js index fed618da..3d2c383d 100644 --- a/src/components/common/ConfirmDeleteForm.js +++ b/src/components/common/ConfirmDeleteForm.js @@ -17,7 +17,7 @@ export function ConfirmDeleteForm({ name, onConfirm, onClose }) { {name} }} />

- + {formatMessage(labels.delete)} diff --git a/src/components/input/RefreshButton.js b/src/components/input/RefreshButton.js index 81bca45f..8b40cafa 100644 --- a/src/components/input/RefreshButton.js +++ b/src/components/input/RefreshButton.js @@ -16,7 +16,7 @@ export function RefreshButton({ websiteId, isLoading }) { return ( - + diff --git a/src/components/pages/reports/ReportHeader.js b/src/components/pages/reports/ReportHeader.js index e81f66fe..e81d6ece 100644 --- a/src/components/pages/reports/ReportHeader.js +++ b/src/components/pages/reports/ReportHeader.js @@ -66,7 +66,7 @@ export function ReportHeader({ icon }) { }> diff --git a/src/components/pages/settings/teams/TeamMemberRemoveButton.js b/src/components/pages/settings/teams/TeamMemberRemoveButton.js index 44fd30ab..3ec0f8b3 100644 --- a/src/components/pages/settings/teams/TeamMemberRemoveButton.js +++ b/src/components/pages/settings/teams/TeamMemberRemoveButton.js @@ -19,7 +19,11 @@ export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) { }; return ( - handleRemoveTeamMember()} disabled={disabled} loading={isLoading}> + handleRemoveTeamMember()} + disabled={disabled} + isLoading={isLoading} + > diff --git a/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js b/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js index 2ea773fc..c0ddf95c 100644 --- a/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js +++ b/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js @@ -19,7 +19,7 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { }; return ( - handleRemoveTeamMember()} loading={isLoading}> + handleRemoveTeamMember()} isLoading={isLoading}> From 236a26136da65c4a3f199115d9fa18aeb21a520f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 25 Aug 2023 13:38:52 -0700 Subject: [PATCH 32/32] Add teamId. --- src/components/pages/settings/teams/TeamMembers.js | 1 + src/components/pages/settings/teams/TeamMembersTable.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/settings/teams/TeamMembers.js b/src/components/pages/settings/teams/TeamMembers.js index 1420f6de..207ad72d 100644 --- a/src/components/pages/settings/teams/TeamMembers.js +++ b/src/components/pages/settings/teams/TeamMembers.js @@ -33,6 +33,7 @@ export function TeamMembers({ teamId, readOnly }) { <>