From baa3851fb4c9bc346df3d26fc6a3e4956724fa16 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 21 Jan 2025 19:10:34 -0800 Subject: [PATCH 01/17] Convert /api/users. --- next-env.d.ts | 2 +- next.config.js | 11 +++ package.json | 3 +- src/app/(main)/App.tsx | 4 + src/app/(main)/layout.tsx | 6 +- src/app/actions/getConfig.ts | 10 ++ src/app/api/heartbeat/route.ts | 3 + src/app/api/users/[userId]/route.ts | 72 ++++++++++++++ src/app/api/users/[userId]/teams/route.ts | 30 ++++++ src/app/api/users/[userId]/usage/route.ts | 66 +++++++++++++ src/app/api/users/[userId]/websites/route.ts | 29 ++++++ src/app/api/users/route.ts | 46 +++++++++ src/app/api/version/route.ts | 6 ++ .../api/websites/[websiteId]/active/route.ts | 24 +++++ .../websites/[websiteId]/daterange/route.ts | 19 ++++ .../api/websites/[websiteId]/metrics/route.ts | 97 +++++++++++++++++++ .../websites/[websiteId]/pageviews/route.ts | 96 ++++++++++++++++++ .../api/websites/[websiteId]/reports/route.ts | 37 +++++++ .../api/websites/[websiteId]/reset/route.ts | 19 ++++ src/app/api/websites/[websiteId]/route.ts | 85 ++++++++++++++++ .../api/websites/[websiteId]/stats/route.ts | 76 +++++++++++++++ .../websites/[websiteId]/transfer/route.ts | 51 ++++++++++ .../api/websites/[websiteId]/values/route.ts | 41 ++++++++ src/app/api/websites/route.ts | 71 ++++++++++++++ src/components/common/DataTable.tsx | 16 +-- src/components/hooks/queries/useConfig.ts | 13 +-- src/components/hooks/usePagedQuery.ts | 2 +- src/components/metrics/ActiveUsers.module.css | 5 - src/components/metrics/ActiveUsers.tsx | 2 +- src/lib/auth.ts | 56 ++++++++++- src/lib/request.ts | 28 +++++- src/lib/response.ts | 21 ++++ src/lib/schema.ts | 18 ++++ src/lib/types.ts | 4 +- src/pages/api/{config.ts => _config.ts} | 0 src/pages/api/{version.ts => _version.ts} | 0 src/pages/api/heartbeat.ts | 6 -- src/pages/api/me/teams.ts | 2 +- src/pages/api/me/websites.ts | 2 +- .../users/[userId]/{index.ts => _index.ts} | 0 .../users/[userId]/{teams.ts => _teams.ts} | 0 .../users/[userId]/{usage.ts => _usage.ts} | 0 .../[userId]/{websites.ts => _websites.ts} | 0 src/pages/api/users/{index.ts => _index.ts} | 0 .../[websiteId]/{active.ts => _active.ts} | 0 .../{daterange.ts => _daterange.ts} | 0 .../[websiteId]/{index.ts => _index.ts} | 0 .../[websiteId]/{metrics.ts => _metrics.ts} | 0 .../{pageviews.ts => _pageviews.ts} | 0 .../[websiteId]/{reports.ts => _reports.ts} | 4 +- .../[websiteId]/{reset.ts => _reset.ts} | 0 .../[websiteId]/{stats.ts => _stats.ts} | 0 .../[websiteId]/{transfer.ts => _transfer.ts} | 0 .../[websiteId]/{values.ts => _values.ts} | 0 .../api/websites/{index.ts => _index.ts} | 2 +- src/queries/analytics/getActiveVisitors.ts | 4 +- .../analytics/pageviews/getPageviewMetrics.ts | 16 ++- .../analytics/sessions/getSessionMetrics.ts | 16 ++- src/queries/prisma/report.ts | 4 +- src/queries/prisma/website.ts | 4 +- yarn.lock | 5 + 61 files changed, 1064 insertions(+), 70 deletions(-) create mode 100644 src/app/actions/getConfig.ts create mode 100644 src/app/api/heartbeat/route.ts create mode 100644 src/app/api/users/[userId]/route.ts create mode 100644 src/app/api/users/[userId]/teams/route.ts create mode 100644 src/app/api/users/[userId]/usage/route.ts create mode 100644 src/app/api/users/[userId]/websites/route.ts create mode 100644 src/app/api/users/route.ts create mode 100644 src/app/api/version/route.ts create mode 100644 src/app/api/websites/[websiteId]/active/route.ts create mode 100644 src/app/api/websites/[websiteId]/daterange/route.ts create mode 100644 src/app/api/websites/[websiteId]/metrics/route.ts create mode 100644 src/app/api/websites/[websiteId]/pageviews/route.ts create mode 100644 src/app/api/websites/[websiteId]/reports/route.ts create mode 100644 src/app/api/websites/[websiteId]/reset/route.ts create mode 100644 src/app/api/websites/[websiteId]/route.ts create mode 100644 src/app/api/websites/[websiteId]/stats/route.ts create mode 100644 src/app/api/websites/[websiteId]/transfer/route.ts create mode 100644 src/app/api/websites/[websiteId]/values/route.ts create mode 100644 src/app/api/websites/route.ts create mode 100644 src/lib/response.ts rename src/pages/api/{config.ts => _config.ts} (100%) rename src/pages/api/{version.ts => _version.ts} (100%) delete mode 100644 src/pages/api/heartbeat.ts rename src/pages/api/users/[userId]/{index.ts => _index.ts} (100%) rename src/pages/api/users/[userId]/{teams.ts => _teams.ts} (100%) rename src/pages/api/users/[userId]/{usage.ts => _usage.ts} (100%) rename src/pages/api/users/[userId]/{websites.ts => _websites.ts} (100%) rename src/pages/api/users/{index.ts => _index.ts} (100%) rename src/pages/api/websites/[websiteId]/{active.ts => _active.ts} (100%) rename src/pages/api/websites/[websiteId]/{daterange.ts => _daterange.ts} (100%) rename src/pages/api/websites/[websiteId]/{index.ts => _index.ts} (100%) rename src/pages/api/websites/[websiteId]/{metrics.ts => _metrics.ts} (100%) rename src/pages/api/websites/[websiteId]/{pageviews.ts => _pageviews.ts} (100%) rename src/pages/api/websites/[websiteId]/{reports.ts => _reports.ts} (94%) rename src/pages/api/websites/[websiteId]/{reset.ts => _reset.ts} (100%) rename src/pages/api/websites/[websiteId]/{stats.ts => _stats.ts} (100%) rename src/pages/api/websites/[websiteId]/{transfer.ts => _transfer.ts} (100%) rename src/pages/api/websites/[websiteId]/{values.ts => _values.ts} (100%) rename src/pages/api/websites/{index.ts => _index.ts} (96%) diff --git a/next-env.d.ts b/next-env.d.ts index 3cd7048e..725dd6f2 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -3,4 +3,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/next.config.js b/next.config.js index e4e55ab7..99791dff 100644 --- a/next.config.js +++ b/next.config.js @@ -8,6 +8,7 @@ const basePath = process.env.BASE_PATH; const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT; const cloudMode = process.env.CLOUD_MODE; const cloudUrl = process.env.CLOUD_URL; +const corsMaxAge = process.env.CORS_MAX_AGE; const defaultLocale = process.env.DEFAULT_LOCALE; const disableLogin = process.env.DISABLE_LOGIN; const disableUI = process.env.DISABLE_UI; @@ -59,6 +60,16 @@ const trackerHeaders = [ ]; const headers = [ + { + source: '/api/:path*', + headers: [ + { key: 'Access-Control-Allow-Credentials', value: 'true' }, + { key: 'Access-Control-Allow-Origin', value: '*' }, + { key: 'Access-Control-Allow-Headers', value: '*' }, + { key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' }, + { key: 'Access-Control-Max-Age', value: corsMaxAge || '86400' }, + ], + }, { source: '/:path*', headers: defaultHeaders, diff --git a/package.json b/package.json index 3c78fffb..1b81c514 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "url": "https://github.com/umami-software/umami.git" }, "scripts": { - "dev": "next dev -p 3000", + "dev": "next dev -p 3000 --turbo", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", @@ -119,6 +119,7 @@ "thenby": "^1.3.4", "uuid": "^9.0.0", "yup": "^0.32.11", + "zod": "^3.24.1", "zustand": "^4.5.5" }, "devDependencies": { diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index efb38043..aca94bc2 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -22,6 +22,10 @@ export function App({ children }) { return null; } + if (config.uiDisabled) { + return null; + } + return ( <> {children} diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index ba221990..8e47d7ee 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -4,11 +4,7 @@ import NavBar from './NavBar'; import Page from 'components/layout/Page'; import styles from './layout.module.css'; -export default function ({ children }) { - if (process.env.DISABLE_UI) { - return null; - } - +export default async function ({ children }) { return (
diff --git a/src/app/actions/getConfig.ts b/src/app/actions/getConfig.ts new file mode 100644 index 00000000..bb892f01 --- /dev/null +++ b/src/app/actions/getConfig.ts @@ -0,0 +1,10 @@ +'use server'; + +export async function getConfig() { + return { + telemetryDisabled: !!process.env.DISABLE_TELEMETRY, + trackerScriptName: process.env.TRACKER_SCRIPT_NAME, + uiDisabled: !!process.env.DISABLE_UI, + updatesDisabled: !!process.env.DISABLE_UPDATES, + }; +} diff --git a/src/app/api/heartbeat/route.ts b/src/app/api/heartbeat/route.ts new file mode 100644 index 00000000..91463089 --- /dev/null +++ b/src/app/api/heartbeat/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return Response.json({ ok: true }); +} diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts new file mode 100644 index 00000000..41cd6bcf --- /dev/null +++ b/src/app/api/users/[userId]/route.ts @@ -0,0 +1,72 @@ +import { z } from 'zod'; +import { canUpdateUser, canViewUser, checkAuth } from 'lib/auth'; +import { getUser, getUserByUsername, updateUser } from 'queries'; +import { json, unauthorized, badRequest } from 'lib/response'; +import { hashPassword } from 'next-basics'; +import { checkRequest } from 'lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { userId } = await params; + const auth = await checkAuth(request); + + if (!auth || !(await canViewUser(auth, userId))) { + return unauthorized(); + } + + const user = await getUser(userId); + + return json(user); +} + +export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + username: z.string().max(255), + password: z.string().max(255), + role: z.string().regex(/admin|user|view-only/i), + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { userId } = await params; + const auth = await checkAuth(request); + + if (!auth || !(await canUpdateUser(auth, userId))) { + return unauthorized(); + } + + const { username, password, role } = body; + + const user = await getUser(userId); + + const data: any = {}; + + if (password) { + data.password = hashPassword(password); + } + + // Only admin can change these fields + if (role && auth.user.isAdmin) { + data.role = role; + } + + if (username && auth.user.isAdmin) { + data.username = username; + } + + // Check when username changes + if (data.username && user.username !== data.username) { + const user = await getUserByUsername(username); + + if (user) { + return badRequest('User already exists'); + } + } + + const updated = await updateUser(userId, data); + + return json(updated); +} diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts new file mode 100644 index 00000000..0cdccdaf --- /dev/null +++ b/src/app/api/users/[userId]/teams/route.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; +import { pagingParams } from 'lib/schema'; +import { getUserTeams } from 'queries'; +import { checkAuth } from 'lib/auth'; +import { unauthorized, badRequest, json } from 'lib/response'; +import { checkRequest } from 'lib/request'; + +const schema = z.object({ + ...pagingParams, +}); + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { userId } = await params; + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + + if (!auth || (!auth.user.isAdmin && (!userId || auth.user.id !== userId))) { + return unauthorized(); + } + + const teams = await getUserTeams(userId, query); + + return json(teams); +} diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts new file mode 100644 index 00000000..177d3c35 --- /dev/null +++ b/src/app/api/users/[userId]/usage/route.ts @@ -0,0 +1,66 @@ +import { z } from 'zod'; +import { json, unauthorized, badRequest } from 'lib/response'; +import { getAllUserWebsitesIncludingTeamOwner } from 'queries/prisma/website'; +import { getEventUsage } from 'queries/analytics/events/getEventUsage'; +import { getEventDataUsage } from 'queries/analytics/events/getEventDataUsage'; +import { checkAuth } from 'lib/auth'; +import { checkRequest } from 'lib/request'; + +const schema = z.object({ + startAt: z.coerce.number(), + endAt: z.coerce.number(), +}); + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + + if (!auth || !auth.user.isAdmin) { + return unauthorized(); + } + + const { userId } = await params; + const { startAt, endAt } = query; + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const websites = await getAllUserWebsitesIncludingTeamOwner(userId); + + const websiteIds = websites.map(a => a.id); + + const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); + const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); + + const websiteUsage = websites.map(a => ({ + websiteId: a.id, + websiteName: a.name, + websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, + eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, + deletedAt: a.deletedAt, + })); + + const usage = websiteUsage.reduce( + (acc, cv) => { + acc.websiteEventUsage += cv.websiteEventUsage; + acc.eventDataUsage += cv.eventDataUsage; + + return acc; + }, + { websiteEventUsage: 0, eventDataUsage: 0 }, + ); + + const filteredWebsiteUsage = websiteUsage.filter( + a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), + ); + + return json({ + ...usage, + websites: filteredWebsiteUsage, + }); +} diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts new file mode 100644 index 00000000..61783cd6 --- /dev/null +++ b/src/app/api/users/[userId]/websites/route.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest } from 'lib/response'; +import { getUserWebsites } from 'queries/prisma/website'; +import { pagingParams } from 'lib/schema'; +import { checkRequest } from 'lib/request'; +import { checkAuth } from 'lib/auth'; + +const schema = z.object({ + ...pagingParams, +}); + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { userId } = await params; + const auth = await checkAuth(request); + + if (!auth || (!auth.user.isAdmin && auth.user.id !== userId)) { + return unauthorized(); + } + + const websites = await getUserWebsites(userId, query); + + return json(websites); +} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 00000000..870e6181 --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { hashPassword } from 'next-basics'; +import { canCreateUser, checkAuth } from 'lib/auth'; +import { ROLES } from 'lib/constants'; +import { uuid } from 'lib/crypto'; +import { checkRequest } from 'lib/request'; +import { unauthorized, json, badRequest } from 'lib/response'; +import { createUser, getUserByUsername } from 'queries'; + +const schema = z.object({ + username: z.string().max(255), + password: z.string(), + id: z.string().uuid(), + role: z.string().regex(/admin|user|view-only/i), +}); + +export async function POST(request: Request) { + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + + if (!auth || !(await canCreateUser(auth))) { + return unauthorized(); + } + + const { username, password, role, id } = body; + + const existingUser = await getUserByUsername(username, { showDeleted: true }); + + if (existingUser) { + return badRequest('User already exists'); + } + + const user = await createUser({ + id: id || uuid(), + username, + password: hashPassword(password), + role: role ?? ROLES.user, + }); + + return json(user); +} diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts new file mode 100644 index 00000000..605d2583 --- /dev/null +++ b/src/app/api/version/route.ts @@ -0,0 +1,6 @@ +import { json } from 'lib/response'; +import { CURRENT_VERSION } from 'lib/constants'; + +export async function GET() { + return json({ version: CURRENT_VERSION }); +} diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts new file mode 100644 index 00000000..22bd1999 --- /dev/null +++ b/src/app/api/websites/[websiteId]/active/route.ts @@ -0,0 +1,24 @@ +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { json, unauthorized } from 'lib/response'; +import { getActiveVisitors } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const auth = await checkAuth(request); + + if (!auth) { + return unauthorized(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await getActiveVisitors(websiteId); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts new file mode 100644 index 00000000..70460bd6 --- /dev/null +++ b/src/app/api/websites/[websiteId]/daterange/route.ts @@ -0,0 +1,19 @@ +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getWebsiteDateRange } from 'queries'; +import { json, unauthorized } from 'lib/response'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const auth = await checkAuth(request); + const { websiteId } = await params; + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await getWebsiteDateRange(websiteId); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts new file mode 100644 index 00000000..3edc0d88 --- /dev/null +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -0,0 +1,97 @@ +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants'; +import { getRequestFilters, getRequestDateRange, checkRequest } from 'lib/request'; +import { getPageviewMetrics, getSessionMetrics } from 'queries'; + +import { z } from 'zod'; +import { json, unauthorized, badRequest } from 'lib/response'; + +const schema = z.object({ + type: z.string(), + startAt: z.coerce.number(), + endAt: z.coerce.number(), + // optional + url: z.string().optional(), + referrer: z.string().optional(), + title: z.string().optional(), + query: z.string().optional(), + host: z.string().optional(), + os: z.string().optional(), + browser: z.string().optional(), + device: z.string().optional(), + country: z.string().optional(), + region: z.string().optional(), + city: z.string().optional(), + language: z.string().optional(), + event: z.string().optional(), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + search: z.string().optional(), + tag: z.string().optional(), +}); + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + const { websiteId } = await params; + const { type, limit, offset, search } = query; + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + const column = FILTER_COLUMNS[type] || type; + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + }; + + if (search) { + filters[type] = { + name: type, + column, + operator: OPERATORS.contains, + value: search, + }; + } + + if (SESSION_COLUMNS.includes(type)) { + const data = await getSessionMetrics(websiteId, type, filters, limit, offset); + + if (type === 'language') { + const combined = {}; + + for (const { x, y } of data) { + const key = String(x).toLowerCase().split('-')[0]; + + if (combined[key] === undefined) { + combined[key] = { x: key, y }; + } else { + combined[key].y += y; + } + } + + return json(Object.values(combined)); + } + + return json(data); + } + + if (EVENT_COLUMNS.includes(type)) { + const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); + + return json(data); + } + + return badRequest(); +} diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts new file mode 100644 index 00000000..7ba5b100 --- /dev/null +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -0,0 +1,96 @@ +import { z } from 'zod'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getRequestFilters, getRequestDateRange, checkRequest } from 'lib/request'; +import { unit, timezone } from 'lib/schema'; +import { getCompareDate } from 'lib/date'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { getPageviewStats, getSessionStats } from 'queries'; + +const schema = z.object({ + startAt: z.coerce.number(), + endAt: z.coerce.number(), + unit, + timezone, + url: z.string().optional(), + referrer: z.string().optional(), + title: z.string().optional(), + host: z.string().optional(), + os: z.string().optional(), + browser: z.string().optional(), + device: z.string().optional(), + country: z.string().optional(), + region: z.string().optional(), + city: z.string().optional(), + tag: z.string().optional(), + compare: z.string().optional(), +}); + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + const { websiteId } = await params; + const { timezone, compare } = query; + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate, unit } = await getRequestDateRange(query); + + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + timezone, + unit, + }; + + const [pageviews, sessions] = await Promise.all([ + getPageviewStats(websiteId, filters), + getSessionStats(websiteId, filters), + ]); + + if (compare) { + const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( + compare, + startDate, + endDate, + ); + + const [comparePageviews, compareSessions] = await Promise.all([ + getPageviewStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }), + getSessionStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }), + ]); + + return json({ + pageviews, + sessions, + startDate, + endDate, + compare: { + pageviews: comparePageviews, + sessions: compareSessions, + startDate: compareStartDate, + endDate: compareEndDate, + }, + }); + } + + return json({ pageviews, sessions }); +} diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts new file mode 100644 index 00000000..f4fc641f --- /dev/null +++ b/src/app/api/websites/[websiteId]/reports/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getWebsiteReports } from 'queries'; +import { pagingParams } from 'lib/schema'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; + +const schema = z.object({ + ...pagingParams, +}); + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + const { websiteId } = await params; + const { page, pageSize, search } = query; + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getWebsiteReports(websiteId, { + page: +page, + pageSize: +pageSize, + search, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts new file mode 100644 index 00000000..ae2131e8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/reset/route.ts @@ -0,0 +1,19 @@ +import { canUpdateWebsite, checkAuth } from 'lib/auth'; +import { resetWebsite } from 'queries'; +import { unauthorized, ok } from 'lib/response'; + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const auth = await checkAuth(request); + const { websiteId } = await params; + + if (!auth || !(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + await resetWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts new file mode 100644 index 00000000..02bb00f8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/route.ts @@ -0,0 +1,85 @@ +import { z } from 'zod'; +import { canUpdateWebsite, canDeleteWebsite, checkAuth, canViewWebsite } from 'lib/auth'; +import { SHARE_ID_REGEX } from 'lib/constants'; +import { checkRequest } from 'lib/request'; +import { ok, json, badRequest, unauthorized, serverError } from 'lib/response'; +import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const auth = await checkAuth(request); + + if (!auth) { + return unauthorized(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const website = await getWebsite(websiteId); + + return json(website); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + name: z.string(), + domain: z.string(), + shareId: z.string().regex(SHARE_ID_REGEX).nullable(), + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + const { websiteId } = await params; + const { name, domain, shareId } = body; + + if (!auth || !(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + try { + const website = await updateWebsite(websiteId, { name, domain, shareId }); + + return Response.json(website); + } catch (e: any) { + if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { + return serverError(new Error('That share ID is already taken.')); + } + + return serverError(e); + } +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const auth = await checkAuth(request); + + if (!auth) { + return unauthorized(); + } + + const { websiteId } = await params; + + if (!(await canDeleteWebsite(auth, websiteId))) { + return unauthorized(); + } + + await deleteWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts new file mode 100644 index 00000000..1c96a74f --- /dev/null +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -0,0 +1,76 @@ +import { z } from 'zod'; +import { checkRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { checkAuth, canViewWebsite } from 'lib/auth'; +import { getCompareDate } from 'lib/date'; +import { getWebsiteStats } from 'queries'; + +const schema = z.object({ + startAt: z.coerce.number(), + endAt: z.coerce.number(), + // optional + url: z.string().optional(), + referrer: z.string().optional(), + title: z.string().optional(), + query: z.string().optional(), + event: z.string().optional(), + host: z.string().optional(), + os: z.string().optional(), + browser: z.string().optional(), + device: z.string().optional(), + country: z.string().optional(), + region: z.string().optional(), + city: z.string().optional(), + tag: z.string().optional(), + compare: z.string().optional(), +}); + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + const { websiteId } = await params; + const { compare } = query; + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( + compare, + startDate, + endDate, + ); + + const filters = getRequestFilters(query); + + const metrics = await getWebsiteStats(websiteId, { + ...filters, + startDate, + endDate, + }); + + const prevPeriod = await getWebsiteStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }); + + const stats = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = { + value: Number(metrics[0][key]) || 0, + prev: Number(prevPeriod[0][key]) || 0, + }; + return obj; + }, {}); + + return json(stats); +} diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts new file mode 100644 index 00000000..d97fe0f8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/transfer/route.ts @@ -0,0 +1,51 @@ +import { z } from 'zod'; +import { canTransferWebsiteToTeam, canTransferWebsiteToUser, checkAuth } from 'lib/auth'; +import { updateWebsite } from 'queries'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; + +const schema = z.object({ + userId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), +}); + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + const { websiteId } = await params; + const { userId, teamId } = body; + + if (!auth) { + return unauthorized(); + } else if (userId) { + if (!(await canTransferWebsiteToUser(auth, websiteId, userId))) { + return unauthorized(); + } + + const website = await updateWebsite(websiteId, { + userId, + teamId: null, + }); + + return json(website); + } else if (teamId) { + if (!(await canTransferWebsiteToTeam(auth, websiteId, teamId))) { + return unauthorized(); + } + + const website = await updateWebsite(websiteId, { + userId: null, + teamId, + }); + + return json(website); + } +} diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts new file mode 100644 index 00000000..1a4967b8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; +import { getValues } from 'queries'; +import { checkRequest, getRequestDateRange } from 'lib/request'; +import { badRequest, json, unauthorized } from 'lib/response'; + +const schema = z.object({ + type: z.string(), + startAt: z.coerce.number(), + endAt: z.coerce.number(), + search: z.string().optional(), +}); + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + const { websiteId } = await params; + const { type, search } = query; + const { startDate, endDate } = await getRequestDateRange(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) { + return badRequest(); + } + + const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); + + return json(values.filter(n => n).sort()); +} diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts new file mode 100644 index 00000000..6bb1e476 --- /dev/null +++ b/src/app/api/websites/route.ts @@ -0,0 +1,71 @@ +import { z } from 'zod'; +import { canCreateTeamWebsite, canCreateWebsite, checkAuth } from 'lib/auth'; +import { json, badRequest, unauthorized } from 'lib/response'; +import { uuid } from 'lib/crypto'; +import { checkRequest } from 'lib/request'; +import { createWebsite, getUserWebsites } from 'queries'; +import { pagingParams } from 'lib/schema'; + +export async function GET(request: Request) { + const schema = z.object({ ...pagingParams }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + + if (!auth) { + return unauthorized(); + } + + const websites = await getUserWebsites(auth.user.userId, query); + + return json(websites); +} + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(100), + domain: z.string().max(500), + shareId: z.string().max(50).nullable(), + teamId: z.string().nullable(), + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + + if (!auth) { + return unauthorized(); + } + + const { name, domain, shareId, teamId } = body; + + if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { + return unauthorized(); + } + + const data: any = { + id: uuid(), + createdBy: auth.user.userId, + name, + domain, + shareId, + teamId, + }; + + if (!teamId) { + data.userId = auth.user.userId; + } + + const website = await createWebsite(data); + + return json(website); +} diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx index d2094329..f3b144a6 100644 --- a/src/components/common/DataTable.tsx +++ b/src/components/common/DataTable.tsx @@ -37,26 +37,26 @@ export function DataTable({ query: { error, isLoading, isFetched }, } = queryResult || {}; const { page, pageSize, count, data } = result || {}; - const { query } = params || {}; + const { search } = params || {}; const hasData = Boolean(!isLoading && data?.length); - const noResults = Boolean(query && !hasData); + const noResults = Boolean(search && !hasData); const { router, renderUrl } = useNavigation(); - const handleSearch = (query: string) => { - setParams({ ...params, query, page: params.page ? page : 1 }); + const handleSearch = (search: string) => { + setParams({ ...params, search, page: params.page ? page : 1 }); }; const handlePageChange = (page: number) => { - setParams({ ...params, query, page }); + setParams({ ...params, search, page }); router.push(renderUrl({ page })); }; return ( <> - {allowSearch && (hasData || query) && ( + {allowSearch && (hasData || search) && ( {hasData ? (typeof children === 'function' ? children(result) : children) : null} {isLoading && } - {!isLoading && !hasData && !query && (renderEmpty ? renderEmpty() : )} + {!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : )} {!isLoading && noResults && } {allowPaging && hasData && ( diff --git a/src/components/hooks/queries/useConfig.ts b/src/components/hooks/queries/useConfig.ts index f6293a44..f4e911a0 100644 --- a/src/components/hooks/queries/useConfig.ts +++ b/src/components/hooks/queries/useConfig.ts @@ -1,23 +1,16 @@ import { useEffect } from 'react'; import useStore, { setConfig } from 'store/app'; -import { useApi } from '../useApi'; - -let loading = false; +import { getConfig } from 'app/actions/getConfig'; export function useConfig() { const { config } = useStore(); - const { get } = useApi(); - const configUrl = process.env.configUrl; async function loadConfig() { - const data = await get(configUrl); - loading = false; - setConfig(data); + setConfig(await getConfig()); } useEffect(() => { - if (!config && !loading && configUrl) { - loading = true; + if (!config) { loadConfig(); } }, []); diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts index 19471432..e62ed0e1 100644 --- a/src/components/hooks/usePagedQuery.ts +++ b/src/components/hooks/usePagedQuery.ts @@ -11,7 +11,7 @@ export function usePagedQuery({ }: Omit & { queryFn: (params?: object) => any }): PagedQueryResult { const { query: queryParams } = useNavigation(); const [params, setParams] = useState({ - query: '', + search: '', page: +queryParams.page || 1, }); diff --git a/src/components/metrics/ActiveUsers.module.css b/src/components/metrics/ActiveUsers.module.css index 5d0a4c7d..4a984725 100644 --- a/src/components/metrics/ActiveUsers.module.css +++ b/src/components/metrics/ActiveUsers.module.css @@ -10,8 +10,3 @@ font-size: var(--font-size-md); font-weight: 400; } - -.value { - font-weight: 600; - margin-inline-end: 4px; -} diff --git a/src/components/metrics/ActiveUsers.tsx b/src/components/metrics/ActiveUsers.tsx index 05d0fc1d..966b91d9 100644 --- a/src/components/metrics/ActiveUsers.tsx +++ b/src/components/metrics/ActiveUsers.tsx @@ -24,7 +24,7 @@ export function ActiveUsers({ const count = useMemo(() => { if (websiteId) { - return data?.x || 0; + return data?.visitors || 0; } return value !== undefined ? value : 0; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 7b8ac823..34ab49b9 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,16 +1,64 @@ import { Report } from '@prisma/client'; -import { getClient } from '@umami/redis-client'; +import { getClient, redisEnabled } from '@umami/redis-client'; import debug from 'debug'; -import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; +import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; import { NextApiRequest } from 'next'; -import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics'; -import { getTeamUser, getWebsite } from 'queries'; +import { + createSecureToken, + ensureArray, + getRandomChars, + parseSecureToken, + parseToken, +} from 'next-basics'; +import { getTeamUser, getUser, getWebsite } from 'queries'; import { Auth } from './types'; const log = debug('umami:auth'); const cloudMode = process.env.CLOUD_MODE; +export async function checkAuth(request: Request) { + const token = request.headers.get('authorization')?.split(' ')?.[1]; + const payload = parseSecureToken(token, secret()); + const shareToken = await parseShareToken(request as any); + + let user = null; + const { userId, authKey, grant } = payload || {}; + + if (userId) { + user = await getUser(userId); + } else if (redisEnabled && authKey) { + const redis = getClient(); + + const key = await redis.get(authKey); + + if (key?.userId) { + user = await getUser(key.userId); + } + } + + if (process.env.NODE_ENV === 'development') { + log('checkAuth:', { token, shareToken, payload, user, grant }); + } + + if (!user?.id && !shareToken) { + log('checkAuth: User not authorized'); + return null; + } + + if (user) { + user.isAdmin = user.role === ROLES.admin; + } + + return { + user, + grant, + token, + shareToken, + authKey, + }; +} + export async function saveAuth(data: any, expire = 0) { const authKey = `auth:${getRandomChars(32)}`; diff --git a/src/lib/request.ts b/src/lib/request.ts index 5e2be2fe..5eb1b477 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,10 +1,28 @@ -import { NextApiRequest } from 'next'; +import { ZodObject } from 'zod'; import { getAllowedUnits, getMinimumUnit } from './date'; import { getWebsiteDateRange } from '../queries'; import { FILTER_COLUMNS } from 'lib/constants'; -export async function getRequestDateRange(req: NextApiRequest) { - const { websiteId, startAt, endAt, unit } = req.query; +export async function getJsonBody(request: Request) { + try { + return await request.clone().json(); + } catch { + return null; + } +} + +export async function checkRequest(request: Request, schema: ZodObject) { + const url = new URL(request.url); + const query = Object.fromEntries(url.searchParams); + const body = await getJsonBody(request); + + const result = schema.safeParse(request.method === 'GET' ? query : body); + + return { query, body, error: result.error }; +} + +export async function getRequestDateRange(query: Record) { + const { websiteId, startAt, endAt, unit } = query; // All-time if (+startAt === 0 && +endAt === 1) { @@ -31,9 +49,9 @@ export async function getRequestDateRange(req: NextApiRequest) { }; } -export function getRequestFilters(req: NextApiRequest) { +export function getRequestFilters(query: Record) { return Object.keys(FILTER_COLUMNS).reduce((obj, key) => { - const value = req.query[key]; + const value = query[key]; if (value !== undefined) { obj[key] = value; diff --git a/src/lib/response.ts b/src/lib/response.ts new file mode 100644 index 00000000..da9e3f89 --- /dev/null +++ b/src/lib/response.ts @@ -0,0 +1,21 @@ +import { serializeError } from 'serialize-error'; + +export function ok() { + return Response.json({ ok: true }); +} + +export function json(data: any) { + return Response.json(data); +} + +export function badRequest(message?: any) { + return Response.json({ error: 'Bad request', message }, { status: 400 }); +} + +export function unauthorized() { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); +} + +export function serverError(error: any) { + return Response.json({ error: 'Server error', message: serializeError(error), status: 500 }); +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 5218af10..9153d0f9 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -1,4 +1,7 @@ +import { z } from 'zod'; import * as yup from 'yup'; +import { isValidTimezone } from 'lib/date'; +import { UNIT_TYPES } from './constants'; export const dateRange = { startAt: yup.number().integer().required(), @@ -11,3 +14,18 @@ export const pageInfo = { pageSize: yup.number().integer().positive().min(1).max(200), orderBy: yup.string(), }; + +export const pagingParams = { + page: z.coerce.number().int().positive(), + pageSize: z.coerce.number().int().positive(), + orderBy: z.string().optional(), + query: z.string().optional(), +}; + +export const timezone = z.string().refine(value => isValidTimezone(value), { + message: 'Invalid timezone', +}); + +export const unit = z.string().refine(value => UNIT_TYPES.includes(value), { + message: 'Invalid unit', +}); diff --git a/src/lib/types.ts b/src/lib/types.ts index d7a12068..70c2aae6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -25,7 +25,7 @@ export type KafkaTopic = ObjectValues; export type ReportType = ObjectValues; export interface PageParams { - query?: string; + search?: string; page?: number; pageSize?: number; orderBy?: string; @@ -43,7 +43,7 @@ export interface PageResult { export interface PagedQueryResult { result: PageResult; - query: any; + search: any; params: PageParams; setParams: Dispatch>; } diff --git a/src/pages/api/config.ts b/src/pages/api/_config.ts similarity index 100% rename from src/pages/api/config.ts rename to src/pages/api/_config.ts diff --git a/src/pages/api/version.ts b/src/pages/api/_version.ts similarity index 100% rename from src/pages/api/version.ts rename to src/pages/api/_version.ts diff --git a/src/pages/api/heartbeat.ts b/src/pages/api/heartbeat.ts deleted file mode 100644 index 1b515d39..00000000 --- a/src/pages/api/heartbeat.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok } from 'next-basics'; - -export default async (req: NextApiRequest, res: NextApiResponse) => { - return ok(res); -}; diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts index 3b88689d..e40e548c 100644 --- a/src/pages/api/me/teams.ts +++ b/src/pages/api/me/teams.ts @@ -3,7 +3,7 @@ import { NextApiRequestQueryBody } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; -import userTeamsRoute from 'pages/api/users/[userId]/teams'; +import userTeamsRoute from 'pages/api/users/[userId]/_teams'; import * as yup from 'yup'; const schema = { diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts index 48800f90..5c3030e6 100644 --- a/src/pages/api/me/websites.ts +++ b/src/pages/api/me/websites.ts @@ -3,7 +3,7 @@ import { NextApiRequestQueryBody } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; -import userWebsitesRoute from 'pages/api/users/[userId]/websites'; +import userWebsitesRoute from 'pages/api/users/[userId]/_websites'; import * as yup from 'yup'; const schema = { diff --git a/src/pages/api/users/[userId]/index.ts b/src/pages/api/users/[userId]/_index.ts similarity index 100% rename from src/pages/api/users/[userId]/index.ts rename to src/pages/api/users/[userId]/_index.ts diff --git a/src/pages/api/users/[userId]/teams.ts b/src/pages/api/users/[userId]/_teams.ts similarity index 100% rename from src/pages/api/users/[userId]/teams.ts rename to src/pages/api/users/[userId]/_teams.ts diff --git a/src/pages/api/users/[userId]/usage.ts b/src/pages/api/users/[userId]/_usage.ts similarity index 100% rename from src/pages/api/users/[userId]/usage.ts rename to src/pages/api/users/[userId]/_usage.ts diff --git a/src/pages/api/users/[userId]/websites.ts b/src/pages/api/users/[userId]/_websites.ts similarity index 100% rename from src/pages/api/users/[userId]/websites.ts rename to src/pages/api/users/[userId]/_websites.ts diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/_index.ts similarity index 100% rename from src/pages/api/users/index.ts rename to src/pages/api/users/_index.ts diff --git a/src/pages/api/websites/[websiteId]/active.ts b/src/pages/api/websites/[websiteId]/_active.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/active.ts rename to src/pages/api/websites/[websiteId]/_active.ts diff --git a/src/pages/api/websites/[websiteId]/daterange.ts b/src/pages/api/websites/[websiteId]/_daterange.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/daterange.ts rename to src/pages/api/websites/[websiteId]/_daterange.ts diff --git a/src/pages/api/websites/[websiteId]/index.ts b/src/pages/api/websites/[websiteId]/_index.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/index.ts rename to src/pages/api/websites/[websiteId]/_index.ts diff --git a/src/pages/api/websites/[websiteId]/metrics.ts b/src/pages/api/websites/[websiteId]/_metrics.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/metrics.ts rename to src/pages/api/websites/[websiteId]/_metrics.ts diff --git a/src/pages/api/websites/[websiteId]/pageviews.ts b/src/pages/api/websites/[websiteId]/_pageviews.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/pageviews.ts rename to src/pages/api/websites/[websiteId]/_pageviews.ts diff --git a/src/pages/api/websites/[websiteId]/reports.ts b/src/pages/api/websites/[websiteId]/_reports.ts similarity index 94% rename from src/pages/api/websites/[websiteId]/reports.ts rename to src/pages/api/websites/[websiteId]/_reports.ts index 72e5b0f2..86260634 100644 --- a/src/pages/api/websites/[websiteId]/reports.ts +++ b/src/pages/api/websites/[websiteId]/_reports.ts @@ -33,12 +33,12 @@ export default async ( return unauthorized(res); } - const { page, query, pageSize } = req.query; + const { page, search, pageSize } = req.query; const data = await getWebsiteReports(websiteId, { page, pageSize, - query, + search, }); return ok(res, data); diff --git a/src/pages/api/websites/[websiteId]/reset.ts b/src/pages/api/websites/[websiteId]/_reset.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/reset.ts rename to src/pages/api/websites/[websiteId]/_reset.ts diff --git a/src/pages/api/websites/[websiteId]/stats.ts b/src/pages/api/websites/[websiteId]/_stats.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/stats.ts rename to src/pages/api/websites/[websiteId]/_stats.ts diff --git a/src/pages/api/websites/[websiteId]/transfer.ts b/src/pages/api/websites/[websiteId]/_transfer.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/transfer.ts rename to src/pages/api/websites/[websiteId]/_transfer.ts diff --git a/src/pages/api/websites/[websiteId]/values.ts b/src/pages/api/websites/[websiteId]/_values.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/values.ts rename to src/pages/api/websites/[websiteId]/_values.ts diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/_index.ts similarity index 96% rename from src/pages/api/websites/index.ts rename to src/pages/api/websites/_index.ts index c5eb7200..483b77cd 100644 --- a/src/pages/api/websites/index.ts +++ b/src/pages/api/websites/_index.ts @@ -5,7 +5,7 @@ import { NextApiRequestQueryBody, PageParams } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; -import userWebsitesRoute from 'pages/api/users/[userId]/websites'; +import userWebsitesRoute from 'pages/api/users/[userId]/_websites'; import * as yup from 'yup'; import { pageInfo } from 'lib/schema'; diff --git a/src/queries/analytics/getActiveVisitors.ts b/src/queries/analytics/getActiveVisitors.ts index c59a265a..d5607e27 100644 --- a/src/queries/analytics/getActiveVisitors.ts +++ b/src/queries/analytics/getActiveVisitors.ts @@ -15,7 +15,7 @@ async function relationalQuery(websiteId: string) { const result = await rawQuery( ` - select count(distinct session_id) x + select count(distinct session_id) as visitors from website_event where website_id = {{websiteId::uuid}} and created_at >= {{startDate}} @@ -32,7 +32,7 @@ async function clickhouseQuery(websiteId: string): Promise<{ x: number }> { const result = await rawQuery( ` select - count(distinct session_id) x + count(distinct session_id) as "visitors" from website_event where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64} diff --git a/src/queries/analytics/pageviews/getPageviewMetrics.ts b/src/queries/analytics/pageviews/getPageviewMetrics.ts index f734b1dd..b356708e 100644 --- a/src/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/src/queries/analytics/pageviews/getPageviewMetrics.ts @@ -5,7 +5,13 @@ import prisma from 'lib/prisma'; import { QueryFilters } from 'lib/types'; export async function getPageviewMetrics( - ...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] + ...args: [ + websiteId: string, + type: string, + filters: QueryFilters, + limit?: number | string, + offset?: number | string, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -17,8 +23,8 @@ async function relationalQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ) { const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = prisma; @@ -80,8 +86,8 @@ async function clickhouseQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ): Promise<{ x: string; y: number }[]> { const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = clickhouse; diff --git a/src/queries/analytics/sessions/getSessionMetrics.ts b/src/queries/analytics/sessions/getSessionMetrics.ts index bb8bc4c5..0e8ebedf 100644 --- a/src/queries/analytics/sessions/getSessionMetrics.ts +++ b/src/queries/analytics/sessions/getSessionMetrics.ts @@ -5,7 +5,13 @@ import prisma from 'lib/prisma'; import { QueryFilters } from 'lib/types'; export async function getSessionMetrics( - ...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] + ...args: [ + websiteId: string, + type: string, + filters: QueryFilters, + limit?: number | string, + offset?: number | string, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -17,8 +23,8 @@ async function relationalQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ) { const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = prisma; @@ -60,8 +66,8 @@ async function clickhouseQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ): Promise<{ x: string; y: number }[]> { const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = clickhouse; diff --git a/src/queries/prisma/report.ts b/src/queries/prisma/report.ts index a0e6364c..51e7ddc2 100644 --- a/src/queries/prisma/report.ts +++ b/src/queries/prisma/report.ts @@ -19,11 +19,11 @@ export async function getReports( criteria: ReportFindManyArgs, pageParams: PageParams = {}, ): Promise> { - const { query } = pageParams; + const { search } = pageParams; const where: Prisma.ReportWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [ + ...prisma.getSearchParameters(search, [ { name: 'contains' }, { description: 'contains' }, { type: 'contains' }, diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index dc1ec438..1477a835 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -30,11 +30,11 @@ export async function getWebsites( criteria: WebsiteFindManyArgs, pageParams: PageParams, ): Promise> { - const { query } = pageParams; + const { search } = pageParams; const where: Prisma.WebsiteWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [ + ...prisma.getSearchParameters(search, [ { name: 'contains', }, diff --git a/yarn.lock b/yarn.lock index 8b364379..fb739910 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11210,6 +11210,11 @@ yup@^0.32.11: property-expr "^2.0.4" toposort "^2.0.2" +zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== + zustand@^4.5.5: version "4.5.6" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.6.tgz#6857d52af44874a79fb3408c9473f78367255c96" From 7d5556a6371b4fd3e298043098781a0c60f2c020 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 21 Jan 2025 20:57:47 -0800 Subject: [PATCH 02/17] Convert event-data, events, session-data, sessions routes. --- src/app/api/users/[userId]/route.ts | 2 + src/app/api/users/[userId]/teams/route.ts | 10 ++-- src/app/api/users/[userId]/usage/route.ts | 10 ++-- src/app/api/users/[userId]/websites/route.ts | 9 ++-- src/app/api/users/route.ts | 14 +++--- .../api/websites/[websiteId]/active/route.ts | 10 ++-- .../websites/[websiteId]/daterange/route.ts | 3 +- .../[websiteId]/event-data/events/route.ts | 42 ++++++++++++++++ .../[websiteId]/event-data/fields/route.ts | 40 +++++++++++++++ .../event-data/properties/route.ts | 38 ++++++++++++++ .../[websiteId]/event-data/stats/route.ts | 38 ++++++++++++++ .../[websiteId]/event-data/values/route.ts | 44 ++++++++++++++++ .../api/websites/[websiteId]/events/route.ts | 39 +++++++++++++++ .../[websiteId]/events/series/route.ts | 47 +++++++++++++++++ .../api/websites/[websiteId]/metrics/route.ts | 43 ++++++---------- .../websites/[websiteId]/pageviews/route.ts | 33 +++++------- .../api/websites/[websiteId]/reports/route.ts | 11 ++-- .../api/websites/[websiteId]/reset/route.ts | 3 +- src/app/api/websites/[websiteId]/route.ts | 23 +++------ .../session-data/properties/route.ts | 38 ++++++++++++++ .../[websiteId]/session-data/values/route.ts | 42 ++++++++++++++++ .../sessions/[sessionId]/activity/route.ts | 37 ++++++++++++++ .../sessions/[sessionId]/properties/route.ts | 20 ++++++++ .../[websiteId]/sessions/[sessionId]/route.ts | 20 ++++++++ .../websites/[websiteId]/sessions/route.ts | 39 +++++++++++++++ .../[websiteId]/sessions/stats/route.ts | 50 +++++++++++++++++++ .../[websiteId]/sessions/weekly/route.ts | 40 +++++++++++++++ .../api/websites/[websiteId]/stats/route.ts | 31 ++++-------- .../websites/[websiteId]/transfer/route.ts | 13 ++--- .../api/websites/[websiteId]/values/route.ts | 17 ++++--- src/lib/schema.ts | 21 +++++++- .../event-data/{events.ts => _events.ts} | 0 .../event-data/{fields.ts => _fields.ts} | 0 .../{properties.ts => _properties.ts} | 0 .../event-data/{stats.ts => _stats.ts} | 0 .../event-data/{values.ts => _values.ts} | 0 .../events/{index.ts => _index.ts} | 0 .../events/{series.ts => _series.ts} | 0 .../{properties.ts => _properties.ts} | 0 .../session-data/{values.ts => _values.ts} | 0 .../[sessionId]/{activity.ts => _activity.ts} | 0 .../[sessionId]/{index.ts => _index.ts} | 0 .../{properties.ts => _properties.ts} | 0 .../sessions/{index.ts => _index.ts} | 0 .../sessions/{stats.ts => _stats.ts} | 0 .../sessions/{weekly.ts => _weekly.ts} | 0 src/queries/index.ts | 1 + 47 files changed, 692 insertions(+), 136 deletions(-) create mode 100644 src/app/api/websites/[websiteId]/event-data/events/route.ts create mode 100644 src/app/api/websites/[websiteId]/event-data/fields/route.ts create mode 100644 src/app/api/websites/[websiteId]/event-data/properties/route.ts create mode 100644 src/app/api/websites/[websiteId]/event-data/stats/route.ts create mode 100644 src/app/api/websites/[websiteId]/event-data/values/route.ts create mode 100644 src/app/api/websites/[websiteId]/events/route.ts create mode 100644 src/app/api/websites/[websiteId]/events/series/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/properties/route.ts create mode 100644 src/app/api/websites/[websiteId]/session-data/values/route.ts create mode 100644 src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts create mode 100644 src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts create mode 100644 src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts create mode 100644 src/app/api/websites/[websiteId]/sessions/route.ts create mode 100644 src/app/api/websites/[websiteId]/sessions/stats/route.ts create mode 100644 src/app/api/websites/[websiteId]/sessions/weekly/route.ts rename src/pages/api/websites/[websiteId]/event-data/{events.ts => _events.ts} (100%) rename src/pages/api/websites/[websiteId]/event-data/{fields.ts => _fields.ts} (100%) rename src/pages/api/websites/[websiteId]/event-data/{properties.ts => _properties.ts} (100%) rename src/pages/api/websites/[websiteId]/event-data/{stats.ts => _stats.ts} (100%) rename src/pages/api/websites/[websiteId]/event-data/{values.ts => _values.ts} (100%) rename src/pages/api/websites/[websiteId]/events/{index.ts => _index.ts} (100%) rename src/pages/api/websites/[websiteId]/events/{series.ts => _series.ts} (100%) rename src/pages/api/websites/[websiteId]/session-data/{properties.ts => _properties.ts} (100%) rename src/pages/api/websites/[websiteId]/session-data/{values.ts => _values.ts} (100%) rename src/pages/api/websites/[websiteId]/sessions/[sessionId]/{activity.ts => _activity.ts} (100%) rename src/pages/api/websites/[websiteId]/sessions/[sessionId]/{index.ts => _index.ts} (100%) rename src/pages/api/websites/[websiteId]/sessions/[sessionId]/{properties.ts => _properties.ts} (100%) rename src/pages/api/websites/[websiteId]/sessions/{index.ts => _index.ts} (100%) rename src/pages/api/websites/[websiteId]/sessions/{stats.ts => _stats.ts} (100%) rename src/pages/api/websites/[websiteId]/sessions/{weekly.ts => _weekly.ts} (100%) diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index 41cd6bcf..30c166f1 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -7,6 +7,7 @@ import { checkRequest } from 'lib/request'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const { userId } = await params; + const auth = await checkAuth(request); if (!auth || !(await canViewUser(auth, userId))) { @@ -32,6 +33,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ use } const { userId } = await params; + const auth = await checkAuth(request); if (!auth || !(await canUpdateUser(auth, userId))) { diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts index 0cdccdaf..83238799 100644 --- a/src/app/api/users/[userId]/teams/route.ts +++ b/src/app/api/users/[userId]/teams/route.ts @@ -5,12 +5,10 @@ import { checkAuth } from 'lib/auth'; import { unauthorized, badRequest, json } from 'lib/response'; import { checkRequest } from 'lib/request'; -const schema = z.object({ - ...pagingParams, -}); - export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { - const { userId } = await params; + const schema = z.object({ + ...pagingParams, + }); const { query, error } = await checkRequest(request, schema); @@ -18,6 +16,8 @@ export async function GET(request: Request, { params }: { params: Promise<{ user return badRequest(error); } + const { userId } = await params; + const auth = await checkAuth(request); if (!auth || (!auth.user.isAdmin && (!userId || auth.user.id !== userId))) { diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts index 177d3c35..275f665f 100644 --- a/src/app/api/users/[userId]/usage/route.ts +++ b/src/app/api/users/[userId]/usage/route.ts @@ -6,12 +6,12 @@ import { getEventDataUsage } from 'queries/analytics/events/getEventDataUsage'; import { checkAuth } from 'lib/auth'; import { checkRequest } from 'lib/request'; -const schema = z.object({ - startAt: z.coerce.number(), - endAt: z.coerce.number(), -}); - export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + const { query, error } = await checkRequest(request, schema); if (error) { diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts index 61783cd6..189bf8fa 100644 --- a/src/app/api/users/[userId]/websites/route.ts +++ b/src/app/api/users/[userId]/websites/route.ts @@ -5,11 +5,11 @@ import { pagingParams } from 'lib/schema'; import { checkRequest } from 'lib/request'; import { checkAuth } from 'lib/auth'; -const schema = z.object({ - ...pagingParams, -}); - export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + const { query, error } = await checkRequest(request, schema); if (error) { @@ -17,6 +17,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user } const { userId } = await params; + const auth = await checkAuth(request); if (!auth || (!auth.user.isAdmin && auth.user.id !== userId)) { diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 870e6181..87959a0c 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -7,14 +7,14 @@ import { checkRequest } from 'lib/request'; import { unauthorized, json, badRequest } from 'lib/response'; import { createUser, getUserByUsername } from 'queries'; -const schema = z.object({ - username: z.string().max(255), - password: z.string(), - id: z.string().uuid(), - role: z.string().regex(/admin|user|view-only/i), -}); - export async function POST(request: Request) { + const schema = z.object({ + username: z.string().max(255), + password: z.string(), + id: z.string().uuid(), + role: z.string().regex(/admin|user|view-only/i), + }); + const { body, error } = await checkRequest(request, schema); if (error) { diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts index 22bd1999..569bdb7b 100644 --- a/src/app/api/websites/[websiteId]/active/route.ts +++ b/src/app/api/websites/[websiteId]/active/route.ts @@ -6,15 +6,11 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const auth = await checkAuth(request); - - if (!auth) { - return unauthorized(); - } - const { websiteId } = await params; - if (!(await canViewWebsite(auth, websiteId))) { + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts index 70460bd6..d4a562de 100644 --- a/src/app/api/websites/[websiteId]/daterange/route.ts +++ b/src/app/api/websites/[websiteId]/daterange/route.ts @@ -6,9 +6,10 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const auth = await checkAuth(request); const { websiteId } = await params; + const auth = await checkAuth(request); + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts new file mode 100644 index 00000000..143fae18 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getEventDataEvents } from 'queries/analytics/events/getEventDataEvents'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + event: z.string().optional(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt, event } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataEvents(websiteId, { + startDate, + endDate, + event, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts new file mode 100644 index 00000000..3ef2f3b1 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getEventDataFields } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + const auth = await checkAuth(request); + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataFields(websiteId, { + startDate, + endDate, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts new file mode 100644 index 00000000..68fdf4e3 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getEventDataProperties } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt, propertyName } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts new file mode 100644 index 00000000..d958bbdc --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getEventDataStats } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataStats(websiteId, { startDate, endDate }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts new file mode 100644 index 00000000..0ecf20d3 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getEventDataValues } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + eventName: z.string().optional(), + propertyName: z.string().optional(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt, eventName, propertyName } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataValues(websiteId, { + startDate, + endDate, + eventName, + propertyName, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts new file mode 100644 index 00000000..ef929312 --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { pagingParams } from 'lib/schema'; +import { getWebsiteEvents } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...pagingParams, + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteEvents(websiteId, { startDate, endDate }, query); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts new file mode 100644 index 00000000..4551e3bf --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import { checkRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { filterParams, timezoneParam, unitParam } from 'lib/schema'; +import { getEventMetrics } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + unit: unitParam, + timezone: timezoneParam, + ...filterParams, + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { timezone } = query; + const { startDate, endDate, unit } = await getRequestDateRange(request); + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = { + ...getRequestFilters(request), + startDate, + endDate, + timezone, + unit, + }; + + const data = await getEventMetrics(websiteId, filters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index 3edc0d88..3842c683 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -1,49 +1,36 @@ +import { z } from 'zod'; import { canViewWebsite, checkAuth } from 'lib/auth'; import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants'; import { getRequestFilters, getRequestDateRange, checkRequest } from 'lib/request'; -import { getPageviewMetrics, getSessionMetrics } from 'queries'; - -import { z } from 'zod'; import { json, unauthorized, badRequest } from 'lib/response'; - -const schema = z.object({ - type: z.string(), - startAt: z.coerce.number(), - endAt: z.coerce.number(), - // optional - url: z.string().optional(), - referrer: z.string().optional(), - title: z.string().optional(), - query: z.string().optional(), - host: z.string().optional(), - os: z.string().optional(), - browser: z.string().optional(), - device: z.string().optional(), - country: z.string().optional(), - region: z.string().optional(), - city: z.string().optional(), - language: z.string().optional(), - event: z.string().optional(), - limit: z.coerce.number().optional(), - offset: z.coerce.number().optional(), - search: z.string().optional(), - tag: z.string().optional(), -}); +import { getPageviewMetrics, getSessionMetrics } from 'queries'; +import { filterParams } from 'lib/schema'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const schema = z.object({ + type: z.string(), + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + search: z.string().optional(), + ...filterParams, + }); + const { query, error } = await checkRequest(request, schema); if (error) { return badRequest(error); } - const auth = await checkAuth(request); const { websiteId } = await params; const { type, limit, offset, search } = query; + const auth = await checkAuth(request); + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts index 7ba5b100..e9e6f32b 100644 --- a/src/app/api/websites/[websiteId]/pageviews/route.ts +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -1,44 +1,35 @@ import { z } from 'zod'; import { canViewWebsite, checkAuth } from 'lib/auth'; import { getRequestFilters, getRequestDateRange, checkRequest } from 'lib/request'; -import { unit, timezone } from 'lib/schema'; +import { unitParam, timezoneParam, filterParams } from 'lib/schema'; import { getCompareDate } from 'lib/date'; import { badRequest, unauthorized, json } from 'lib/response'; import { getPageviewStats, getSessionStats } from 'queries'; -const schema = z.object({ - startAt: z.coerce.number(), - endAt: z.coerce.number(), - unit, - timezone, - url: z.string().optional(), - referrer: z.string().optional(), - title: z.string().optional(), - host: z.string().optional(), - os: z.string().optional(), - browser: z.string().optional(), - device: z.string().optional(), - country: z.string().optional(), - region: z.string().optional(), - city: z.string().optional(), - tag: z.string().optional(), - compare: z.string().optional(), -}); - export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + unit: unitParam, + timezone: timezoneParam, + compare: z.string().optional(), + ...filterParams, + }); + const { query, error } = await checkRequest(request, schema); if (error) { return badRequest(error); } - const auth = await checkAuth(request); const { websiteId } = await params; const { timezone, compare } = query; + const auth = await checkAuth(request); + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts index f4fc641f..0098fa15 100644 --- a/src/app/api/websites/[websiteId]/reports/route.ts +++ b/src/app/api/websites/[websiteId]/reports/route.ts @@ -5,24 +5,25 @@ import { pagingParams } from 'lib/schema'; import { checkRequest } from 'lib/request'; import { badRequest, unauthorized, json } from 'lib/response'; -const schema = z.object({ - ...pagingParams, -}); - export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const schema = z.object({ + ...pagingParams, + }); + const { query, error } = await checkRequest(request, schema); if (error) { return badRequest(error); } - const auth = await checkAuth(request); const { websiteId } = await params; const { page, pageSize, search } = query; + const auth = await checkAuth(request); + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts index ae2131e8..bfbd11a8 100644 --- a/src/app/api/websites/[websiteId]/reset/route.ts +++ b/src/app/api/websites/[websiteId]/reset/route.ts @@ -6,9 +6,10 @@ export async function POST( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const auth = await checkAuth(request); const { websiteId } = await params; + const auth = await checkAuth(request); + if (!auth || !(await canUpdateWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts index 02bb00f8..e8ad8a0b 100644 --- a/src/app/api/websites/[websiteId]/route.ts +++ b/src/app/api/websites/[websiteId]/route.ts @@ -9,15 +9,11 @@ export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const auth = await checkAuth(request); - - if (!auth) { - return unauthorized(); - } - const { websiteId } = await params; - if (!(await canViewWebsite(auth, websiteId))) { + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } @@ -42,10 +38,11 @@ export async function POST( return badRequest(error); } - const auth = await checkAuth(request); const { websiteId } = await params; const { name, domain, shareId } = body; + const auth = await checkAuth(request); + if (!auth || !(await canUpdateWebsite(auth, websiteId))) { return unauthorized(); } @@ -67,15 +64,11 @@ export async function DELETE( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { - const auth = await checkAuth(request); - - if (!auth) { - return unauthorized(); - } - const { websiteId } = await params; - if (!(await canDeleteWebsite(auth, websiteId))) { + const auth = await checkAuth(request); + + if (!auth || !(await canDeleteWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts new file mode 100644 index 00000000..af168f84 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getSessionDataProperties } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { startAt, endAt, propertyName } = query; + const { websiteId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts new file mode 100644 index 00000000..627298af --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getEventDataEvents } from 'queries/analytics/events/getEventDataEvents'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { startAt, endAt, event } = query; + const { websiteId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataEvents(websiteId, { + startDate, + endDate, + event, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts new file mode 100644 index 00000000..48123ffe --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getSessionActivity } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId, sessionId } = await params; + const { startAt, endAt } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts new file mode 100644 index 00000000..7c5863e8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts @@ -0,0 +1,20 @@ +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getSessionData } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const { websiteId, sessionId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getSessionData(websiteId, sessionId); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts new file mode 100644 index 00000000..6822aaa0 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts @@ -0,0 +1,20 @@ +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { getWebsiteSession } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const { websiteId, sessionId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getWebsiteSession(websiteId, sessionId); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts new file mode 100644 index 00000000..c96a8ddb --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { pagingParams } from 'lib/schema'; +import { getWebsiteSessions } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...pagingParams, + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteSessions(websiteId, { startDate, endDate }, query); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts new file mode 100644 index 00000000..35c17021 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { checkRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { filterParams } from 'lib/schema'; +import { getWebsiteSessionStats } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...filterParams, + }); + + const { error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(request); + + const filters = getRequestFilters(request); + + const metrics = await getWebsiteSessionStats(websiteId, { + ...filters, + startDate, + endDate, + }); + + const data = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = { + value: Number(metrics[0][key]) || 0, + }; + return obj; + }, {}); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts new file mode 100644 index 00000000..05cc9cad --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { checkRequest } from 'lib/request'; +import { badRequest, unauthorized, json } from 'lib/response'; +import { canViewWebsite, checkAuth } from 'lib/auth'; +import { pagingParams, timezoneParam } from 'lib/schema'; +import { getWebsiteSessionsWeekly } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + timezone: timezoneParam, + ...pagingParams, + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { websiteId } = await params; + const { startAt, endAt, timezone } = query; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts index 1c96a74f..76d9f9a6 100644 --- a/src/app/api/websites/[websiteId]/stats/route.ts +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -3,42 +3,31 @@ import { checkRequest, getRequestDateRange, getRequestFilters } from 'lib/reques import { badRequest, unauthorized, json } from 'lib/response'; import { checkAuth, canViewWebsite } from 'lib/auth'; import { getCompareDate } from 'lib/date'; +import { filterParams } from 'lib/schema'; import { getWebsiteStats } from 'queries'; -const schema = z.object({ - startAt: z.coerce.number(), - endAt: z.coerce.number(), - // optional - url: z.string().optional(), - referrer: z.string().optional(), - title: z.string().optional(), - query: z.string().optional(), - event: z.string().optional(), - host: z.string().optional(), - os: z.string().optional(), - browser: z.string().optional(), - device: z.string().optional(), - country: z.string().optional(), - region: z.string().optional(), - city: z.string().optional(), - tag: z.string().optional(), - compare: z.string().optional(), -}); - export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + compare: z.string().optional(), + ...filterParams, + }); + const { query, error } = await checkRequest(request, schema); if (error) { return badRequest(error); } - const auth = await checkAuth(request); const { websiteId } = await params; const { compare } = query; + const auth = await checkAuth(request); + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts index d97fe0f8..8771ecc2 100644 --- a/src/app/api/websites/[websiteId]/transfer/route.ts +++ b/src/app/api/websites/[websiteId]/transfer/route.ts @@ -4,25 +4,26 @@ import { updateWebsite } from 'queries'; import { checkRequest } from 'lib/request'; import { badRequest, unauthorized, json } from 'lib/response'; -const schema = z.object({ - userId: z.string().uuid().optional(), - teamId: z.string().uuid().optional(), -}); - export async function POST( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const schema = z.object({ + userId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), + }); + const { body, error } = await checkRequest(request, schema); if (error) { return badRequest(error); } - const auth = await checkAuth(request); const { websiteId } = await params; const { userId, teamId } = body; + const auth = await checkAuth(request); + if (!auth) { return unauthorized(); } else if (userId) { diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts index 1a4967b8..fe4edfbb 100644 --- a/src/app/api/websites/[websiteId]/values/route.ts +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -5,28 +5,29 @@ import { getValues } from 'queries'; import { checkRequest, getRequestDateRange } from 'lib/request'; import { badRequest, json, unauthorized } from 'lib/response'; -const schema = z.object({ - type: z.string(), - startAt: z.coerce.number(), - endAt: z.coerce.number(), - search: z.string().optional(), -}); - export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const schema = z.object({ + type: z.string(), + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + search: z.string().optional(), + }); + const { query, error } = await checkRequest(request, schema); if (error) { return badRequest(error); } - const auth = await checkAuth(request); const { websiteId } = await params; const { type, search } = query; const { startDate, endDate } = await getRequestDateRange(request); + const auth = await checkAuth(request); + if (!auth || !(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 9153d0f9..c39e47fd 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -22,10 +22,27 @@ export const pagingParams = { query: z.string().optional(), }; -export const timezone = z.string().refine(value => isValidTimezone(value), { +export const timezoneParam = z.string().refine(value => isValidTimezone(value), { message: 'Invalid timezone', }); -export const unit = z.string().refine(value => UNIT_TYPES.includes(value), { +export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), { message: 'Invalid unit', }); + +export const filterParams = { + url: z.string().optional(), + referrer: z.string().optional(), + title: z.string().optional(), + query: z.string().optional(), + os: z.string().optional(), + browser: z.string().optional(), + device: z.string().optional(), + country: z.string().optional(), + region: z.string().optional(), + city: z.string().optional(), + tag: z.string().optional(), + host: z.string().optional(), + language: z.string().optional(), + event: z.string().optional(), +}; diff --git a/src/pages/api/websites/[websiteId]/event-data/events.ts b/src/pages/api/websites/[websiteId]/event-data/_events.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/event-data/events.ts rename to src/pages/api/websites/[websiteId]/event-data/_events.ts diff --git a/src/pages/api/websites/[websiteId]/event-data/fields.ts b/src/pages/api/websites/[websiteId]/event-data/_fields.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/event-data/fields.ts rename to src/pages/api/websites/[websiteId]/event-data/_fields.ts diff --git a/src/pages/api/websites/[websiteId]/event-data/properties.ts b/src/pages/api/websites/[websiteId]/event-data/_properties.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/event-data/properties.ts rename to src/pages/api/websites/[websiteId]/event-data/_properties.ts diff --git a/src/pages/api/websites/[websiteId]/event-data/stats.ts b/src/pages/api/websites/[websiteId]/event-data/_stats.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/event-data/stats.ts rename to src/pages/api/websites/[websiteId]/event-data/_stats.ts diff --git a/src/pages/api/websites/[websiteId]/event-data/values.ts b/src/pages/api/websites/[websiteId]/event-data/_values.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/event-data/values.ts rename to src/pages/api/websites/[websiteId]/event-data/_values.ts diff --git a/src/pages/api/websites/[websiteId]/events/index.ts b/src/pages/api/websites/[websiteId]/events/_index.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/events/index.ts rename to src/pages/api/websites/[websiteId]/events/_index.ts diff --git a/src/pages/api/websites/[websiteId]/events/series.ts b/src/pages/api/websites/[websiteId]/events/_series.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/events/series.ts rename to src/pages/api/websites/[websiteId]/events/_series.ts diff --git a/src/pages/api/websites/[websiteId]/session-data/properties.ts b/src/pages/api/websites/[websiteId]/session-data/_properties.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/session-data/properties.ts rename to src/pages/api/websites/[websiteId]/session-data/_properties.ts diff --git a/src/pages/api/websites/[websiteId]/session-data/values.ts b/src/pages/api/websites/[websiteId]/session-data/_values.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/session-data/values.ts rename to src/pages/api/websites/[websiteId]/session-data/_values.ts diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts rename to src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts rename to src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts rename to src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts diff --git a/src/pages/api/websites/[websiteId]/sessions/index.ts b/src/pages/api/websites/[websiteId]/sessions/_index.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/sessions/index.ts rename to src/pages/api/websites/[websiteId]/sessions/_index.ts diff --git a/src/pages/api/websites/[websiteId]/sessions/stats.ts b/src/pages/api/websites/[websiteId]/sessions/_stats.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/sessions/stats.ts rename to src/pages/api/websites/[websiteId]/sessions/_stats.ts diff --git a/src/pages/api/websites/[websiteId]/sessions/weekly.ts b/src/pages/api/websites/[websiteId]/sessions/_weekly.ts similarity index 100% rename from src/pages/api/websites/[websiteId]/sessions/weekly.ts rename to src/pages/api/websites/[websiteId]/sessions/_weekly.ts diff --git a/src/queries/index.ts b/src/queries/index.ts index 8c7e564a..63cebb45 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -27,6 +27,7 @@ export * from './analytics/sessions/getSessionDataProperties'; export * from './analytics/sessions/getSessionDataValues'; export * from './analytics/sessions/getSessionMetrics'; export * from './analytics/sessions/getWebsiteSessions'; +export * from './analytics/sessions/getWebsiteSessionStats'; export * from './analytics/sessions/getWebsiteSessionsWeekly'; export * from './analytics/sessions/getSessionActivity'; export * from './analytics/sessions/getSessionStats'; From e51f182403878c48359d11df12b2eaee41c0078f Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 24 Jan 2025 23:34:02 -0800 Subject: [PATCH 03/17] Convert teams api routes. --- src/app/api/teams/[teamId]/route.ts | 75 +++++++++++++++++ .../teams/[teamId]/users/[userId]/route.ts | 72 ++++++++++++++++ src/app/api/teams/[teamId]/users/route.ts | 83 +++++++++++++++++++ src/app/api/teams/[teamId]/websites/route.ts | 30 +++++++ src/app/api/teams/join/route.ts | 46 ++++++++++ src/app/api/teams/route.ts | 38 +++++++++ .../hooks/queries/useWebsitePageviews.ts | 2 +- src/lib/response.ts | 10 ++- src/lib/schema.ts | 2 + .../teams/[teamId]/{index.ts => _index.ts} | 0 .../users/{[userId].ts => _[userId].ts} | 0 .../[teamId]/users/{index.ts => _index.ts} | 0 .../[teamId]/websites/{index.ts => _index.ts} | 0 src/pages/api/teams/{index.ts => _index.ts} | 0 src/pages/api/teams/{join.ts => _join.ts} | 0 15 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 src/app/api/teams/[teamId]/route.ts create mode 100644 src/app/api/teams/[teamId]/users/[userId]/route.ts create mode 100644 src/app/api/teams/[teamId]/users/route.ts create mode 100644 src/app/api/teams/[teamId]/websites/route.ts create mode 100644 src/app/api/teams/join/route.ts create mode 100644 src/app/api/teams/route.ts rename src/pages/api/teams/[teamId]/{index.ts => _index.ts} (100%) rename src/pages/api/teams/[teamId]/users/{[userId].ts => _[userId].ts} (100%) rename src/pages/api/teams/[teamId]/users/{index.ts => _index.ts} (100%) rename src/pages/api/teams/[teamId]/websites/{index.ts => _index.ts} (100%) rename src/pages/api/teams/{index.ts => _index.ts} (100%) rename src/pages/api/teams/{join.ts => _join.ts} (100%) diff --git a/src/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts new file mode 100644 index 00000000..7348e3c4 --- /dev/null +++ b/src/app/api/teams/[teamId]/route.ts @@ -0,0 +1,75 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest, notFound, ok } from 'lib/response'; +import { canDeleteTeam, canUpdateTeam, canViewTeam, checkAuth } from 'lib/auth'; +import { checkRequest } from 'lib/request'; +import { deleteTeam, getTeam, updateTeam } from 'queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + teamId: z.string().uuid(), + }); + + const { error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { teamId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewTeam(auth, teamId))) { + return unauthorized(); + } + + const team = await getTeam(teamId, { includeMembers: true }); + + if (!team) { + return notFound('Team not found.'); + } + + return json(team); +} + +export async function POST(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + name: z.string().max(50), + accessCode: z.string().max(50), + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { teamId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const team = await updateTeam(teamId, body); + + return json(team); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ teamId: string }> }, +) { + const { teamId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canDeleteTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + await deleteTeam(teamId); + + return ok(); +} diff --git a/src/app/api/teams/[teamId]/users/[userId]/route.ts b/src/app/api/teams/[teamId]/users/[userId]/route.ts new file mode 100644 index 00000000..c0a7f11f --- /dev/null +++ b/src/app/api/teams/[teamId]/users/[userId]/route.ts @@ -0,0 +1,72 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest, ok } from 'lib/response'; +import { canDeleteTeam, canUpdateTeam, checkAuth } from 'lib/auth'; +import { checkRequest } from 'lib/request'; +import { deleteTeam, getTeamUser, updateTeamUser } from 'queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const { teamId, userId } = await params; + + const auth = await checkAuth(request); + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const teamUser = await getTeamUser(teamId, userId); + + return json(teamUser); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const schema = z.object({ + role: z.string().regex(/team-member|team-view-only|team-manager/), + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { teamId, userId } = await params; + + const auth = await checkAuth(request); + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const teamUser = await getTeamUser(teamId, userId); + + if (!teamUser) { + return badRequest('The User does not exists on this team.'); + } + + const user = await updateTeamUser(teamUser.id, body); + + return json(user); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ teamId: string }> }, +) { + const { teamId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canDeleteTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + await deleteTeam(teamId); + + return ok(); +} diff --git a/src/app/api/teams/[teamId]/users/route.ts b/src/app/api/teams/[teamId]/users/route.ts new file mode 100644 index 00000000..3b7f9558 --- /dev/null +++ b/src/app/api/teams/[teamId]/users/route.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest } from 'lib/response'; +import { canAddUserToTeam, canUpdateTeam, checkAuth } from 'lib/auth'; +import { checkRequest } from 'lib/request'; +import { pagingParams, roleParam } from 'lib/schema'; +import { createTeamUser, getTeamUser, getTeamUsers } from 'queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { teamId } = await params; + + const auth = await checkAuth(request); + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const users = await getTeamUsers( + { + where: { + teamId, + user: { + deletedAt: null, + }, + }, + include: { + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, + query, + ); + + return json(users); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const schema = z.object({ + role: roleParam, + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { teamId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canAddUserToTeam(auth))) { + return unauthorized(); + } + + const { userId, role } = body; + + const teamUser = await getTeamUser(teamId, userId); + + if (teamUser) { + return badRequest('User is already a member of the Team.'); + } + + const users = await createTeamUser(userId, teamId, role); + + return json(users); +} diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts new file mode 100644 index 00000000..1d06b3c8 --- /dev/null +++ b/src/app/api/teams/[teamId]/websites/route.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest } from 'lib/response'; +import { canViewTeam, checkAuth } from 'lib/auth'; +import { checkRequest } from 'lib/request'; +import { pagingParams } from 'lib/schema'; +import { getTeamWebsites } from 'queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { query, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const { teamId } = await params; + + const auth = await checkAuth(request); + + if (!auth || !(await canViewTeam(auth, teamId))) { + return unauthorized(); + } + + const websites = await getTeamWebsites(teamId, query); + + return json(websites); +} diff --git a/src/app/api/teams/join/route.ts b/src/app/api/teams/join/route.ts new file mode 100644 index 00000000..c7bff019 --- /dev/null +++ b/src/app/api/teams/join/route.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest, notFound } from 'lib/response'; +import { canCreateTeam, checkAuth } from 'lib/auth'; +import { checkRequest } from 'lib/request'; +import { ROLES } from 'lib/constants'; +import { createTeamUser, findTeam, getTeamUser } from 'queries'; + +export async function POST(request: Request) { + const schema = z.object({ + accessCode: z.string().max(50), + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + + if (!auth || !(await canCreateTeam(auth))) { + return unauthorized(); + } + + const { accessCode } = body; + + const team = await findTeam({ + where: { + accessCode, + }, + }); + + if (!team) { + return notFound('Team not found.'); + } + + const teamUser = await getTeamUser(team.id, auth.user.id); + + if (teamUser) { + return badRequest('User is already a team member.'); + } + + const user = await createTeamUser(auth.user.id, team.id, ROLES.teamMember); + + return json(user); +} diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts new file mode 100644 index 00000000..1c097e8e --- /dev/null +++ b/src/app/api/teams/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { getRandomChars } from 'next-basics'; +import { unauthorized, json, badRequest } from 'lib/response'; +import { canCreateTeam, checkAuth } from 'lib/auth'; +import { uuid } from 'lib/crypto'; +import { checkRequest } from 'lib/request'; +import { createTeam } from 'queries'; + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(50), + }); + + const { body, error } = await checkRequest(request, schema); + + if (error) { + return badRequest(error); + } + + const auth = await checkAuth(request); + + if (!auth || !(await canCreateTeam(auth))) { + return unauthorized(); + } + + const { name } = body; + + const team = await createTeam( + { + id: uuid(), + name, + accessCode: `team_${getRandomChars(16)}`, + }, + auth.user.userId, + ); + + return json(team); +} diff --git a/src/components/hooks/queries/useWebsitePageviews.ts b/src/components/hooks/queries/useWebsitePageviews.ts index 42fb527e..43c51745 100644 --- a/src/components/hooks/queries/useWebsitePageviews.ts +++ b/src/components/hooks/queries/useWebsitePageviews.ts @@ -1,6 +1,6 @@ import { UseQueryOptions } from '@tanstack/react-query'; import { useApi } from '../useApi'; -import { useFilterParams } from '..//useFilterParams'; +import { useFilterParams } from '../useFilterParams'; export function useWebsitePageviews( websiteId: string, diff --git a/src/lib/response.ts b/src/lib/response.ts index da9e3f89..7ed0316e 100644 --- a/src/lib/response.ts +++ b/src/lib/response.ts @@ -12,10 +12,14 @@ export function badRequest(message?: any) { return Response.json({ error: 'Bad request', message }, { status: 400 }); } -export function unauthorized() { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); +export function notFound(message?: any) { + return Response.json({ error: 'Not found', message, status: 404 }); } -export function serverError(error: any) { +export function unauthorized(message?: any) { + return Response.json({ error: 'Unauthorized', message }, { status: 401 }); +} + +export function serverError(error?: any) { return Response.json({ error: 'Server error', message: serializeError(error), status: 500 }); } diff --git a/src/lib/schema.ts b/src/lib/schema.ts index c39e47fd..5f81b1f1 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -30,6 +30,8 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), message: 'Invalid unit', }); +export const roleParam = z.string().regex(/team-member|team-view-only|team-manager/); + export const filterParams = { url: z.string().optional(), referrer: z.string().optional(), diff --git a/src/pages/api/teams/[teamId]/index.ts b/src/pages/api/teams/[teamId]/_index.ts similarity index 100% rename from src/pages/api/teams/[teamId]/index.ts rename to src/pages/api/teams/[teamId]/_index.ts diff --git a/src/pages/api/teams/[teamId]/users/[userId].ts b/src/pages/api/teams/[teamId]/users/_[userId].ts similarity index 100% rename from src/pages/api/teams/[teamId]/users/[userId].ts rename to src/pages/api/teams/[teamId]/users/_[userId].ts diff --git a/src/pages/api/teams/[teamId]/users/index.ts b/src/pages/api/teams/[teamId]/users/_index.ts similarity index 100% rename from src/pages/api/teams/[teamId]/users/index.ts rename to src/pages/api/teams/[teamId]/users/_index.ts diff --git a/src/pages/api/teams/[teamId]/websites/index.ts b/src/pages/api/teams/[teamId]/websites/_index.ts similarity index 100% rename from src/pages/api/teams/[teamId]/websites/index.ts rename to src/pages/api/teams/[teamId]/websites/_index.ts diff --git a/src/pages/api/teams/index.ts b/src/pages/api/teams/_index.ts similarity index 100% rename from src/pages/api/teams/index.ts rename to src/pages/api/teams/_index.ts diff --git a/src/pages/api/teams/join.ts b/src/pages/api/teams/_join.ts similarity index 100% rename from src/pages/api/teams/join.ts rename to src/pages/api/teams/_join.ts From 2d6428172b53f8cba34775bf166b1e846b079a3d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 25 Jan 2025 00:19:22 -0800 Subject: [PATCH 04/17] Merged auth into new parseRequest method. --- src/app/api/teams/[teamId]/route.ts | 32 ++++++++--------- .../teams/[teamId]/users/[userId]/route.ts | 28 +++++++++------ src/app/api/teams/[teamId]/users/route.ts | 18 ++++------ src/app/api/teams/[teamId]/websites/route.ts | 18 ++++------ src/app/api/teams/join/route.ts | 12 +++---- src/app/api/teams/route.ts | 14 ++++---- src/app/api/users/[userId]/route.ts | 22 ++++++------ src/app/api/users/[userId]/teams/route.ts | 13 +++---- src/app/api/users/[userId]/usage/route.ts | 13 +++---- src/app/api/users/[userId]/websites/route.ts | 13 +++---- src/app/api/users/route.ts | 12 +++---- .../api/websites/[websiteId]/active/route.ts | 13 ++++--- .../websites/[websiteId]/daterange/route.ts | 13 ++++--- .../[websiteId]/event-data/events/route.ts | 15 ++++---- .../[websiteId]/event-data/fields/route.ts | 12 +++---- .../event-data/properties/route.ts | 14 ++++---- .../[websiteId]/event-data/stats/route.ts | 14 ++++---- .../[websiteId]/event-data/values/route.ts | 14 ++++---- .../api/websites/[websiteId]/events/route.ts | 14 ++++---- .../[websiteId]/events/series/route.ts | 14 ++++---- .../api/websites/[websiteId]/metrics/route.ts | 12 +++---- .../websites/[websiteId]/pageviews/route.ts | 14 ++++---- .../api/websites/[websiteId]/reports/route.ts | 14 ++++---- .../api/websites/[websiteId]/reset/route.ts | 13 ++++--- src/app/api/websites/[websiteId]/route.ts | 34 +++++++++++-------- .../session-data/properties/route.ts | 14 ++++---- .../[websiteId]/session-data/values/route.ts | 14 ++++---- .../sessions/[sessionId]/activity/route.ts | 14 ++++---- .../sessions/[sessionId]/properties/route.ts | 13 ++++--- .../[websiteId]/sessions/[sessionId]/route.ts | 13 ++++--- .../websites/[websiteId]/sessions/route.ts | 14 ++++---- .../[websiteId]/sessions/stats/route.ts | 14 ++++---- .../[websiteId]/sessions/weekly/route.ts | 14 ++++---- .../api/websites/[websiteId]/stats/route.ts | 14 ++++---- .../websites/[websiteId]/transfer/route.ts | 16 ++++----- .../api/websites/[websiteId]/values/route.ts | 14 ++++---- src/app/api/websites/route.ts | 26 ++++---------- src/lib/request.ts | 23 ++++++++++--- src/lib/schema.ts | 4 +-- 39 files changed, 296 insertions(+), 316 deletions(-) diff --git a/src/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts index 7348e3c4..8ea41442 100644 --- a/src/app/api/teams/[teamId]/route.ts +++ b/src/app/api/teams/[teamId]/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { unauthorized, json, badRequest, notFound, ok } from 'lib/response'; -import { canDeleteTeam, canUpdateTeam, canViewTeam, checkAuth } from 'lib/auth'; -import { checkRequest } from 'lib/request'; +import { unauthorized, json, notFound, ok } from 'lib/response'; +import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; +import { parseRequest } from 'lib/request'; import { deleteTeam, getTeam, updateTeam } from 'queries'; export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { @@ -9,17 +9,15 @@ export async function GET(request: Request, { params }: { params: Promise<{ team teamId: z.string().uuid(), }); - const { error } = await checkRequest(request, schema); + const { auth, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { teamId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewTeam(auth, teamId))) { + if (!(await canViewTeam(auth, teamId))) { return unauthorized(); } @@ -38,17 +36,15 @@ export async function POST(request: Request, { params }: { params: Promise<{ tea accessCode: z.string().max(50), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { teamId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canUpdateTeam(auth, teamId))) { + if (!(await canUpdateTeam(auth, teamId))) { return unauthorized('You must be the owner of this team.'); } @@ -61,11 +57,15 @@ export async function DELETE( request: Request, { params }: { params: Promise<{ teamId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { teamId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canDeleteTeam(auth, teamId))) { + if (!(await canDeleteTeam(auth, teamId))) { return unauthorized('You must be the owner of this team.'); } diff --git a/src/app/api/teams/[teamId]/users/[userId]/route.ts b/src/app/api/teams/[teamId]/users/[userId]/route.ts index c0a7f11f..9276c194 100644 --- a/src/app/api/teams/[teamId]/users/[userId]/route.ts +++ b/src/app/api/teams/[teamId]/users/[userId]/route.ts @@ -1,16 +1,20 @@ import { z } from 'zod'; import { unauthorized, json, badRequest, ok } from 'lib/response'; -import { canDeleteTeam, canUpdateTeam, checkAuth } from 'lib/auth'; -import { checkRequest } from 'lib/request'; +import { canDeleteTeam, canUpdateTeam } from 'lib/auth'; +import { parseRequest } from 'lib/request'; import { deleteTeam, getTeamUser, updateTeamUser } from 'queries'; export async function GET( request: Request, { params }: { params: Promise<{ teamId: string; userId: string }> }, ) { - const { teamId, userId } = await params; + const { auth, error } = await parseRequest(request); - const auth = await checkAuth(request); + if (error) { + return error(); + } + + const { teamId, userId } = await params; if (!(await canUpdateTeam(auth, teamId))) { return unauthorized('You must be the owner of this team.'); @@ -29,16 +33,14 @@ export async function POST( role: z.string().regex(/team-member|team-view-only|team-manager/), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { teamId, userId } = await params; - const auth = await checkAuth(request); - if (!(await canUpdateTeam(auth, teamId))) { return unauthorized('You must be the owner of this team.'); } @@ -58,11 +60,15 @@ export async function DELETE( request: Request, { params }: { params: Promise<{ teamId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { teamId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canDeleteTeam(auth, teamId))) { + if (!(await canDeleteTeam(auth, teamId))) { return unauthorized('You must be the owner of this team.'); } diff --git a/src/app/api/teams/[teamId]/users/route.ts b/src/app/api/teams/[teamId]/users/route.ts index 3b7f9558..a69a6b85 100644 --- a/src/app/api/teams/[teamId]/users/route.ts +++ b/src/app/api/teams/[teamId]/users/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { unauthorized, json, badRequest } from 'lib/response'; -import { canAddUserToTeam, canUpdateTeam, checkAuth } from 'lib/auth'; -import { checkRequest } from 'lib/request'; +import { canAddUserToTeam, canUpdateTeam } from 'lib/auth'; +import { parseRequest } from 'lib/request'; import { pagingParams, roleParam } from 'lib/schema'; import { createTeamUser, getTeamUser, getTeamUsers } from 'queries'; @@ -10,16 +10,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ team ...pagingParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { teamId } = await params; - const auth = await checkAuth(request); - if (!(await canUpdateTeam(auth, teamId))) { return unauthorized('You must be the owner of this team.'); } @@ -55,17 +53,15 @@ export async function POST( role: roleParam, }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { teamId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canAddUserToTeam(auth))) { + if (!(await canAddUserToTeam(auth))) { return unauthorized(); } diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts index 1d06b3c8..9f800e0e 100644 --- a/src/app/api/teams/[teamId]/websites/route.ts +++ b/src/app/api/teams/[teamId]/websites/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { unauthorized, json, badRequest } from 'lib/response'; -import { canViewTeam, checkAuth } from 'lib/auth'; -import { checkRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewTeam } from 'lib/auth'; +import { parseRequest } from 'lib/request'; import { pagingParams } from 'lib/schema'; import { getTeamWebsites } from 'queries'; @@ -9,18 +9,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, }); - - const { query, error } = await checkRequest(request, schema); + const { teamId } = await params; + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } - const { teamId } = await params; - - const auth = await checkAuth(request); - - if (!auth || !(await canViewTeam(auth, teamId))) { + if (!(await canViewTeam(auth, teamId))) { return unauthorized(); } diff --git a/src/app/api/teams/join/route.ts b/src/app/api/teams/join/route.ts index c7bff019..76d4a83f 100644 --- a/src/app/api/teams/join/route.ts +++ b/src/app/api/teams/join/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { unauthorized, json, badRequest, notFound } from 'lib/response'; -import { canCreateTeam, checkAuth } from 'lib/auth'; -import { checkRequest } from 'lib/request'; +import { canCreateTeam } from 'lib/auth'; +import { parseRequest } from 'lib/request'; import { ROLES } from 'lib/constants'; import { createTeamUser, findTeam, getTeamUser } from 'queries'; @@ -10,15 +10,13 @@ export async function POST(request: Request) { accessCode: z.string().max(50), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } - const auth = await checkAuth(request); - - if (!auth || !(await canCreateTeam(auth))) { + if (!(await canCreateTeam(auth))) { return unauthorized(); } diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index 1c097e8e..cd71b24a 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; import { getRandomChars } from 'next-basics'; -import { unauthorized, json, badRequest } from 'lib/response'; -import { canCreateTeam, checkAuth } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; -import { checkRequest } from 'lib/request'; +import { parseRequest } from 'lib/request'; import { createTeam } from 'queries'; export async function POST(request: Request) { @@ -11,15 +11,13 @@ export async function POST(request: Request) { name: z.string().max(50), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } - const auth = await checkAuth(request); - - if (!auth || !(await canCreateTeam(auth))) { + if (!(await canCreateTeam(auth))) { return unauthorized(); } diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index 30c166f1..684be305 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -1,16 +1,20 @@ import { z } from 'zod'; -import { canUpdateUser, canViewUser, checkAuth } from 'lib/auth'; +import { canUpdateUser, canViewUser } from 'lib/auth'; import { getUser, getUserByUsername, updateUser } from 'queries'; import { json, unauthorized, badRequest } from 'lib/response'; import { hashPassword } from 'next-basics'; -import { checkRequest } from 'lib/request'; +import { parseRequest } from 'lib/request'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { userId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewUser(auth, userId))) { + if (!(await canViewUser(auth, userId))) { return unauthorized(); } @@ -26,17 +30,15 @@ export async function POST(request: Request, { params }: { params: Promise<{ use role: z.string().regex(/admin|user|view-only/i), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { userId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canUpdateUser(auth, userId))) { + if (!(await canUpdateUser(auth, userId))) { return unauthorized(); } diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts index 83238799..4eb37a61 100644 --- a/src/app/api/users/[userId]/teams/route.ts +++ b/src/app/api/users/[userId]/teams/route.ts @@ -1,26 +1,23 @@ import { z } from 'zod'; import { pagingParams } from 'lib/schema'; import { getUserTeams } from 'queries'; -import { checkAuth } from 'lib/auth'; -import { unauthorized, badRequest, json } from 'lib/response'; -import { checkRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ ...pagingParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { userId } = await params; - const auth = await checkAuth(request); - - if (!auth || (!auth.user.isAdmin && (!userId || auth.user.id !== userId))) { + if (!auth.user.isAdmin && (!userId || auth.user.id !== userId)) { return unauthorized(); } diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts index 275f665f..72510bd3 100644 --- a/src/app/api/users/[userId]/usage/route.ts +++ b/src/app/api/users/[userId]/usage/route.ts @@ -1,10 +1,9 @@ import { z } from 'zod'; -import { json, unauthorized, badRequest } from 'lib/response'; +import { json, unauthorized } from 'lib/response'; import { getAllUserWebsitesIncludingTeamOwner } from 'queries/prisma/website'; import { getEventUsage } from 'queries/analytics/events/getEventUsage'; import { getEventDataUsage } from 'queries/analytics/events/getEventDataUsage'; -import { checkAuth } from 'lib/auth'; -import { checkRequest } from 'lib/request'; +import { parseRequest } from 'lib/request'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ @@ -12,15 +11,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ user endAt: z.coerce.number().int(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } - const auth = await checkAuth(request); - - if (!auth || !auth.user.isAdmin) { + if (!auth.user.isAdmin) { return unauthorized(); } diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts index 189bf8fa..22aced51 100644 --- a/src/app/api/users/[userId]/websites/route.ts +++ b/src/app/api/users/[userId]/websites/route.ts @@ -1,26 +1,23 @@ import { z } from 'zod'; -import { unauthorized, json, badRequest } from 'lib/response'; +import { unauthorized, json } from 'lib/response'; import { getUserWebsites } from 'queries/prisma/website'; import { pagingParams } from 'lib/schema'; -import { checkRequest } from 'lib/request'; -import { checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ ...pagingParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { userId } = await params; - const auth = await checkAuth(request); - - if (!auth || (!auth.user.isAdmin && auth.user.id !== userId)) { + if (!auth.user.isAdmin && auth.user.id !== userId) { return unauthorized(); } diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 87959a0c..8f9e5723 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; import { hashPassword } from 'next-basics'; -import { canCreateUser, checkAuth } from 'lib/auth'; +import { canCreateUser } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; -import { checkRequest } from 'lib/request'; +import { parseRequest } from 'lib/request'; import { unauthorized, json, badRequest } from 'lib/response'; import { createUser, getUserByUsername } from 'queries'; @@ -15,15 +15,13 @@ export async function POST(request: Request) { role: z.string().regex(/admin|user|view-only/i), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } - const auth = await checkAuth(request); - - if (!auth || !(await canCreateUser(auth))) { + if (!(await canCreateUser(auth))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts index 569bdb7b..9e25cea6 100644 --- a/src/app/api/websites/[websiteId]/active/route.ts +++ b/src/app/api/websites/[websiteId]/active/route.ts @@ -1,16 +1,21 @@ -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { canViewWebsite } from 'lib/auth'; import { json, unauthorized } from 'lib/response'; import { getActiveVisitors } from 'queries'; +import { parseRequest } from 'lib/request'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts index d4a562de..75423454 100644 --- a/src/app/api/websites/[websiteId]/daterange/route.ts +++ b/src/app/api/websites/[websiteId]/daterange/route.ts @@ -1,16 +1,21 @@ -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { canViewWebsite } from 'lib/auth'; import { getWebsiteDateRange } from 'queries'; import { json, unauthorized } from 'lib/response'; +import { parseRequest } from 'lib/request'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts index 143fae18..4937482a 100644 --- a/src/app/api/websites/[websiteId]/event-data/events/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getEventDataEvents } from 'queries/analytics/events/getEventDataEvents'; export async function GET( @@ -13,19 +13,16 @@ export async function GET( endAt: z.coerce.number().int(), event: z.string().optional(), }); - - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt, event } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts index 3ef2f3b1..e95998ed 100644 --- a/src/app/api/websites/[websiteId]/event-data/fields/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getEventDataFields } from 'queries'; export async function GET( @@ -13,17 +13,15 @@ export async function GET( endAt: z.coerce.number().int(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt } = query; - const auth = await checkAuth(request); - if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts index 68fdf4e3..25f915b1 100644 --- a/src/app/api/websites/[websiteId]/event-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getEventDataProperties } from 'queries'; export async function GET( @@ -14,18 +14,16 @@ export async function GET( propertyName: z.string().optional(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt, propertyName } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts index d958bbdc..a8093e61 100644 --- a/src/app/api/websites/[websiteId]/event-data/stats/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getEventDataStats } from 'queries'; export async function GET( @@ -14,18 +14,16 @@ export async function GET( propertyName: z.string().optional(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts index 0ecf20d3..7734d920 100644 --- a/src/app/api/websites/[websiteId]/event-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getEventDataValues } from 'queries'; export async function GET( @@ -15,18 +15,16 @@ export async function GET( propertyName: z.string().optional(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt, eventName, propertyName } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts index ef929312..71046b3c 100644 --- a/src/app/api/websites/[websiteId]/events/route.ts +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { pagingParams } from 'lib/schema'; import { getWebsiteEvents } from 'queries'; @@ -15,18 +15,16 @@ export async function GET( ...pagingParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts index 4551e3bf..99378612 100644 --- a/src/app/api/websites/[websiteId]/events/series/route.ts +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { filterParams, timezoneParam, unitParam } from 'lib/schema'; import { getEventMetrics } from 'queries'; @@ -17,19 +17,17 @@ export async function GET( ...filterParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { timezone } = query; const { startDate, endDate, unit } = await getRequestDateRange(request); - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index 3842c683..44312429 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { canViewWebsite } from 'lib/auth'; import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants'; -import { getRequestFilters, getRequestDateRange, checkRequest } from 'lib/request'; +import { getRequestFilters, getRequestDateRange, parseRequest } from 'lib/request'; import { json, unauthorized, badRequest } from 'lib/response'; import { getPageviewMetrics, getSessionMetrics } from 'queries'; import { filterParams } from 'lib/schema'; @@ -20,18 +20,16 @@ export async function GET( ...filterParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { type, limit, offset, search } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts index e9e6f32b..020e139b 100644 --- a/src/app/api/websites/[websiteId]/pageviews/route.ts +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { canViewWebsite, checkAuth } from 'lib/auth'; -import { getRequestFilters, getRequestDateRange, checkRequest } from 'lib/request'; +import { canViewWebsite } from 'lib/auth'; +import { getRequestFilters, getRequestDateRange, parseRequest } from 'lib/request'; import { unitParam, timezoneParam, filterParams } from 'lib/schema'; import { getCompareDate } from 'lib/date'; -import { badRequest, unauthorized, json } from 'lib/response'; +import { unauthorized, json } from 'lib/response'; import { getPageviewStats, getSessionStats } from 'queries'; export async function GET( @@ -19,18 +19,16 @@ export async function GET( ...filterParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { timezone, compare } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts index 0098fa15..44f62625 100644 --- a/src/app/api/websites/[websiteId]/reports/route.ts +++ b/src/app/api/websites/[websiteId]/reports/route.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { canViewWebsite } from 'lib/auth'; import { getWebsiteReports } from 'queries'; import { pagingParams } from 'lib/schema'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; export async function GET( request: Request, @@ -13,18 +13,16 @@ export async function GET( ...pagingParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { page, pageSize, search } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts index bfbd11a8..8f2df289 100644 --- a/src/app/api/websites/[websiteId]/reset/route.ts +++ b/src/app/api/websites/[websiteId]/reset/route.ts @@ -1,16 +1,21 @@ -import { canUpdateWebsite, checkAuth } from 'lib/auth'; +import { canUpdateWebsite } from 'lib/auth'; import { resetWebsite } from 'queries'; import { unauthorized, ok } from 'lib/response'; +import { parseRequest } from 'lib/request'; export async function POST( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canUpdateWebsite(auth, websiteId))) { + if (!(await canUpdateWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts index e8ad8a0b..b72a5e1d 100644 --- a/src/app/api/websites/[websiteId]/route.ts +++ b/src/app/api/websites/[websiteId]/route.ts @@ -1,19 +1,23 @@ import { z } from 'zod'; -import { canUpdateWebsite, canDeleteWebsite, checkAuth, canViewWebsite } from 'lib/auth'; +import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from 'lib/auth'; import { SHARE_ID_REGEX } from 'lib/constants'; -import { checkRequest } from 'lib/request'; -import { ok, json, badRequest, unauthorized, serverError } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { ok, json, unauthorized, serverError } from 'lib/response'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } @@ -32,18 +36,16 @@ export async function POST( shareId: z.string().regex(SHARE_ID_REGEX).nullable(), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { name, domain, shareId } = body; - const auth = await checkAuth(request); - - if (!auth || !(await canUpdateWebsite(auth, websiteId))) { + if (!(await canUpdateWebsite(auth, websiteId))) { return unauthorized(); } @@ -64,11 +66,15 @@ export async function DELETE( request: Request, { params }: { params: Promise<{ websiteId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canDeleteWebsite(auth, websiteId))) { + if (!(await canDeleteWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts index af168f84..5729ddbd 100644 --- a/src/app/api/websites/[websiteId]/session-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getSessionDataProperties } from 'queries'; export async function GET( @@ -14,18 +14,16 @@ export async function GET( propertyName: z.string().optional(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { startAt, endAt, propertyName } = query; const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts index 627298af..f14b1916 100644 --- a/src/app/api/websites/[websiteId]/session-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getEventDataEvents } from 'queries/analytics/events/getEventDataEvents'; export async function GET( @@ -14,18 +14,16 @@ export async function GET( propertyName: z.string().optional(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { startAt, endAt, event } = query; const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts index 48123ffe..ccd05d4d 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getSessionActivity } from 'queries'; export async function GET( @@ -13,18 +13,16 @@ export async function GET( endAt: z.coerce.number().int(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId, sessionId } = await params; const { startAt, endAt } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts index 7c5863e8..a95a0233 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts @@ -1,16 +1,21 @@ import { unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { canViewWebsite } from 'lib/auth'; import { getSessionData } from 'queries'; +import { parseRequest } from 'lib/request'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { websiteId, sessionId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts index 6822aaa0..0a968eb2 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts @@ -1,16 +1,21 @@ import { unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { canViewWebsite } from 'lib/auth'; import { getWebsiteSession } from 'queries'; +import { parseRequest } from 'lib/request'; export async function GET( request: Request, { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, ) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + const { websiteId, sessionId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts index c96a8ddb..da8d9d09 100644 --- a/src/app/api/websites/[websiteId]/sessions/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { pagingParams } from 'lib/schema'; import { getWebsiteSessions } from 'queries'; @@ -15,18 +15,16 @@ export async function GET( ...pagingParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts index 35c17021..8149942a 100644 --- a/src/app/api/websites/[websiteId]/sessions/stats/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { filterParams } from 'lib/schema'; import { getWebsiteSessionStats } from 'queries'; @@ -15,17 +15,15 @@ export async function GET( ...filterParams, }); - const { error } = await checkRequest(request, schema); + const { auth, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts index 05cc9cad..b57bb95c 100644 --- a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { parseRequest } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { pagingParams, timezoneParam } from 'lib/schema'; import { getWebsiteSessionsWeekly } from 'queries'; @@ -16,18 +16,16 @@ export async function GET( ...pagingParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { startAt, endAt, timezone } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts index 76d9f9a6..55dc4e3e 100644 --- a/src/app/api/websites/[websiteId]/stats/route.ts +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { checkRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; -import { badRequest, unauthorized, json } from 'lib/response'; -import { checkAuth, canViewWebsite } from 'lib/auth'; +import { parseRequest, getRequestDateRange, getRequestFilters } from 'lib/request'; +import { unauthorized, json } from 'lib/response'; +import { canViewWebsite } from 'lib/auth'; import { getCompareDate } from 'lib/date'; import { filterParams } from 'lib/schema'; import { getWebsiteStats } from 'queries'; @@ -17,18 +17,16 @@ export async function GET( ...filterParams, }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { compare } = query; - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts index 8771ecc2..d3e497cd 100644 --- a/src/app/api/websites/[websiteId]/transfer/route.ts +++ b/src/app/api/websites/[websiteId]/transfer/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { canTransferWebsiteToTeam, canTransferWebsiteToUser, checkAuth } from 'lib/auth'; +import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from 'lib/auth'; import { updateWebsite } from 'queries'; -import { checkRequest } from 'lib/request'; +import { parseRequest } from 'lib/request'; import { badRequest, unauthorized, json } from 'lib/response'; export async function POST( @@ -13,20 +13,16 @@ export async function POST( teamId: z.string().uuid().optional(), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { userId, teamId } = body; - const auth = await checkAuth(request); - - if (!auth) { - return unauthorized(); - } else if (userId) { + if (userId) { if (!(await canTransferWebsiteToUser(auth, websiteId, userId))) { return unauthorized(); } @@ -49,4 +45,6 @@ export async function POST( return json(website); } + + return badRequest(); } diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts index fe4edfbb..6e7b51d6 100644 --- a/src/app/api/websites/[websiteId]/values/route.ts +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { canViewWebsite, checkAuth } from 'lib/auth'; +import { canViewWebsite } from 'lib/auth'; import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; import { getValues } from 'queries'; -import { checkRequest, getRequestDateRange } from 'lib/request'; +import { parseRequest, getRequestDateRange } from 'lib/request'; import { badRequest, json, unauthorized } from 'lib/response'; export async function GET( @@ -16,24 +16,22 @@ export async function GET( search: z.string().optional(), }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); + return error(); } const { websiteId } = await params; const { type, search } = query; const { startDate, endDate } = await getRequestDateRange(request); - const auth = await checkAuth(request); - - if (!auth || !(await canViewWebsite(auth, websiteId))) { + if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); } if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) { - return badRequest(); + return badRequest('Invalid type.'); } const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index 6bb1e476..3911d8d8 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -1,24 +1,18 @@ import { z } from 'zod'; -import { canCreateTeamWebsite, canCreateWebsite, checkAuth } from 'lib/auth'; -import { json, badRequest, unauthorized } from 'lib/response'; +import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth'; +import { json, unauthorized } from 'lib/response'; import { uuid } from 'lib/crypto'; -import { checkRequest } from 'lib/request'; +import { parseRequest } from 'lib/request'; import { createWebsite, getUserWebsites } from 'queries'; import { pagingParams } from 'lib/schema'; export async function GET(request: Request) { const schema = z.object({ ...pagingParams }); - const { query, error } = await checkRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); - } - - const auth = await checkAuth(request); - - if (!auth) { - return unauthorized(); + return error(); } const websites = await getUserWebsites(auth.user.userId, query); @@ -34,16 +28,10 @@ export async function POST(request: Request) { teamId: z.string().nullable(), }); - const { body, error } = await checkRequest(request, schema); + const { auth, body, error } = await parseRequest(request, schema); if (error) { - return badRequest(error); - } - - const auth = await checkAuth(request); - - if (!auth) { - return unauthorized(); + return error(); } const { name, domain, shareId, teamId } = body; diff --git a/src/lib/request.ts b/src/lib/request.ts index 5eb1b477..ed5270d2 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,7 +1,9 @@ import { ZodObject } from 'zod'; +import { FILTER_COLUMNS } from 'lib/constants'; +import { badRequest, unauthorized } from 'lib/response'; import { getAllowedUnits, getMinimumUnit } from './date'; import { getWebsiteDateRange } from '../queries'; -import { FILTER_COLUMNS } from 'lib/constants'; +import { checkAuth } from 'lib/auth'; export async function getJsonBody(request: Request) { try { @@ -11,14 +13,27 @@ export async function getJsonBody(request: Request) { } } -export async function checkRequest(request: Request, schema: ZodObject) { +export async function parseRequest(request: Request, schema?: ZodObject) { + let error: () => void | undefined; const url = new URL(request.url); const query = Object.fromEntries(url.searchParams); const body = await getJsonBody(request); - const result = schema.safeParse(request.method === 'GET' ? query : body); + if (schema) { + const result = schema.safeParse(request.method === 'GET' ? query : body); - return { query, body, error: result.error }; + if (result.error) { + error = () => badRequest(result.error); + } + } + + const auth = !error ? await checkAuth(request) : null; + + if (!error && !auth) { + error = () => unauthorized(); + } + + return { url, query, body, auth, error }; } export async function getRequestDateRange(query: Record) { diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 5f81b1f1..0410a965 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -16,8 +16,8 @@ export const pageInfo = { }; export const pagingParams = { - page: z.coerce.number().int().positive(), - pageSize: z.coerce.number().int().positive(), + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().optional(), orderBy: z.string().optional(), query: z.string().optional(), }; From dcac7b7c96473c0f8960b01785a9bb17ec1813dd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 27 Jan 2025 19:21:14 -0800 Subject: [PATCH 05/17] Updated request parsing. --- src/lib/request.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/request.ts b/src/lib/request.ts index ed5270d2..769a8f21 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -9,21 +9,26 @@ export async function getJsonBody(request: Request) { try { return await request.clone().json(); } catch { - return null; + return undefined; } } export async function parseRequest(request: Request, schema?: ZodObject) { - let error: () => void | undefined; const url = new URL(request.url); - const query = Object.fromEntries(url.searchParams); - const body = await getJsonBody(request); + let query = Object.fromEntries(url.searchParams); + let body = await getJsonBody(request); + let error: () => void | undefined; if (schema) { - const result = schema.safeParse(request.method === 'GET' ? query : body); + const isGet = request.method === 'GET'; + const result = schema.safeParse(isGet ? query : body); - if (result.error) { + if (!result.success) { error = () => badRequest(result.error); + } else if (isGet) { + query = result.data; + } else { + body = result.data; } } From 6c9f1ad06b5e9d442d51bed992707d84521141a7 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 28 Jan 2025 10:21:56 -0800 Subject: [PATCH 06/17] Converted reports and share routes. --- src/app/api/reports/[reportId]/route.ts | 91 +++++++++++++++++++ src/app/api/reports/funnel/route.ts | 50 ++++++++++ src/app/api/reports/goals/route.ts | 53 +++++++++++ src/app/api/reports/insights/route.ts | 71 +++++++++++++++ src/app/api/reports/journey/route.ts | 46 ++++++++++ src/app/api/reports/retention/route.ts | 41 +++++++++ src/app/api/reports/revenue/route.ts | 75 +++++++++++++++ src/app/api/reports/route.ts | 73 +++++++++++++++ src/app/api/reports/utm/route.ts | 40 ++++++++ src/app/api/share/[shareId]/route.ts | 19 ++++ src/lib/prisma.ts | 4 +- src/lib/schema.ts | 12 ++- src/lib/types.ts | 4 +- .../reports/{[reportId].ts => _[reportId].ts} | 0 .../api/reports/{funnel.ts => _funnel.ts} | 0 src/pages/api/reports/{goals.ts => _goals.ts} | 0 src/pages/api/reports/{index.ts => _index.ts} | 0 .../api/reports/{insights.ts => _insights.ts} | 0 .../api/reports/{journey.ts => _journey.ts} | 0 .../reports/{retention.ts => _retention.ts} | 0 .../api/reports/{revenue.ts => _revenue.ts} | 0 src/pages/api/reports/{utm.ts => _utm.ts} | 0 .../api/share/{[shareId].ts => _[shareId].ts} | 0 23 files changed, 574 insertions(+), 5 deletions(-) create mode 100644 src/app/api/reports/[reportId]/route.ts create mode 100644 src/app/api/reports/funnel/route.ts create mode 100644 src/app/api/reports/goals/route.ts create mode 100644 src/app/api/reports/insights/route.ts create mode 100644 src/app/api/reports/journey/route.ts create mode 100644 src/app/api/reports/retention/route.ts create mode 100644 src/app/api/reports/revenue/route.ts create mode 100644 src/app/api/reports/route.ts create mode 100644 src/app/api/reports/utm/route.ts create mode 100644 src/app/api/share/[shareId]/route.ts rename src/pages/api/reports/{[reportId].ts => _[reportId].ts} (100%) rename src/pages/api/reports/{funnel.ts => _funnel.ts} (100%) rename src/pages/api/reports/{goals.ts => _goals.ts} (100%) rename src/pages/api/reports/{index.ts => _index.ts} (100%) rename src/pages/api/reports/{insights.ts => _insights.ts} (100%) rename src/pages/api/reports/{journey.ts => _journey.ts} (100%) rename src/pages/api/reports/{retention.ts => _retention.ts} (100%) rename src/pages/api/reports/{revenue.ts => _revenue.ts} (100%) rename src/pages/api/reports/{utm.ts => _utm.ts} (100%) rename src/pages/api/share/{[shareId].ts => _[shareId].ts} (100%) diff --git a/src/app/api/reports/[reportId]/route.ts b/src/app/api/reports/[reportId]/route.ts new file mode 100644 index 00000000..0d7c0845 --- /dev/null +++ b/src/app/api/reports/[reportId]/route.ts @@ -0,0 +1,91 @@ +import { z } from 'zod'; +import { parseRequest } from 'lib/request'; +import { deleteReport, getReport, updateReport } from 'queries'; +import { canDeleteReport, canUpdateReport, canViewReport } from 'lib/auth'; +import { unauthorized, json, notFound, ok } from 'lib/response'; +import { reportTypeParam } from 'lib/schema'; + +export async function GET(request: Request, { params }: { params: Promise<{ reportId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { reportId } = await params; + + const report = await getReport(reportId); + + if (!(await canViewReport(auth, report))) { + return unauthorized(); + } + + report.parameters = JSON.parse(report.parameters); + + return json(report); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ reportId: string }> }, +) { + const schema = z.object({ + websiteId: z.string().uuid(), + type: reportTypeParam, + name: z.string().max(200), + description: z.string().max(500), + parameters: z.object({}).passthrough(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { reportId } = await params; + const { websiteId, type, name, description, parameters } = body; + + const report = await getReport(reportId); + + if (!report) { + return notFound(); + } + + if (!(await canUpdateReport(auth, report))) { + return unauthorized(); + } + + const result = await updateReport(reportId, { + websiteId, + userId: auth.user.id, + type, + name, + description, + parameters: JSON.stringify(parameters), + } as any); + + return json(result); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ reportId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { reportId } = await params; + const report = await getReport(reportId); + + if (!(await canDeleteReport(auth, report))) { + return unauthorized(); + } + + await deleteReport(reportId); + + return ok(); +} diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts new file mode 100644 index 00000000..9a0cfd65 --- /dev/null +++ b/src/app/api/reports/funnel/route.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { getFunnel } from 'queries'; + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + steps: z + .array( + z.object({ + type: z.string(), + value: z.string(), + }), + ) + .min(2), + window: z.number().positive(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + }), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + steps, + window, + dateRange: { startDate, endDate }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getFunnel(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + steps, + windowMinutes: +window, + }); + + return json(data); +} diff --git a/src/app/api/reports/goals/route.ts b/src/app/api/reports/goals/route.ts new file mode 100644 index 00000000..ee102bc6 --- /dev/null +++ b/src/app/api/reports/goals/route.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { getGoals } from 'queries/analytics/reports/getGoals'; + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + }), + goals: z + .array( + z.object({ + type: z.string().regex(/url|event|event-data/), + value: z.string(), + goal: z.number(), + operator: z + .string() + .regex(/count|sum|average/) + .refine(data => data['type'] === 'event-data'), + property: z.string().refine(data => data['type'] === 'event-data'), + }), + ) + .min(1), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + goals, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getGoals(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + goals, + }); + + return json(data); +} diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts new file mode 100644 index 00000000..ae361934 --- /dev/null +++ b/src/app/api/reports/insights/route.ts @@ -0,0 +1,71 @@ +import { z } from 'zod'; +import { canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { getInsights } from 'queries'; + +function convertFilters(filters: any[]) { + return filters.reduce((obj, filter) => { + obj[filter.name] = filter; + + return obj; + }, {}); +} + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + }), + fields: z + .array( + z.object({ + name: z.string(), + type: z.string(), + label: z.string(), + }), + ) + .min(1), + filters: z.array( + z.object({ + name: z.string(), + type: z.string(), + operator: z.string(), + value: z.string(), + }), + ), + groups: z.array( + z.object({ + name: z.string(), + type: z.string(), + }), + ), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + fields, + filters, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getInsights(websiteId, fields, { + ...convertFilters(filters), + startDate: new Date(startDate), + endDate: new Date(endDate), + }); + + return json(data); +} diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts new file mode 100644 index 00000000..50b64952 --- /dev/null +++ b/src/app/api/reports/journey/route.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { getJourney } from 'queries'; + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + }), + steps: z.number().min(3).max(7), + startStep: z.string(), + endStep: z.string(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + steps, + startStep, + endStep, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getJourney(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + steps, + startStep, + endStep, + }); + + return json(data); +} diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts new file mode 100644 index 00000000..794ebbe4 --- /dev/null +++ b/src/app/api/reports/retention/route.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { getRetention } from 'queries'; +import { timezoneParam } from 'lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + }), + timezone: timezoneParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + timezone, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRetention(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + timezone, + }); + + return json(data); +} diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts new file mode 100644 index 00000000..0f8f7d55 --- /dev/null +++ b/src/app/api/reports/revenue/route.ts @@ -0,0 +1,75 @@ +import { z } from 'zod'; +import { canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { timezoneParam, unitParam } from 'lib/schema'; +import { getRevenue } from 'queries/analytics/reports/getRevenue'; +import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues'; + +export async function GET(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + }), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId, startDate, endDate } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRevenueValues(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + }); + + return json(data); +} + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + unit: unitParam, + }), + timezone: timezoneParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + currency, + timezone, + dateRange: { startDate, endDate, unit }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRevenue(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + unit, + timezone, + currency, + }); + + return json(data); +} diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts new file mode 100644 index 00000000..bc5284a3 --- /dev/null +++ b/src/app/api/reports/route.ts @@ -0,0 +1,73 @@ +import { z } from 'zod'; +import { pagingParams } from 'lib/schema'; +import { parseRequest } from 'lib/request'; +import { canViewTeam, canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { getReports } from 'queries/prisma/report'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { page, search, pageSize, websiteId, teamId } = query; + const userId = auth.user.id; + const filters = { + page, + pageSize, + search, + }; + + if ( + (websiteId && !(await canViewWebsite(auth, websiteId))) || + (teamId && !(await canViewTeam(auth, teamId))) + ) { + return unauthorized(); + } + + const data = await getReports( + { + where: { + OR: [ + ...(websiteId ? [{ websiteId }] : []), + ...(teamId + ? [ + { + website: { + deletedAt: null, + teamId, + }, + }, + ] + : []), + ...(userId && !websiteId && !teamId + ? [ + { + website: { + deletedAt: null, + userId, + }, + }, + ] + : []), + ], + }, + include: { + website: { + select: { + domain: true, + }, + }, + }, + }, + filters, + ); + + return json(data); +} diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts new file mode 100644 index 00000000..0af8b419 --- /dev/null +++ b/src/app/api/reports/utm/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { canViewWebsite } from 'lib/auth'; +import { unauthorized, json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { getUTM } from 'queries'; +import { timezoneParam } from 'lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.date(), + endDate: z.date(), + timezone: timezoneParam, + }), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate, timezone }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getUTM(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + timezone, + }); + + return json(data); +} diff --git a/src/app/api/share/[shareId]/route.ts b/src/app/api/share/[shareId]/route.ts new file mode 100644 index 00000000..f5c5ab5a --- /dev/null +++ b/src/app/api/share/[shareId]/route.ts @@ -0,0 +1,19 @@ +import { json, notFound } from 'lib/response'; +import { getSharedWebsite } from 'queries'; +import { createToken } from 'next-basics'; +import { secret } from 'lib/crypto'; + +export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) { + const { shareId } = await params; + + const website = await getSharedWebsite(shareId); + + if (!website) { + return notFound(); + } + + const data = { websiteId: website.id }; + const token = createToken(data, secret()); + + return json({ ...data, token }); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index cc1b8734..deee67f7 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -243,7 +243,7 @@ async function pagedQuery(model: string, criteria: T, pageParams: PageParams) const data = await prisma.client[model].findMany({ ...criteria, ...{ - ...(size > 0 && { take: +size, skip: +size * (page - 1) }), + ...(size > 0 && { take: +size, skip: +size * (+page - 1) }), ...(orderBy && { orderBy: [ { @@ -266,7 +266,7 @@ async function pagedRawQuery( ) { const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams; const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (page - 1); + const offset = +size * (+page - 1); const direction = sortDescending ? 'desc' : 'asc'; const statements = [ diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 0410a965..80be7691 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -30,7 +30,17 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), message: 'Invalid unit', }); -export const roleParam = z.string().regex(/team-member|team-view-only|team-manager/); +export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']); + +export const reportTypeParam = z.enum([ + 'funnel', + 'insights', + 'retention', + 'utm', + 'goals', + 'journey', + 'revenue', +]); export const filterParams = { url: z.string().optional(), diff --git a/src/lib/types.ts b/src/lib/types.ts index 70c2aae6..5c397d5e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -26,8 +26,8 @@ export type ReportType = ObjectValues; export interface PageParams { search?: string; - page?: number; - pageSize?: number; + page?: string; + pageSize?: string; orderBy?: string; sortDescending?: boolean; } diff --git a/src/pages/api/reports/[reportId].ts b/src/pages/api/reports/_[reportId].ts similarity index 100% rename from src/pages/api/reports/[reportId].ts rename to src/pages/api/reports/_[reportId].ts diff --git a/src/pages/api/reports/funnel.ts b/src/pages/api/reports/_funnel.ts similarity index 100% rename from src/pages/api/reports/funnel.ts rename to src/pages/api/reports/_funnel.ts diff --git a/src/pages/api/reports/goals.ts b/src/pages/api/reports/_goals.ts similarity index 100% rename from src/pages/api/reports/goals.ts rename to src/pages/api/reports/_goals.ts diff --git a/src/pages/api/reports/index.ts b/src/pages/api/reports/_index.ts similarity index 100% rename from src/pages/api/reports/index.ts rename to src/pages/api/reports/_index.ts diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/_insights.ts similarity index 100% rename from src/pages/api/reports/insights.ts rename to src/pages/api/reports/_insights.ts diff --git a/src/pages/api/reports/journey.ts b/src/pages/api/reports/_journey.ts similarity index 100% rename from src/pages/api/reports/journey.ts rename to src/pages/api/reports/_journey.ts diff --git a/src/pages/api/reports/retention.ts b/src/pages/api/reports/_retention.ts similarity index 100% rename from src/pages/api/reports/retention.ts rename to src/pages/api/reports/_retention.ts diff --git a/src/pages/api/reports/revenue.ts b/src/pages/api/reports/_revenue.ts similarity index 100% rename from src/pages/api/reports/revenue.ts rename to src/pages/api/reports/_revenue.ts diff --git a/src/pages/api/reports/utm.ts b/src/pages/api/reports/_utm.ts similarity index 100% rename from src/pages/api/reports/utm.ts rename to src/pages/api/reports/_utm.ts diff --git a/src/pages/api/share/[shareId].ts b/src/pages/api/share/_[shareId].ts similarity index 100% rename from src/pages/api/share/[shareId].ts rename to src/pages/api/share/_[shareId].ts From 5205551ca8ffd99d177a3e668eca052be40611d0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 28 Jan 2025 22:29:03 -0800 Subject: [PATCH 07/17] Converted admin, auth, me and realtime routes. --- src/app/api/admin/users/route.ts | 39 +++++++++ src/app/api/admin/websites/route.ts | 87 +++++++++++++++++++ src/app/api/auth/login/route.ts | 44 ++++++++++ src/app/api/auth/logout/route.ts | 14 +++ src/app/api/auth/sso/route.ts | 18 ++++ src/app/api/auth/verify/route.ts | 12 +++ src/app/api/me/password/route.ts | 33 +++++++ src/app/api/me/route.ts | 12 +++ src/app/api/me/teams/route.ts | 21 +++++ src/app/api/me/websites/route.ts | 21 +++++ src/app/api/realtime/[websiteId]/route.ts | 30 +++++++ src/app/api/users/[userId]/teams/route.ts | 2 +- src/lib/request.ts | 8 +- src/lib/response.ts | 12 ++- src/pages/api/admin/{users.ts => _users.ts} | 0 .../api/admin/{websites.ts => _websites.ts} | 0 src/pages/api/auth/{login.ts => _login.ts} | 0 src/pages/api/auth/{logout.ts => _logout.ts} | 0 src/pages/api/auth/{sso.ts => _sso.ts} | 0 src/pages/api/auth/{verify.ts => _verify.ts} | 0 src/pages/api/me/{index.ts => _index.ts} | 0 .../api/me/{password.ts => _password.ts} | 0 src/pages/api/me/{teams.ts => _teams.ts} | 0 .../api/me/{websites.ts => _websites.ts} | 0 .../{[websiteId].ts => _[websiteId].ts} | 0 25 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 src/app/api/admin/users/route.ts create mode 100644 src/app/api/admin/websites/route.ts create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts create mode 100644 src/app/api/auth/sso/route.ts create mode 100644 src/app/api/auth/verify/route.ts create mode 100644 src/app/api/me/password/route.ts create mode 100644 src/app/api/me/route.ts create mode 100644 src/app/api/me/teams/route.ts create mode 100644 src/app/api/me/websites/route.ts create mode 100644 src/app/api/realtime/[websiteId]/route.ts rename src/pages/api/admin/{users.ts => _users.ts} (100%) rename src/pages/api/admin/{websites.ts => _websites.ts} (100%) rename src/pages/api/auth/{login.ts => _login.ts} (100%) rename src/pages/api/auth/{logout.ts => _logout.ts} (100%) rename src/pages/api/auth/{sso.ts => _sso.ts} (100%) rename src/pages/api/auth/{verify.ts => _verify.ts} (100%) rename src/pages/api/me/{index.ts => _index.ts} (100%) rename src/pages/api/me/{password.ts => _password.ts} (100%) rename src/pages/api/me/{teams.ts => _teams.ts} (100%) rename src/pages/api/me/{websites.ts => _websites.ts} (100%) rename src/pages/api/realtime/{[websiteId].ts => _[websiteId].ts} (100%) diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 00000000..5ed4a8ff --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parseRequest } from 'lib/request'; +import { json, unauthorized } from 'lib/response'; +import { pagingParams } from 'lib/schema'; +import { canViewUsers } from 'lib/auth'; +import { getUsers } from 'queries/prisma/user'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewUsers(auth))) { + return unauthorized(); + } + + const users = await getUsers( + { + include: { + _count: { + select: { + websiteUser: { + where: { deletedAt: null }, + }, + }, + }, + }, + }, + query, + ); + + return json(users); +} diff --git a/src/app/api/admin/websites/route.ts b/src/app/api/admin/websites/route.ts new file mode 100644 index 00000000..014ef8d5 --- /dev/null +++ b/src/app/api/admin/websites/route.ts @@ -0,0 +1,87 @@ +import { z } from 'zod'; +import { parseRequest } from 'lib/request'; +import { json, unauthorized } from 'lib/response'; +import { pagingParams } from 'lib/schema'; +import { canViewAllWebsites } from 'lib/auth'; +import { getWebsites } from 'queries/prisma/website'; +import { ROLES } from 'lib/constants'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewAllWebsites(auth))) { + return unauthorized(); + } + + const { userId, includeOwnedTeams, includeAllTeams } = query; + + const websites = await getWebsites( + { + where: { + OR: [ + ...(userId && [{ userId }]), + ...(userId && includeOwnedTeams + ? [ + { + team: { + deletedAt: null, + teamUser: { + some: { + role: ROLES.teamOwner, + userId, + }, + }, + }, + }, + ] + : []), + ...(userId && includeAllTeams + ? [ + { + team: { + deletedAt: null, + teamUser: { + some: { + userId, + }, + }, + }, + }, + ] + : []), + ], + }, + include: { + user: { + select: { + username: true, + id: true, + }, + }, + team: { + where: { + deletedAt: null, + }, + include: { + teamUser: { + where: { + role: ROLES.teamOwner, + }, + }, + }, + }, + }, + }, + query, + ); + + return json(websites); +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 00000000..42d71fcf --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; +import { checkPassword, createSecureToken } from 'next-basics'; +import { redisEnabled } from '@umami/redis-client'; +import { getUserByUsername } from 'queries'; +import { json, unauthorized } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { saveAuth } from 'lib/auth'; +import { secret } from 'lib/crypto'; +import { ROLES } from 'lib/constants'; + +export async function POST(request: Request) { + const schema = z.object({ + username: z.string(), + password: z.string(), + }); + + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const { username, password } = body; + + const user = await getUserByUsername(username, { includePassword: true }); + + if (!user || !checkPassword(password, user.password)) { + return unauthorized(); + } + + if (redisEnabled) { + const token = await saveAuth({ userId: user.id }); + + return json({ token, user }); + } + + const token = createSecureToken({ userId: user.id }, secret()); + const { id, role, createdAt } = user; + + return json({ + token, + user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, + }); +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 00000000..ce7ce7f8 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,14 @@ +import { getClient, redisEnabled } from '@umami/redis-client'; +import { ok } from 'lib/response'; + +export async function POST(request: Request) { + if (redisEnabled) { + const redis = getClient(); + + const token = request.headers.get('authorization')?.split(' ')?.[1]; + + await redis.del(token); + } + + return ok(); +} diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts new file mode 100644 index 00000000..e06e403c --- /dev/null +++ b/src/app/api/auth/sso/route.ts @@ -0,0 +1,18 @@ +import { redisEnabled } from '@umami/redis-client'; +import { json } from 'lib/response'; +import { parseRequest } from 'lib/request'; +import { saveAuth } from 'lib/auth'; + +export async function POST(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + if (redisEnabled) { + const token = await saveAuth({ userId: auth.user.id }, 86400); + + return json({ user: auth.user, token }); + } +} diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts new file mode 100644 index 00000000..db62a339 --- /dev/null +++ b/src/app/api/auth/verify/route.ts @@ -0,0 +1,12 @@ +import { parseRequest } from 'lib/request'; +import { json } from 'lib/response'; + +export async function GET(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + return json(auth.user); +} diff --git a/src/app/api/me/password/route.ts b/src/app/api/me/password/route.ts new file mode 100644 index 00000000..39af3d0e --- /dev/null +++ b/src/app/api/me/password/route.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { parseRequest } from 'lib/request'; +import { json, badRequest } from 'lib/response'; +import { getUser, updateUser } from 'queries/prisma/user'; +import { checkPassword, hashPassword } from 'next-basics'; + +export async function POST(request: Request) { + const schema = z.object({ + currentPassword: z.string(), + newPassword: z.string().min(8), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const userId = auth.user.id; + const { currentPassword, newPassword } = body; + + const user = await getUser(userId, { includePassword: true }); + + if (!checkPassword(currentPassword, user.password)) { + return badRequest('Current password is incorrect'); + } + + const password = hashPassword(newPassword); + + const updated = await updateUser(userId, { password }); + + return json(updated); +} diff --git a/src/app/api/me/route.ts b/src/app/api/me/route.ts new file mode 100644 index 00000000..60a14271 --- /dev/null +++ b/src/app/api/me/route.ts @@ -0,0 +1,12 @@ +import { parseRequest } from 'lib/request'; +import { json } from 'lib/response'; + +export async function GET(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + return json(auth); +} diff --git a/src/app/api/me/teams/route.ts b/src/app/api/me/teams/route.ts new file mode 100644 index 00000000..0624e94f --- /dev/null +++ b/src/app/api/me/teams/route.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { pagingParams } from 'lib/schema'; +import { getUserTeams } from 'queries'; +import { json } from 'lib/response'; +import { parseRequest } from 'lib/request'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const teams = await getUserTeams(auth.user.id, query); + + return json(teams); +} diff --git a/src/app/api/me/websites/route.ts b/src/app/api/me/websites/route.ts new file mode 100644 index 00000000..725ca94b --- /dev/null +++ b/src/app/api/me/websites/route.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { pagingParams } from 'lib/schema'; +import { getUserWebsites } from 'queries'; +import { json } from 'lib/response'; +import { parseRequest } from 'lib/request'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const websites = await getUserWebsites(auth.user.id, query); + + return json(websites); +} diff --git a/src/app/api/realtime/[websiteId]/route.ts b/src/app/api/realtime/[websiteId]/route.ts new file mode 100644 index 00000000..b575ac12 --- /dev/null +++ b/src/app/api/realtime/[websiteId]/route.ts @@ -0,0 +1,30 @@ +import { json, unauthorized } from 'lib/response'; +import { getRealtimeData } from 'queries'; +import { canViewWebsite } from 'lib/auth'; +import { startOfMinute, subMinutes } from 'date-fns'; +import { REALTIME_RANGE } from 'lib/constants'; +import { parseRequest } from 'lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, query, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { timezone } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + + const data = await getRealtimeData(websiteId, { startDate, timezone }); + + return json(data); +} diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts index 4eb37a61..329e7413 100644 --- a/src/app/api/users/[userId]/teams/route.ts +++ b/src/app/api/users/[userId]/teams/route.ts @@ -17,7 +17,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user const { userId } = await params; - if (!auth.user.isAdmin && (!userId || auth.user.id !== userId)) { + if (auth.user.id !== userId && !auth.user.isAdmin) { return unauthorized(); } diff --git a/src/lib/request.ts b/src/lib/request.ts index 769a8f21..dc721225 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -13,7 +13,11 @@ export async function getJsonBody(request: Request) { } } -export async function parseRequest(request: Request, schema?: ZodObject) { +export async function parseRequest( + request: Request, + schema?: ZodObject, + options?: { skipAuth: boolean }, +): Promise { const url = new URL(request.url); let query = Object.fromEntries(url.searchParams); let body = await getJsonBody(request); @@ -32,7 +36,7 @@ export async function parseRequest(request: Request, schema?: ZodObject) { } } - const auth = !error ? await checkAuth(request) : null; + const auth = !error && !options?.skipAuth ? await checkAuth(request) : null; if (!error && !auth) { error = () => unauthorized(); diff --git a/src/lib/response.ts b/src/lib/response.ts index 7ed0316e..5e3b020f 100644 --- a/src/lib/response.ts +++ b/src/lib/response.ts @@ -12,14 +12,18 @@ export function badRequest(message?: any) { return Response.json({ error: 'Bad request', message }, { status: 400 }); } -export function notFound(message?: any) { - return Response.json({ error: 'Not found', message, status: 404 }); -} - export function unauthorized(message?: any) { return Response.json({ error: 'Unauthorized', message }, { status: 401 }); } +export function forbidden(message?: any) { + return Response.json({ error: 'Forbidden', message, status: 403 }); +} + +export function notFound(message?: any) { + return Response.json({ error: 'Not found', message, status: 404 }); +} + export function serverError(error?: any) { return Response.json({ error: 'Server error', message: serializeError(error), status: 500 }); } diff --git a/src/pages/api/admin/users.ts b/src/pages/api/admin/_users.ts similarity index 100% rename from src/pages/api/admin/users.ts rename to src/pages/api/admin/_users.ts diff --git a/src/pages/api/admin/websites.ts b/src/pages/api/admin/_websites.ts similarity index 100% rename from src/pages/api/admin/websites.ts rename to src/pages/api/admin/_websites.ts diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/_login.ts similarity index 100% rename from src/pages/api/auth/login.ts rename to src/pages/api/auth/_login.ts diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/_logout.ts similarity index 100% rename from src/pages/api/auth/logout.ts rename to src/pages/api/auth/_logout.ts diff --git a/src/pages/api/auth/sso.ts b/src/pages/api/auth/_sso.ts similarity index 100% rename from src/pages/api/auth/sso.ts rename to src/pages/api/auth/_sso.ts diff --git a/src/pages/api/auth/verify.ts b/src/pages/api/auth/_verify.ts similarity index 100% rename from src/pages/api/auth/verify.ts rename to src/pages/api/auth/_verify.ts diff --git a/src/pages/api/me/index.ts b/src/pages/api/me/_index.ts similarity index 100% rename from src/pages/api/me/index.ts rename to src/pages/api/me/_index.ts diff --git a/src/pages/api/me/password.ts b/src/pages/api/me/_password.ts similarity index 100% rename from src/pages/api/me/password.ts rename to src/pages/api/me/_password.ts diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/_teams.ts similarity index 100% rename from src/pages/api/me/teams.ts rename to src/pages/api/me/_teams.ts diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/_websites.ts similarity index 100% rename from src/pages/api/me/websites.ts rename to src/pages/api/me/_websites.ts diff --git a/src/pages/api/realtime/[websiteId].ts b/src/pages/api/realtime/_[websiteId].ts similarity index 100% rename from src/pages/api/realtime/[websiteId].ts rename to src/pages/api/realtime/_[websiteId].ts From 85382e25af0cc7b5354187b4d71e43b57c09a147 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 31 Jan 2025 00:15:39 -0800 Subject: [PATCH 08/17] Refactored send. Purged pages api routes. --- next.config.js | 1 - src/app/api/reports/insights/route.ts | 10 +- src/app/api/send/route.ts | 191 ++++++++++++++++++ src/lib/constants.ts | 14 ++ src/lib/detect.ts | 107 +++++----- src/lib/load.ts | 47 ++--- src/lib/middleware.ts | 105 ---------- src/lib/session.ts | 94 --------- src/pages/api/_config.ts | 22 -- src/pages/api/_version.ts | 17 -- src/pages/api/admin/_users.ts | 64 ------ src/pages/api/admin/_websites.ts | 113 ----------- src/pages/api/auth/_login.ts | 71 ------- src/pages/api/auth/_logout.ts | 21 -- src/pages/api/auth/_sso.ts | 18 -- src/pages/api/auth/_verify.ts | 12 -- src/pages/api/me/_index.ts | 13 -- src/pages/api/me/_password.ts | 56 ----- src/pages/api/me/_teams.ts | 27 --- src/pages/api/me/_websites.ts | 27 --- src/pages/api/realtime/_[websiteId].ts | 43 ---- src/pages/api/reports/_[reportId].ts | 102 ---------- src/pages/api/reports/_funnel.ts | 81 -------- src/pages/api/reports/_goals.ts | 84 -------- src/pages/api/reports/_index.ts | 128 ------------ src/pages/api/reports/_insights.ts | 96 --------- src/pages/api/reports/_journey.ts | 66 ------ src/pages/api/reports/_retention.ts | 59 ------ src/pages/api/reports/_revenue.ts | 80 -------- src/pages/api/reports/_utm.ts | 54 ----- src/pages/api/send.ts | 172 ---------------- src/pages/api/share/_[shareId].ts | 46 ----- src/pages/api/teams/[teamId]/_index.ts | 80 -------- .../api/teams/[teamId]/users/_[userId].ts | 85 -------- src/pages/api/teams/[teamId]/users/_index.ts | 90 --------- .../api/teams/[teamId]/websites/_index.ts | 39 ---- src/pages/api/teams/_index.ts | 57 ------ src/pages/api/teams/_join.ts | 52 ----- src/pages/api/users/[userId]/_index.ts | 110 ---------- src/pages/api/users/[userId]/_teams.ts | 42 ---- src/pages/api/users/[userId]/_usage.ts | 89 -------- src/pages/api/users/[userId]/_websites.ts | 40 ---- src/pages/api/users/_index.ts | 66 ------ src/pages/api/websites/[websiteId]/_active.ts | 40 ---- .../api/websites/[websiteId]/_daterange.ts | 40 ---- src/pages/api/websites/[websiteId]/_index.ts | 87 -------- .../api/websites/[websiteId]/_metrics.ts | 125 ------------ .../api/websites/[websiteId]/_pageviews.ts | 122 ----------- .../api/websites/[websiteId]/_reports.ts | 48 ----- src/pages/api/websites/[websiteId]/_reset.ts | 40 ---- src/pages/api/websites/[websiteId]/_stats.ts | 101 --------- .../api/websites/[websiteId]/_transfer.ts | 66 ------ src/pages/api/websites/[websiteId]/_values.ts | 64 ------ .../[websiteId]/event-data/_events.ts | 53 ----- .../[websiteId]/event-data/_fields.ts | 51 ----- .../[websiteId]/event-data/_properties.ts | 49 ----- .../websites/[websiteId]/event-data/_stats.ts | 47 ----- .../[websiteId]/event-data/_values.ts | 57 ------ .../api/websites/[websiteId]/events/_index.ts | 45 ----- .../websites/[websiteId]/events/_series.ts | 81 -------- .../[websiteId]/session-data/_properties.ts | 49 ----- .../[websiteId]/session-data/_values.ts | 50 ----- .../sessions/[sessionId]/_activity.ts | 49 ----- .../sessions/[sessionId]/_index.ts | 42 ---- .../sessions/[sessionId]/_properties.ts | 42 ---- .../websites/[websiteId]/sessions/_index.ts | 47 ----- .../websites/[websiteId]/sessions/_stats.ts | 86 -------- .../websites/[websiteId]/sessions/_weekly.ts | 50 ----- src/pages/api/websites/_index.ts | 82 -------- 69 files changed, 286 insertions(+), 4118 deletions(-) create mode 100644 src/app/api/send/route.ts delete mode 100644 src/lib/middleware.ts delete mode 100644 src/lib/session.ts delete mode 100644 src/pages/api/_config.ts delete mode 100644 src/pages/api/_version.ts delete mode 100644 src/pages/api/admin/_users.ts delete mode 100644 src/pages/api/admin/_websites.ts delete mode 100644 src/pages/api/auth/_login.ts delete mode 100644 src/pages/api/auth/_logout.ts delete mode 100644 src/pages/api/auth/_sso.ts delete mode 100644 src/pages/api/auth/_verify.ts delete mode 100644 src/pages/api/me/_index.ts delete mode 100644 src/pages/api/me/_password.ts delete mode 100644 src/pages/api/me/_teams.ts delete mode 100644 src/pages/api/me/_websites.ts delete mode 100644 src/pages/api/realtime/_[websiteId].ts delete mode 100644 src/pages/api/reports/_[reportId].ts delete mode 100644 src/pages/api/reports/_funnel.ts delete mode 100644 src/pages/api/reports/_goals.ts delete mode 100644 src/pages/api/reports/_index.ts delete mode 100644 src/pages/api/reports/_insights.ts delete mode 100644 src/pages/api/reports/_journey.ts delete mode 100644 src/pages/api/reports/_retention.ts delete mode 100644 src/pages/api/reports/_revenue.ts delete mode 100644 src/pages/api/reports/_utm.ts delete mode 100644 src/pages/api/send.ts delete mode 100644 src/pages/api/share/_[shareId].ts delete mode 100644 src/pages/api/teams/[teamId]/_index.ts delete mode 100644 src/pages/api/teams/[teamId]/users/_[userId].ts delete mode 100644 src/pages/api/teams/[teamId]/users/_index.ts delete mode 100644 src/pages/api/teams/[teamId]/websites/_index.ts delete mode 100644 src/pages/api/teams/_index.ts delete mode 100644 src/pages/api/teams/_join.ts delete mode 100644 src/pages/api/users/[userId]/_index.ts delete mode 100644 src/pages/api/users/[userId]/_teams.ts delete mode 100644 src/pages/api/users/[userId]/_usage.ts delete mode 100644 src/pages/api/users/[userId]/_websites.ts delete mode 100644 src/pages/api/users/_index.ts delete mode 100644 src/pages/api/websites/[websiteId]/_active.ts delete mode 100644 src/pages/api/websites/[websiteId]/_daterange.ts delete mode 100644 src/pages/api/websites/[websiteId]/_index.ts delete mode 100644 src/pages/api/websites/[websiteId]/_metrics.ts delete mode 100644 src/pages/api/websites/[websiteId]/_pageviews.ts delete mode 100644 src/pages/api/websites/[websiteId]/_reports.ts delete mode 100644 src/pages/api/websites/[websiteId]/_reset.ts delete mode 100644 src/pages/api/websites/[websiteId]/_stats.ts delete mode 100644 src/pages/api/websites/[websiteId]/_transfer.ts delete mode 100644 src/pages/api/websites/[websiteId]/_values.ts delete mode 100644 src/pages/api/websites/[websiteId]/event-data/_events.ts delete mode 100644 src/pages/api/websites/[websiteId]/event-data/_fields.ts delete mode 100644 src/pages/api/websites/[websiteId]/event-data/_properties.ts delete mode 100644 src/pages/api/websites/[websiteId]/event-data/_stats.ts delete mode 100644 src/pages/api/websites/[websiteId]/event-data/_values.ts delete mode 100644 src/pages/api/websites/[websiteId]/events/_index.ts delete mode 100644 src/pages/api/websites/[websiteId]/events/_series.ts delete mode 100644 src/pages/api/websites/[websiteId]/session-data/_properties.ts delete mode 100644 src/pages/api/websites/[websiteId]/session-data/_values.ts delete mode 100644 src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts delete mode 100644 src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts delete mode 100644 src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts delete mode 100644 src/pages/api/websites/[websiteId]/sessions/_index.ts delete mode 100644 src/pages/api/websites/[websiteId]/sessions/_stats.ts delete mode 100644 src/pages/api/websites/[websiteId]/sessions/_weekly.ts delete mode 100644 src/pages/api/websites/_index.ts diff --git a/next.config.js b/next.config.js index 99791dff..7a65c472 100644 --- a/next.config.js +++ b/next.config.js @@ -63,7 +63,6 @@ const headers = [ { source: '/api/:path*', headers: [ - { key: 'Access-Control-Allow-Credentials', value: 'true' }, { key: 'Access-Control-Allow-Origin', value: '*' }, { key: 'Access-Control-Allow-Headers', value: '*' }, { key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' }, diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts index ae361934..f622de6a 100644 --- a/src/app/api/reports/insights/route.ts +++ b/src/app/api/reports/insights/route.ts @@ -16,8 +16,8 @@ export async function POST(request: Request) { const schema = z.object({ websiteId: z.string().uuid(), dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), }), fields: z .array( @@ -36,12 +36,6 @@ export async function POST(request: Request) { value: z.string(), }), ), - groups: z.array( - z.object({ - name: z.string(), - type: z.string(), - }), - ), }); const { auth, body, error } = await parseRequest(request, schema); diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts new file mode 100644 index 00000000..864ac375 --- /dev/null +++ b/src/app/api/send/route.ts @@ -0,0 +1,191 @@ +import { z } from 'zod'; +import { isbot } from 'isbot'; +import { serializeError } from 'serialize-error'; +import { createToken, parseToken, safeDecodeURI } from 'next-basics'; +import clickhouse from 'lib/clickhouse'; +import { parseRequest } from 'lib/request'; +import { badRequest, json, forbidden, serverError } from 'lib/response'; +import { fetchSession, fetchWebsite } from 'lib/load'; +import { getClientInfo, hasBlockedIp } from 'lib/detect'; +import { secret, uuid, visitSalt } from 'lib/crypto'; +import { createSession, saveEvent, saveSessionData } from 'queries'; +import { COLLECTION_TYPE } from 'lib/constants'; + +export async function POST(request: Request) { + // Bot check + if (!process.env.DISABLE_BOT_CHECK && isbot(request.headers.get('user-agent'))) { + return json({ beep: 'boop' }); + } + + const schema = z.object({ + type: z.enum(['event', 'identity']), + payload: z.object({ + website: z.string().uuid(), + data: z.object({}).passthrough().optional(), + hostname: z.string().max(100).optional(), + language: z.string().max(35).optional(), + referrer: z.string().optional(), + screen: z.string().max(11).optional(), + title: z.string().optional(), + url: z.string().optional(), + name: z.string().max(50).optional(), + tag: z.string().max(50).optional(), + ip: z.string().ip().optional(), + userAgent: z.string().optional(), + }), + }); + + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const { type, payload } = body; + + const { + website: websiteId, + hostname, + screen, + language, + url, + referrer, + name, + data, + title, + tag, + } = payload; + + // Cache check + let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null; + const cacheHeader = request.headers.get('x-umami-cache'); + + if (cacheHeader) { + const result = await parseToken(cacheHeader, secret()); + + if (result) { + cache = result; + } + } + + // Find website + if (!cache?.websiteId) { + const website = await fetchWebsite(websiteId); + + if (!website) { + return badRequest('Website not found.'); + } + } + + // Client info + const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } = + await getClientInfo(request, payload); + + // IP block + if (hasBlockedIp(ip)) { + return forbidden(); + } + + const sessionId = uuid(websiteId, hostname, ip, userAgent); + + // Find session + if (!cache?.sessionId) { + const session = await fetchSession(websiteId, sessionId); + + // Create a session if not found + if (!session && !clickhouse.enabled) { + try { + await createSession({ + id: sessionId, + websiteId, + hostname, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + }); + } catch (e: any) { + if (!e.message.toLowerCase().includes('unique constraint')) { + return serverError(serializeError(e)); + } + } + } + } + + // Visit info + let visitId = cache?.visitId || uuid(sessionId, visitSalt()); + const iat = Math.floor(new Date().getTime() / 1000); + + // Expire visit after 30 minutes + if (cache?.iat && iat - cache?.iat > 1800) { + visitId = uuid(sessionId, visitSalt()); + } + + if (type === COLLECTION_TYPE.event) { + // eslint-disable-next-line prefer-const + let [urlPath, urlQuery] = safeDecodeURI(url)?.split('?') || []; + let [referrerPath, referrerQuery] = safeDecodeURI(referrer)?.split('?') || []; + let referrerDomain = ''; + + if (!urlPath) { + urlPath = '/'; + } + + if (/^[\w-]+:\/\/\w+/.test(referrerPath)) { + const refUrl = new URL(referrer); + referrerPath = refUrl.pathname; + referrerQuery = refUrl.search.substring(1); + referrerDomain = refUrl.hostname.replace(/www\./, ''); + } + + if (process.env.REMOVE_TRAILING_SLASH) { + urlPath = urlPath.replace(/(.+)\/$/, '$1'); + } + + await saveEvent({ + websiteId, + sessionId, + visitId, + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle: title, + eventName: name, + eventData: data, + hostname, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + tag, + }); + } + + if (type === COLLECTION_TYPE.identify) { + if (!data) { + return badRequest('Data required.'); + } + + await saveSessionData({ + websiteId, + sessionId, + sessionData: data, + }); + } + + const token = createToken({ websiteId, sessionId, visitId, iat }, secret()); + + return json(token); +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a9e13c14..cbc954e3 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -324,6 +324,20 @@ export const BROWSERS = { yandexbrowser: 'Yandex', }; +export const IP_ADDRESS_HEADERS = [ + 'cf-connecting-ip', + 'x-client-ip', + 'x-forwarded-for', + 'do-connecting-ip', + 'fastly-client-ip', + 'true-client-ip', + 'x-real-ip', + 'x-cluster-client-ip', + 'x-forwarded', + 'forwarded', + 'x-appengine-user-ip', +]; + export const MAP_FILE = '/datamaps.world.json'; export const ISO_COUNTRIES = { diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 0bea4403..fe1c2124 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -1,34 +1,45 @@ import path from 'path'; -import { getClientIp } from 'request-ip'; import { browserName, detectOS } from 'detect-browser'; import isLocalhost from 'is-localhost-ip'; import ipaddr from 'ipaddr.js'; import maxmind from 'maxmind'; -import { safeDecodeURIComponent } from 'next-basics'; import { DESKTOP_OS, MOBILE_OS, DESKTOP_SCREEN_WIDTH, LAPTOP_SCREEN_WIDTH, MOBILE_SCREEN_WIDTH, + IP_ADDRESS_HEADERS, } from './constants'; -import { NextApiRequestCollect } from 'pages/api/send'; let lookup; -export function getIpAddress(req: NextApiRequestCollect) { - const customHeader = String(process.env.CLIENT_IP_HEADER).toLowerCase(); +export function getIpAddress(headers: Headers) { + const customHeader = process.env.CLIENT_IP_HEADER; - // Custom header - if (customHeader !== 'undefined' && req.headers[customHeader]) { - return req.headers[customHeader]; - } - // Cloudflare - else if (req.headers['cf-connecting-ip']) { - return req.headers['cf-connecting-ip']; + if (customHeader && headers.get(customHeader)) { + return headers.get(customHeader); } - return getClientIp(req); + const header = IP_ADDRESS_HEADERS.find(name => { + return headers.get(name); + }); + + const ip = headers.get(header); + + if (header === 'x-forwarded-for') { + return ip?.split[',']?.[0]?.trim(); + } + + if (header === 'forwarded') { + const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/); + + if (match) { + return match[1]; + } + } + + return ip; } export function getDevice(screen: string, os: string) { @@ -67,7 +78,7 @@ function getRegionCode(country: string, region: string) { return region.includes('-') ? region : `${country}-${region}`; } -function safeDecodeCfHeader(s: string | undefined | null): string | undefined | null { +function decodeHeader(s: string | undefined | null): string | undefined | null { if (s === undefined || s === null) { return s; } @@ -75,36 +86,38 @@ function safeDecodeCfHeader(s: string | undefined | null): string | undefined | return Buffer.from(s, 'latin1').toString('utf-8'); } -export async function getLocation(ip: string, req: NextApiRequestCollect) { +export async function getLocation(ip: string, headers: Headers) { // Ignore local ips if (await isLocalhost(ip)) { return; } - // Cloudflare headers - if (req.headers['cf-ipcountry']) { - const country = safeDecodeCfHeader(req.headers['cf-ipcountry']); - const subdivision1 = safeDecodeCfHeader(req.headers['cf-region-code']); - const city = safeDecodeCfHeader(req.headers['cf-ipcity']); + if (!process.env.SKIP_LOCATION_HEADERS) { + // Cloudflare headers + if (headers.get('cf-ipcountry')) { + const country = decodeHeader(headers.get('cf-ipcountry')); + const subdivision1 = decodeHeader(headers.get('cf-region-code')); + const city = decodeHeader(headers.get('cf-ipcity')); - return { - country, - subdivision1: getRegionCode(country, subdivision1), - city, - }; - } + return { + country, + subdivision1: getRegionCode(country, subdivision1), + city, + }; + } - // Vercel headers - if (req.headers['x-vercel-ip-country']) { - const country = safeDecodeURIComponent(req.headers['x-vercel-ip-country']); - const subdivision1 = safeDecodeURIComponent(req.headers['x-vercel-ip-country-region']); - const city = safeDecodeURIComponent(req.headers['x-vercel-ip-city']); + // Vercel headers + if (headers.get('x-vercel-ip-country')) { + const country = decodeHeader(headers.get('x-vercel-ip-country')); + const subdivision1 = decodeHeader(headers.get('x-vercel-ip-country-region')); + const city = decodeHeader(headers.get('x-vercel-ip-city')); - return { - country, - subdivision1: getRegionCode(country, subdivision1), - city, - }; + return { + country, + subdivision1: getRegionCode(country, subdivision1), + city, + }; + } } // Database lookup @@ -131,22 +144,22 @@ export async function getLocation(ip: string, req: NextApiRequestCollect) { } } -export async function getClientInfo(req: NextApiRequestCollect) { - const userAgent = req.body?.payload?.userAgent || req.headers['user-agent']; - const ip = req.body?.payload?.ip || getIpAddress(req); - const location = await getLocation(ip, req); +export async function getClientInfo(request: Request, payload: Record) { + const userAgent = payload?.userAgent || request.headers.get('user-agent'); + const ip = payload?.ip || getIpAddress(request.headers); + const location = await getLocation(ip, request.headers); const country = location?.country; const subdivision1 = location?.subdivision1; const subdivision2 = location?.subdivision2; const city = location?.city; const browser = browserName(userAgent); const os = detectOS(userAgent) as string; - const device = getDevice(req.body?.payload?.screen, os); + const device = getDevice(payload?.screen, os); return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device }; } -export function hasBlockedIp(req: NextApiRequestCollect) { +export function hasBlockedIp(clientIp: string) { const ignoreIps = process.env.IGNORE_IP; if (ignoreIps) { @@ -156,17 +169,19 @@ export function hasBlockedIp(req: NextApiRequestCollect) { ips.push(...ignoreIps.split(',').map(n => n.trim())); } - const clientIp = getIpAddress(req); - return ips.find(ip => { - if (ip === clientIp) return true; + if (ip === clientIp) { + return true; + } // CIDR notation if (ip.indexOf('/') > 0) { const addr = ipaddr.parse(clientIp); const range = ipaddr.parseCIDR(ip); - if (addr.kind() === range[0].kind() && addr.match(range)) return true; + if (addr.kind() === range[0].kind() && addr.match(range)) { + return true; + } } }); } diff --git a/src/lib/load.ts b/src/lib/load.ts index 3650f233..0cb09da4 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -1,26 +1,20 @@ -import { serializeError } from 'serialize-error'; -import { getWebsiteSession, getWebsite } from 'queries'; import { Website, Session } from '@prisma/client'; import { getClient, redisEnabled } from '@umami/redis-client'; +import { getWebsiteSession, getWebsite } from 'queries'; export async function fetchWebsite(websiteId: string): Promise { let website = null; - try { - if (redisEnabled) { - const redis = getClient(); + if (redisEnabled) { + const redis = getClient(); - website = await redis.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400); - } else { - website = await getWebsite(websiteId); - } + website = await redis.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400); + } else { + website = await getWebsite(websiteId); + } - if (!website || website.deletedAt) { - return null; - } - } catch (e) { - // eslint-disable-next-line no-console - console.log('FETCH WEBSITE ERROR:', serializeError(e)); + if (!website || website.deletedAt) { + return null; } return website; @@ -29,21 +23,16 @@ export async function fetchWebsite(websiteId: string): Promise { export async function fetchSession(websiteId: string, sessionId: string): Promise { let session = null; - try { - if (redisEnabled) { - const redis = getClient(); + if (redisEnabled) { + const redis = getClient(); - session = await redis.fetch( - `session:${sessionId}`, - () => getWebsiteSession(websiteId, sessionId), - 86400, - ); - } else { - session = await getWebsiteSession(websiteId, sessionId); - } - } catch (e) { - // eslint-disable-next-line no-console - console.log('FETCH SESSION ERROR:', serializeError(e)); + session = await redis.fetch( + `session:${sessionId}`, + () => getWebsiteSession(websiteId, sessionId), + 86400, + ); + } else { + session = await getWebsiteSession(websiteId, sessionId); } if (!session) { diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts deleted file mode 100644 index 3f7b9504..00000000 --- a/src/lib/middleware.ts +++ /dev/null @@ -1,105 +0,0 @@ -import cors from 'cors'; -import debug from 'debug'; -import { getClient, redisEnabled } from '@umami/redis-client'; -import { getAuthToken, parseShareToken } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { secret } from 'lib/crypto'; -import { getSession } from 'lib/session'; -import { - badRequest, - createMiddleware, - notFound, - parseSecureToken, - unauthorized, -} from 'next-basics'; -import { NextApiRequestCollect } from 'pages/api/send'; -import { getUser } from '../queries'; - -const log = debug('umami:middleware'); - -export const useCors = createMiddleware( - cors({ - // Cache CORS preflight request 24 hours by default - maxAge: Number(process.env.CORS_MAX_AGE) || 86400, - }), -); - -export const useSession = createMiddleware(async (req, res, next) => { - try { - const session = await getSession(req as NextApiRequestCollect); - - if (!session) { - log('useSession: Session not found'); - return badRequest(res, 'Session not found.'); - } - - (req as any).session = session; - } catch (e: any) { - if (e.message.startsWith('Website not found')) { - return notFound(res, e.message); - } - return badRequest(res, e.message); - } - - next(); -}); - -export const useAuth = createMiddleware(async (req, res, next) => { - const token = getAuthToken(req); - const payload = parseSecureToken(token, secret()); - const shareToken = await parseShareToken(req as any); - - let user = null; - const { userId, authKey, grant } = payload || {}; - - if (userId) { - user = await getUser(userId); - } else if (redisEnabled && authKey) { - const redis = getClient(); - - const key = await redis.get(authKey); - - if (key?.userId) { - user = await getUser(key.userId); - } - } - - if (process.env.NODE_ENV === 'development') { - log('useAuth:', { token, shareToken, payload, user, grant }); - } - - if (!user?.id && !shareToken) { - log('useAuth: User not authorized'); - return unauthorized(res); - } - - if (user) { - user.isAdmin = user.role === ROLES.admin; - } - - (req as any).auth = { - user, - grant, - token, - shareToken, - authKey, - }; - - next(); -}); - -export const useValidate = async (schema, req, res) => { - return createMiddleware(async (req: any, res, next) => { - try { - const rules = schema[req.method]; - - if (rules) { - rules.validateSync({ ...req.query, ...req.body }); - } - } catch (e: any) { - return badRequest(res, e.message); - } - - next(); - })(req, res); -}; diff --git a/src/lib/session.ts b/src/lib/session.ts deleted file mode 100644 index 0bfe0302..00000000 --- a/src/lib/session.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { secret, uuid, visitSalt } from 'lib/crypto'; -import { getClientInfo } from 'lib/detect'; -import { parseToken } from 'next-basics'; -import { NextApiRequestCollect } from 'pages/api/send'; -import { createSession } from 'queries'; -import clickhouse from './clickhouse'; -import { fetchSession, fetchWebsite } from './load'; -import { SessionData } from 'lib/types'; - -export async function getSession(req: NextApiRequestCollect): Promise { - const { payload } = req.body; - - if (!payload) { - throw new Error('Invalid payload.'); - } - - // Check if cache token is passed - const cacheToken = req.headers['x-umami-cache']; - - if (cacheToken) { - const result = await parseToken(cacheToken, secret()); - - // Token is valid - if (result) { - return result; - } - } - - // Verify payload - const { website: websiteId, hostname, screen, language } = payload; - - // Find website - const website = await fetchWebsite(websiteId); - - if (!website) { - throw new Error(`Website not found: ${websiteId}.`); - } - - const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } = - await getClientInfo(req); - - const sessionId = uuid(websiteId, hostname, ip, userAgent); - const visitId = uuid(sessionId, visitSalt()); - - // Clickhouse does not require session lookup - if (clickhouse.enabled) { - return { - id: sessionId, - websiteId, - visitId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - ip, - userAgent, - }; - } - - // Find session - let session = await fetchSession(websiteId, sessionId); - - // Create a session if not found - if (!session) { - try { - session = await createSession({ - id: sessionId, - websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - }); - } catch (e: any) { - if (!e.message.toLowerCase().includes('unique constraint')) { - throw e; - } - } - } - - return { ...session, visitId }; -} diff --git a/src/pages/api/_config.ts b/src/pages/api/_config.ts deleted file mode 100644 index adba894a..00000000 --- a/src/pages/api/_config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; - -export interface ConfigResponse { - telemetryDisabled: boolean; - trackerScriptName: string; - uiDisabled: boolean; - updatesDisabled: boolean; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - telemetryDisabled: !!process.env.DISABLE_TELEMETRY, - trackerScriptName: process.env.TRACKER_SCRIPT_NAME, - uiDisabled: !!process.env.DISABLE_UI, - updatesDisabled: !!process.env.DISABLE_UPDATES, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/_version.ts b/src/pages/api/_version.ts deleted file mode 100644 index 4453b56f..00000000 --- a/src/pages/api/_version.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; -import { CURRENT_VERSION } from 'lib/constants'; - -export interface VersionResponse { - version: string; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - version: CURRENT_VERSION, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/admin/_users.ts b/src/pages/api/admin/_users.ts deleted file mode 100644 index 4f03ec9f..00000000 --- a/src/pages/api/admin/_users.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { canViewUsers } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUsers } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid(), - username: yup.string().max(255).required(), - password: yup.string().required(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewUsers(req.auth))) { - return unauthorized(res); - } - - const users = await getUsers( - { - include: { - _count: { - select: { - websiteUser: { - where: { deletedAt: null }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/admin/_websites.ts b/src/pages/api/admin/_websites.ts deleted file mode 100644 index d7dd6b74..00000000 --- a/src/pages/api/admin/_websites.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { canViewAllWebsites } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsites } from 'queries'; -import * as yup from 'yup'; - -export interface WebsitesRequestQuery extends PageParams { - userId?: string; - includeOwnedTeams?: boolean; - includeAllTeams?: boolean; -} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewAllWebsites(req.auth))) { - return unauthorized(res); - } - - const { userId, includeOwnedTeams, includeAllTeams } = req.query; - - const websites = await getWebsites( - { - where: { - OR: [ - ...(userId && [{ userId }]), - ...(userId && includeOwnedTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - role: ROLES.teamOwner, - userId, - }, - }, - }, - }, - ] - : []), - ...(userId && includeAllTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - userId, - }, - }, - }, - }, - ] - : []), - ], - }, - include: { - user: { - select: { - username: true, - id: true, - }, - }, - team: { - where: { - deletedAt: null, - }, - include: { - teamUser: { - where: { - role: ROLES.teamOwner, - }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/_login.ts b/src/pages/api/auth/_login.ts deleted file mode 100644 index fc671785..00000000 --- a/src/pages/api/auth/_login.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; -import { secret } from 'lib/crypto'; -import { useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - checkPassword, - createSecureToken, - forbidden, - methodNotAllowed, - ok, - unauthorized, -} from 'next-basics'; -import { getUserByUsername } from 'queries'; -import * as yup from 'yup'; -import { ROLES } from 'lib/constants'; - -export interface LoginRequestBody { - username: string; - password: string; -} - -export interface LoginResponse { - token: string; - user: User; -} - -const schema = { - POST: yup.object().shape({ - username: yup.string().required(), - password: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.disableLogin) { - return forbidden(res); - } - - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { username, password } = req.body; - - const user = await getUserByUsername(username, { includePassword: true }); - - if (user && checkPassword(password, user.password)) { - if (redisEnabled) { - const token = await saveAuth({ userId: user.id }); - - return ok(res, { token, user }); - } - - const token = createSecureToken({ userId: user.id }, secret()); - const { id, username, role, createdAt } = user; - - return ok(res, { - token, - user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, - }); - } - - return unauthorized(res, 'message.incorrect-username-password'); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/_logout.ts b/src/pages/api/auth/_logout.ts deleted file mode 100644 index f1604989..00000000 --- a/src/pages/api/auth/_logout.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { methodNotAllowed, ok } from 'next-basics'; -import { getClient, redisEnabled } from '@umami/redis-client'; -import { useAuth } from 'lib/middleware'; -import { getAuthToken } from 'lib/auth'; -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async (req: NextApiRequest, res: NextApiResponse) => { - await useAuth(req, res); - - if (req.method === 'POST') { - if (redisEnabled) { - const redis = getClient(); - - await redis.del(getAuthToken(req)); - } - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/_sso.ts b/src/pages/api/auth/_sso.ts deleted file mode 100644 index c5560cb1..00000000 --- a/src/pages/api/auth/_sso.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, ok } from 'next-basics'; -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - if (redisEnabled && req.auth.user) { - const token = await saveAuth({ userId: req.auth.user.id }, 86400); - - return ok(res, { user: req.auth.user, token }); - } - - return badRequest(res); -}; diff --git a/src/pages/api/auth/_verify.ts b/src/pages/api/auth/_verify.ts deleted file mode 100644 index 3cc78ed3..00000000 --- a/src/pages/api/auth/_verify.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { ok } from 'next-basics'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - const { user } = req.auth; - - return ok(res, user); -}; diff --git a/src/pages/api/me/_index.ts b/src/pages/api/me/_index.ts deleted file mode 100644 index 93e97067..00000000 --- a/src/pages/api/me/_index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextApiResponse } from 'next'; -import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { ok } from 'next-basics'; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - - return ok(res, req.auth.user); -}; diff --git a/src/pages/api/me/_password.ts b/src/pages/api/me/_password.ts deleted file mode 100644 index 2ba91d86..00000000 --- a/src/pages/api/me/_password.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - badRequest, - checkPassword, - forbidden, - hashPassword, - methodNotAllowed, - ok, -} from 'next-basics'; -import { getUser, updateUser } from 'queries'; -import * as yup from 'yup'; - -export interface UserPasswordRequestBody { - currentPassword: string; - newPassword: string; -} - -const schema = { - POST: yup.object().shape({ - currentPassword: yup.string().required(), - newPassword: yup.string().min(8).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.CLOUD_MODE) { - return forbidden(res); - } - - await useAuth(req, res); - await useValidate(schema, req, res); - - const { currentPassword, newPassword } = req.body; - const { id: userId } = req.auth.user; - - if (req.method === 'POST') { - const user = await getUser(userId, { includePassword: true }); - - if (!checkPassword(currentPassword, user.password)) { - return badRequest(res, 'Current password is incorrect'); - } - - const password = hashPassword(newPassword); - - const updated = await updateUser(userId, { password }); - - return ok(res, updated); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/_teams.ts b/src/pages/api/me/_teams.ts deleted file mode 100644 index e40e548c..00000000 --- a/src/pages/api/me/_teams.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userTeamsRoute from 'pages/api/users/[userId]/_teams'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userTeamsRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/_websites.ts b/src/pages/api/me/_websites.ts deleted file mode 100644 index 5c3030e6..00000000 --- a/src/pages/api/me/_websites.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userWebsitesRoute from 'pages/api/users/[userId]/_websites'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userWebsitesRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/realtime/_[websiteId].ts b/src/pages/api/realtime/_[websiteId].ts deleted file mode 100644 index 08e9bc47..00000000 --- a/src/pages/api/realtime/_[websiteId].ts +++ /dev/null @@ -1,43 +0,0 @@ -import { startOfMinute, subMinutes } from 'date-fns'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRealtimeData } from 'queries'; -import * as yup from 'yup'; -import { REALTIME_RANGE } from 'lib/constants'; -import { TimezoneTest } from 'lib/yup'; - -export interface RealtimeRequestQuery { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, timezone } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - - const data = await getRealtimeData(websiteId, { startDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_[reportId].ts b/src/pages/api/reports/_[reportId].ts deleted file mode 100644 index 91b5fb51..00000000 --- a/src/pages/api/reports/_[reportId].ts +++ /dev/null @@ -1,102 +0,0 @@ -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 { deleteReport, getReport, updateReport } from 'queries'; -import * as yup from 'yup'; - -export interface ReportRequestQuery { - reportId: string; -} - -export interface ReportRequestBody { - websiteId: string; - type: ReportType; - name: string; - description: string; - parameters: string; -} - -const schema: YupRequest = { - GET: yup.object().shape({ - reportId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - reportId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/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({ - reportId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { reportId } = req.query; - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const report = await getReport(reportId); - - if (!(await canViewReport(req.auth, report))) { - return unauthorized(res); - } - - report.parameters = JSON.parse(report.parameters); - - return ok(res, report); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - const report = await getReport(reportId); - - if (!(await canUpdateReport(req.auth, report))) { - return unauthorized(res); - } - - const result = await updateReport(reportId, { - websiteId, - userId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - if (req.method === 'DELETE') { - const report = await getReport(reportId); - - if (!(await canDeleteReport(req.auth, report))) { - return unauthorized(res); - } - - await deleteReport(reportId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_funnel.ts b/src/pages/api/reports/_funnel.ts deleted file mode 100644 index 35759a30..00000000 --- a/src/pages/api/reports/_funnel.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getFunnel } from 'queries'; -import * as yup from 'yup'; - -export interface FunnelRequestBody { - websiteId: string; - steps: { type: string; value: string }[]; - window: number; - dateRange: { - startDate: string; - endDate: string; - }; -} - -export interface FunnelResponse { - steps: { type: string; value: string }[]; - window: number; - startAt: number; - endAt: number; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - steps: yup - .array() - .of( - yup.object().shape({ - type: yup.string().required(), - value: yup.string().required(), - }), - ) - .min(2) - .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, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - steps, - window, - dateRange: { startDate, endDate }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getFunnel(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - windowMinutes: +window, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_goals.ts b/src/pages/api/reports/_goals.ts deleted file mode 100644 index f775dc3c..00000000 --- a/src/pages/api/reports/_goals.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getGoals } from 'queries/analytics/reports/getGoals'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; - goals: { type: string; value: string; goal: number }[]; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - goals: yup - .array() - .of( - yup.object().shape({ - type: yup - .string() - .matches(/url|event|event-data/i) - .required(), - value: yup.string().required(), - goal: yup.number().required(), - operator: yup - .string() - .matches(/count|sum|average/i) - .when('type', { - is: 'eventData', - then: yup.string().required(), - }), - property: yup.string().when('type', { - is: 'eventData', - then: yup.string().required(), - }), - }), - ) - .min(1) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - goals, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getGoals(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - goals, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_index.ts b/src/pages/api/reports/_index.ts deleted file mode 100644 index 38996b7a..00000000 --- a/src/pages/api/reports/_index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getReports } from 'queries'; -import * as yup from 'yup'; -import { canUpdateWebsite, canViewTeam, canViewWebsite } from 'lib/auth'; - -export interface ReportRequestBody { - websiteId: string; - name: string; - type: string; - description: string; - parameters: { - [key: string]: any; - }; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string().max(200).required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/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, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const { page, query, pageSize, websiteId, teamId } = req.query; - const filters = { - page, - pageSize, - query, - }; - - if ( - (websiteId && !(await canViewWebsite(req.auth, websiteId))) || - (teamId && !(await canViewTeam(req.auth, teamId))) - ) { - return unauthorized(res); - } - - const data = await getReports( - { - where: { - OR: [ - ...(websiteId ? [{ websiteId }] : []), - ...(teamId - ? [ - { - website: { - deletedAt: null, - teamId, - }, - }, - ] - : []), - ...(userId && !websiteId && !teamId - ? [ - { - website: { - deletedAt: null, - userId, - }, - }, - ] - : []), - ], - }, - include: { - website: { - select: { - domain: true, - }, - }, - }, - }, - filters, - ); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await createReport({ - id: uuid(), - userId, - websiteId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_insights.ts b/src/pages/api/reports/_insights.ts deleted file mode 100644 index ba4f643e..00000000 --- a/src/pages/api/reports/_insights.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getInsights } from 'queries'; -import * as yup from 'yup'; - -export interface InsightsRequestBody { - websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; - fields: { name: string; type: string; label: string }[]; - filters: { name: string; type: string; operator: string; value: string }[]; - 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(), - label: yup.string().required(), - }), - ) - .min(1) - .required(), - filters: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - operator: yup.string().required(), - value: yup.string().required(), - }), - ), - groups: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - }), - ), - }), -}; - -function convertFilters(filters: any[]) { - return filters.reduce((obj, filter) => { - obj[filter.name] = filter; - - return obj; - }, {}); -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - fields, - filters, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getInsights(websiteId, fields, { - ...convertFilters(filters), - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_journey.ts b/src/pages/api/reports/_journey.ts deleted file mode 100644 index dd3bd57b..00000000 --- a/src/pages/api/reports/_journey.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getJourney } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - steps: number; - startStep?: string; - endStep?: 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(), - steps: yup.number().min(3).max(7).required(), - startStep: yup.string(), - endStep: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - steps, - startStep, - endStep, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getJourney(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - startStep, - endStep, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_retention.ts b/src/pages/api/reports/_retention.ts deleted file mode 100644 index f4d9b7df..00000000 --- a/src/pages/api/reports/_retention.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRetention } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - timezone: 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(), - timezone: TimezoneTest, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - timezone, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRetention(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_revenue.ts b/src/pages/api/reports/_revenue.ts deleted file mode 100644 index d23ce55a..00000000 --- a/src/pages/api/reports/_revenue.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRevenue } from 'queries/analytics/reports/getRevenue'; -import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues'; -import * as yup from 'yup'; - -export interface RevenueRequestBody { - websiteId: string; - currency?: string; - timezone?: string; - dateRange: { startDate: string; endDate: string; unit?: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - unit: UnitTypeTest, - }) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startDate, endDate } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenueValues(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { - websiteId, - currency, - timezone, - dateRange: { startDate, endDate, unit }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenue(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - unit, - timezone, - currency, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_utm.ts b/src/pages/api/reports/_utm.ts deleted file mode 100644 index 59399ee4..00000000 --- a/src/pages/api/reports/_utm.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUTM } from 'queries'; -import * as yup from 'yup'; - -export interface UTMRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate, timezone }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getUTM(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts deleted file mode 100644 index a47b91d2..00000000 --- a/src/pages/api/send.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* eslint-disable no-console */ -import { isbot } from 'isbot'; -import { NextApiRequest, NextApiResponse } from 'next'; -import { - badRequest, - createToken, - forbidden, - methodNotAllowed, - ok, - safeDecodeURI, -} from 'next-basics'; -import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants'; -import { secret, visitSalt, uuid } from 'lib/crypto'; -import { hasBlockedIp } from 'lib/detect'; -import { useCors, useSession, useValidate } from 'lib/middleware'; -import { CollectionType, YupRequest } from 'lib/types'; -import { saveEvent, saveSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface CollectRequestBody { - payload: { - website: string; - data?: { [key: string]: any }; - hostname?: string; - language?: string; - name?: string; - referrer?: string; - screen?: string; - tag?: string; - title?: string; - url: string; - ip?: string; - userAgent?: string; - }; - type: CollectionType; -} - -export interface NextApiRequestCollect extends NextApiRequest { - body: CollectRequestBody; - session: { - id: string; - websiteId: string; - visitId: string; - hostname: string; - browser: string; - os: string; - device: string; - screen: string; - language: string; - country: string; - subdivision1: string; - subdivision2: string; - city: string; - iat: number; - }; - 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(), - screen: yup.string().max(11), - title: yup.string(), - url: yup.string(), - website: yup.string().uuid().required(), - name: yup.string().max(50), - tag: yup.string().max(50).nullable(), - ip: yup.string().matches(IP_REGEX), - userAgent: yup.string(), - }) - .required(), - type: yup - .string() - .matches(/event|identify/i) - .required(), - }), -}; - -export default async (req: NextApiRequestCollect, res: NextApiResponse) => { - await useCors(req, res); - - if (req.method === 'POST') { - if (!process.env.DISABLE_BOT_CHECK && isbot(req.headers['user-agent'])) { - return ok(res, { beep: 'boop' }); - } - - await useValidate(schema, req, res); - - if (hasBlockedIp(req)) { - return forbidden(res); - } - - const { type, payload } = req.body; - const { url, referrer, name, data, title, tag } = payload; - - await useSession(req, res); - - const session = req.session; - - if (!session?.id || !session?.websiteId) { - return ok(res, {}); - } - - const iat = Math.floor(new Date().getTime() / 1000); - - // expire visitId after 30 minutes - if (session.iat && iat - session.iat > 1800) { - session.visitId = uuid(session.id, visitSalt()); - } - - session.iat = iat; - - if (type === COLLECTION_TYPE.event) { - // eslint-disable-next-line prefer-const - let [urlPath, urlQuery] = safeDecodeURI(url)?.split('?') || []; - let [referrerPath, referrerQuery] = safeDecodeURI(referrer)?.split('?') || []; - let referrerDomain = ''; - - if (!urlPath) { - urlPath = '/'; - } - - if (/^[\w-]+:\/\/\w+/.test(referrerPath)) { - const refUrl = new URL(referrer); - referrerPath = refUrl.pathname; - referrerQuery = refUrl.search.substring(1); - referrerDomain = refUrl.hostname.replace(/www\./, ''); - } - - if (process.env.REMOVE_TRAILING_SLASH) { - urlPath = urlPath.replace(/(.+)\/$/, '$1'); - } - - await saveEvent({ - urlPath, - urlQuery, - referrerPath, - referrerQuery, - referrerDomain, - pageTitle: title, - eventName: name, - eventData: data, - ...session, - sessionId: session.id, - tag, - }); - } else if (type === COLLECTION_TYPE.identify) { - if (!data) { - return badRequest(res, 'Data required.'); - } - - await saveSessionData({ - websiteId: session.websiteId, - sessionId: session.id, - sessionData: data, - }); - } - - const cache = createToken(session, secret()); - - return ok(res, { cache }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/share/_[shareId].ts b/src/pages/api/share/_[shareId].ts deleted file mode 100644 index 26ac4cdc..00000000 --- a/src/pages/api/share/_[shareId].ts +++ /dev/null @@ -1,46 +0,0 @@ -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 { getSharedWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface ShareRequestQuery { - shareId: string; -} - -export interface ShareResponse { - shareId: string; - token: string; -} - -const schema = { - GET: yup.object().shape({ - shareId: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useValidate(schema, req, res); - - const { shareId } = req.query; - - if (req.method === 'GET') { - const website = await getSharedWebsite(shareId); - - if (website) { - const data = { websiteId: website.id }; - const token = createToken(data, secret()); - - return ok(res, { ...data, token }); - } - - return notFound(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/_index.ts b/src/pages/api/teams/[teamId]/_index.ts deleted file mode 100644 index b731ee0c..00000000 --- a/src/pages/api/teams/[teamId]/_index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Team } from '@prisma/client'; -import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, notFound, ok, unauthorized } from 'next-basics'; -import { deleteTeam, getTeam, updateTeam } from 'queries'; -import * as yup from 'yup'; - -export interface TeamRequestQuery { - teamId: string; -} - -export interface TeamRequestBody { - name: string; - accessCode: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - id: yup.string().uuid().required(), - name: yup.string().max(50), - accessCode: yup.string().max(50), - }), - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const team = await getTeam(teamId, { includeMembers: true }); - - if (!team) { - return notFound(res); - } - - return ok(res, team); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const { name, accessCode } = req.body; - const data = { name, accessCode }; - - const updated = await updateTeam(teamId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - await deleteTeam(teamId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/_[userId].ts b/src/pages/api/teams/[teamId]/users/_[userId].ts deleted file mode 100644 index c1e80b1a..00000000 --- a/src/pages/api/teams/[teamId]/users/_[userId].ts +++ /dev/null @@ -1,85 +0,0 @@ -import { canDeleteTeamUser, canUpdateTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteTeamUser, getTeamUser, updateTeamUser } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery { - teamId: string; - userId: string; -} - -export interface TeamUserRequestBody { - role: string; -} - -const schema = { - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId, userId } = req.query; - - if (req.method === 'GET') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - return ok(res, teamUser); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - const { role } = req.body; - - await updateTeamUser(teamUser.id, { role }); - - return ok(res); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeamUser(req.auth, teamId, userId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - await deleteTeamUser(teamId, userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/_index.ts b/src/pages/api/teams/[teamId]/users/_index.ts deleted file mode 100644 index f25b99da..00000000 --- a/src/pages/api/teams/[teamId]/users/_index.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { canAddUserToTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getTeamUser, getTeamUsers } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery extends PageParams { - teamId: string; -} - -export interface TeamUserRequestBody { - userId: string; - role: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const users = await getTeamUsers( - { - where: { - teamId, - user: { - deletedAt: null, - }, - }, - include: { - user: { - select: { - id: true, - username: true, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - // admin function only - if (req.method === 'POST') { - if (!(await canAddUserToTeam(req.auth))) { - return unauthorized(res); - } - - const { userId, role } = req.body; - - const teamUser = await getTeamUser(teamId, userId); - - if (teamUser) { - return badRequest(res, 'User is already a member of the Team.'); - } - - const users = await createTeamUser(userId, teamId, role); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/websites/_index.ts b/src/pages/api/teams/[teamId]/websites/_index.ts deleted file mode 100644 index 75020fa4..00000000 --- a/src/pages/api/teams/[teamId]/websites/_index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as yup from 'yup'; -import { canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { ok, unauthorized } from 'next-basics'; -import { getTeamWebsites } from 'queries'; - -export interface TeamWebsiteRequestQuery extends PageParams { - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const websites = await getTeamWebsites(teamId, req.query); - - return ok(res, websites); - } -}; diff --git a/src/pages/api/teams/_index.ts b/src/pages/api/teams/_index.ts deleted file mode 100644 index 1e683469..00000000 --- a/src/pages/api/teams/_index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as yup from 'yup'; -import { Team } from '@prisma/client'; -import { canCreateTeam } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeam } from 'queries'; - -export interface TeamsRequestQuery extends PageParams {} -export interface TeamsRequestBody { - name: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(50).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'POST') { - if (!(await canCreateTeam(req.auth))) { - return unauthorized(res); - } - - const { name } = req.body; - - const team = await createTeam( - { - id: uuid(), - name, - accessCode: getRandomChars(16), - }, - userId, - ); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/_join.ts b/src/pages/api/teams/_join.ts deleted file mode 100644 index a9943f64..00000000 --- a/src/pages/api/teams/_join.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Team } from '@prisma/client'; -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, findTeam, 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); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { accessCode } = req.body; - - const team = await findTeam({ - where: { - accessCode, - }, - }); - - if (!team) { - return notFound(res, 'message.team-not-found'); - } - - const teamUser = await getTeamUser(team.id, req.auth.user.id); - - if (teamUser) { - return methodNotAllowed(res, 'message.team-already-member'); - } - - await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_index.ts b/src/pages/api/users/[userId]/_index.ts deleted file mode 100644 index d69cad3c..00000000 --- a/src/pages/api/users/[userId]/_index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as yup from 'yup'; -import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; -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, getUser, getUserByUsername, updateUser } from 'queries'; - -export interface UserRequestQuery { - userId: string; -} - -export interface UserRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - username: yup.string().max(255), - password: yup.string(), - role: yup.string().matches(/admin|user|view-only/i), - }), - DELETE: yup.object().shape({ - userId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { isAdmin }, - } = req.auth; - const userId: string = req.query.userId; - - if (req.method === 'GET') { - if (!(await canViewUser(req.auth, userId))) { - return unauthorized(res); - } - - const user = await getUser(userId); - - return ok(res, user); - } - - if (req.method === 'POST') { - if (!(await canUpdateUser(req.auth, userId))) { - return unauthorized(res); - } - - const { username, password, role } = req.body; - - const user = await getUser(userId); - - const data: any = {}; - - if (password) { - data.password = hashPassword(password); - } - - // Only admin can change these fields - if (role && isAdmin) { - data.role = role; - } - - if (username && isAdmin) { - data.username = username; - } - - // Check when username changes - if (data.username && user.username !== data.username) { - const user = await getUserByUsername(username); - - if (user) { - return badRequest(res, 'User already exists'); - } - } - - const updated = await updateUser(userId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteUser(req.auth))) { - return unauthorized(res); - } - - if (userId === req.auth.user.id) { - return badRequest(res, 'You cannot delete yourself.'); - } - - await deleteUser(userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_teams.ts b/src/pages/api/users/[userId]/_teams.ts deleted file mode 100644 index 3f2af9e2..00000000 --- a/src/pages/api/users/[userId]/_teams.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserTeams } from 'queries'; - -export interface UserTeamsRequestQuery extends PageParams { - userId: string; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && (!userId || user.id !== userId)) { - return unauthorized(res); - } - - const teams = await getUserTeams(userId as string, req.query); - - return ok(res, teams); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_usage.ts b/src/pages/api/users/[userId]/_usage.ts deleted file mode 100644 index b5000395..00000000 --- a/src/pages/api/users/[userId]/_usage.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getAllUserWebsitesIncludingTeamOwner, getEventDataUsage, getEventUsage } from 'queries'; -import * as yup from 'yup'; - -export interface UserUsageRequestQuery { - userId: string; - startAt: string; - endAt: string; -} - -export interface UserUsageRequestResponse { - websiteEventUsage: number; - eventDataUsage: number; - websites: { - websiteEventUsage: number; - eventDataUsage: number; - websiteId: string; - websiteName: string; - }[]; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - - if (req.method === 'GET') { - if (!user.isAdmin) { - return unauthorized(res); - } - - const { userId, startAt, endAt } = req.query; - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const websites = await getAllUserWebsitesIncludingTeamOwner(userId); - - const websiteIds = websites.map(a => a.id); - - const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); - const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); - - const websiteUsage = websites.map(a => ({ - websiteId: a.id, - websiteName: a.name, - websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, - eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, - deletedAt: a.deletedAt, - })); - - const usage = websiteUsage.reduce( - (acc, cv) => { - acc.websiteEventUsage += cv.websiteEventUsage; - acc.eventDataUsage += cv.eventDataUsage; - - return acc; - }, - { websiteEventUsage: 0, eventDataUsage: 0 }, - ); - - const filteredWebsiteUsage = websiteUsage.filter( - a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), - ); - - return ok(res, { - ...usage, - websites: filteredWebsiteUsage, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_websites.ts b/src/pages/api/users/[userId]/_websites.ts deleted file mode 100644 index 88a2bad1..00000000 --- a/src/pages/api/users/[userId]/_websites.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserWebsites } from 'queries'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId, page = 1, pageSize, query = '', ...rest } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && user.id !== userId) { - return unauthorized(res); - } - - const websites = await getUserWebsites(userId, { - page, - pageSize, - query, - ...rest, - }); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/_index.ts b/src/pages/api/users/_index.ts deleted file mode 100644 index 333670a9..00000000 --- a/src/pages/api/users/_index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canCreateUser } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createUser, getUserByUsername } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - username: string; - password: string; - id: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - 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); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - if (!(await canCreateUser(req.auth))) { - return unauthorized(res); - } - - const { username, password, role, id } = req.body; - - const existingUser = await getUserByUsername(username, { showDeleted: true }); - - if (existingUser) { - return badRequest(res, 'User already exists'); - } - - const created = await createUser({ - id: id || uuid(), - username, - password: hashPassword(password), - role: role ?? ROLES.user, - }); - - return ok(res, created); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_active.ts b/src/pages/api/websites/[websiteId]/_active.ts deleted file mode 100644 index d87a7818..00000000 --- a/src/pages/api/websites/[websiteId]/_active.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -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 { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId as string))) { - return unauthorized(res); - } - - const result = await getActiveVisitors(websiteId as string); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_daterange.ts b/src/pages/api/websites/[websiteId]/_daterange.ts deleted file mode 100644 index 1aeb76cb..00000000 --- a/src/pages/api/websites/[websiteId]/_daterange.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -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 { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await getWebsiteDateRange(websiteId); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_index.ts b/src/pages/api/websites/[websiteId]/_index.ts deleted file mode 100644 index c60a8399..00000000 --- a/src/pages/api/websites/[websiteId]/_index.ts +++ /dev/null @@ -1,87 +0,0 @@ -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, useValidate } from 'lib/middleware'; -import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; -import { SHARE_ID_REGEX } from 'lib/constants'; - -export interface WebsiteRequestQuery { - websiteId: string; -} - -export interface WebsiteRequestBody { - name: string; - domain: string; - shareId: string; -} - -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string(), - domain: yup.string(), - shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }).nullable(), - }), - DELETE: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const website = await getWebsite(websiteId); - - return ok(res, website); - } - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { name, domain, shareId } = req.body; - - try { - const website = await updateWebsite(websiteId, { name, domain, shareId }); - - return ok(res, website); - } catch (e: any) { - if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { - return serverError(res, 'That share ID is already taken.'); - } - - return serverError(res, e); - } - } - - if (req.method === 'DELETE') { - if (!(await canDeleteWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await deleteWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_metrics.ts b/src/pages/api/websites/[websiteId]/_metrics.ts deleted file mode 100644 index 1996a61a..00000000 --- a/src/pages/api/websites/[websiteId]/_metrics.ts +++ /dev/null @@ -1,125 +0,0 @@ -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, useValidate } from 'lib/middleware'; -import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants'; -import { getPageviewMetrics, getSessionMetrics } from 'queries'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface WebsiteMetricsRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - language?: string; - event?: string; - limit?: number; - offset?: number; - search?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - language: yup.string(), - event: yup.string(), - limit: yup.number(), - offset: yup.number(), - search: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, limit, offset, search } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const column = FILTER_COLUMNS[type] || type; - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - }; - - if (search) { - filters[type] = { - name: type, - column, - operator: OPERATORS.contains, - value: search, - }; - } - - if (SESSION_COLUMNS.includes(type)) { - const data = await getSessionMetrics(websiteId, type, filters, limit, offset); - - if (type === 'language') { - const combined = {}; - - for (const { x, y } of data) { - const key = String(x).toLowerCase().split('-')[0]; - - if (combined[key] === undefined) { - combined[key] = { x: key, y }; - } else { - combined[key].y += y; - } - } - - return ok(res, Object.values(combined)); - } - - return ok(res, data); - } - - if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); - - return ok(res, data); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_pageviews.ts b/src/pages/api/websites/[websiteId]/_pageviews.ts deleted file mode 100644 index c3b6b797..00000000 --- a/src/pages/api/websites/[websiteId]/_pageviews.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getPageviewStats, getSessionStats } from 'queries'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { getCompareDate } from 'lib/date'; - -export interface WebsitePageviewRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - unit?: string; - timezone?: string; - url?: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate, unit } = await getRequestDateRange(req); - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, filters), - getSessionStats(websiteId, filters), - ]); - - if (compare) { - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const [comparePageviews, compareSessions] = await Promise.all([ - getPageviewStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - getSessionStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - ]); - - return ok(res, { - pageviews, - sessions, - startDate, - endDate, - compare: { - pageviews: comparePageviews, - sessions: compareSessions, - startDate: compareStartDate, - endDate: compareEndDate, - }, - }); - } - - return ok(res, { pageviews, sessions }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_reports.ts b/src/pages/api/websites/[websiteId]/_reports.ts deleted file mode 100644 index 86260634..00000000 --- a/src/pages/api/websites/[websiteId]/_reports.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteReports } from 'queries'; -import { pageInfo } from 'lib/schema'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { page, search, pageSize } = req.query; - - const data = await getWebsiteReports(websiteId, { - page, - pageSize, - search, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_reset.ts b/src/pages/api/websites/[websiteId]/_reset.ts deleted file mode 100644 index 82e769dc..00000000 --- a/src/pages/api/websites/[websiteId]/_reset.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canUpdateWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { resetWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteResetRequestQuery { - websiteId: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await resetWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_stats.ts b/src/pages/api/websites/[websiteId]/_stats.ts deleted file mode 100644 index dfc9198d..00000000 --- a/src/pages/api/websites/[websiteId]/_stats.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as yup from 'yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { getWebsiteStats } from 'queries'; -import { getCompareDate } from 'lib/date'; - -export interface WebsiteStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const prevPeriod = await getWebsiteStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - prev: Number(prevPeriod[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_transfer.ts b/src/pages/api/websites/[websiteId]/_transfer.ts deleted file mode 100644 index 56cf6bac..00000000 --- a/src/pages/api/websites/[websiteId]/_transfer.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { updateWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteTransferRequestQuery { - websiteId: string; -} - -export interface WebsiteTransferRequestBody { - userId?: string; - teamId?: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - userId: yup.string().uuid(), - teamId: yup.string().uuid(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - const { userId, teamId } = req.body; - - if (req.method === 'POST') { - if (userId) { - if (!(await canTransferWebsiteToUser(req.auth, websiteId, userId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId, - teamId: null, - }); - - return ok(res, website); - } else if (teamId) { - if (!(await canTransferWebsiteToTeam(req.auth, websiteId, teamId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId: null, - teamId, - }); - - return ok(res, website); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_values.ts b/src/pages/api/websites/[websiteId]/_values.ts deleted file mode 100644 index 53d717a5..00000000 --- a/src/pages/api/websites/[websiteId]/_values.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { - badRequest, - methodNotAllowed, - ok, - safeDecodeURIComponent, - unauthorized, -} from 'next-basics'; -import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { getValues } from 'queries'; -import { getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface ValuesRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - search?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - search: yup.string(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, search } = req.query; - const { startDate, endDate } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!SESSION_COLUMNS.includes(type as string) && !EVENT_COLUMNS.includes(type as string)) { - return badRequest(res); - } - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); - - return ok( - res, - values - .map(({ value }) => safeDecodeURIComponent(value)) - .filter(n => n) - .sort(), - ); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_events.ts b/src/pages/api/websites/[websiteId]/event-data/_events.ts deleted file mode 100644 index bf0f409a..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_events.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataEvents } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: 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().min(yup.ref('startAt')).required(), - event: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, event } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataEvents(websiteId, { - startDate, - endDate, - event, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_fields.ts b/src/pages/api/websites/[websiteId]/event-data/_fields.ts deleted file mode 100644 index c5075c5e..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_fields.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataFields } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: 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().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataFields(websiteId, { - startDate, - endDate, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_properties.ts b/src/pages/api/websites/[websiteId]/event-data/_properties.ts deleted file mode 100644 index 19e9bbb8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_stats.ts b/src/pages/api/websites/[websiteId]/event-data/_stats.ts deleted file mode 100644 index 7e440b88..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_stats.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -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 { - websiteId: 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().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataStats(websiteId, { startDate, endDate }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_values.ts b/src/pages/api/websites/[websiteId]/event-data/_values.ts deleted file mode 100644 index e5bb4ab8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_values.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - eventName?: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - eventName: yup.string(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, eventName, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataValues(websiteId, { - startDate, - endDate, - eventName, - propertyName, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/_index.ts b/src/pages/api/websites/[websiteId]/events/_index.ts deleted file mode 100644 index 13b31fc0..00000000 --- a/src/pages/api/websites/[websiteId]/events/_index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteEvents } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteEvents(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/_series.ts b/src/pages/api/websites/[websiteId]/events/_series.ts deleted file mode 100644 index 6d67a264..00000000 --- a/src/pages/api/websites/[websiteId]/events/_series.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventMetrics } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteEventsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - unit?: string; - timezone?: string; - url: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone } = req.query; - const { startDate, endDate, unit } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const events = await getEventMetrics(websiteId, filters); - - return ok(res, events); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/_properties.ts b/src/pages/api/websites/[websiteId]/session-data/_properties.ts deleted file mode 100644 index 92e182d2..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/_properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/_values.ts b/src/pages/api/websites/[websiteId]/session-data/_values.ts deleted file mode 100644 index 98463f15..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/_values.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataValues(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts deleted file mode 100644 index 2b0fc084..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionActivity } from 'queries'; - -export interface SessionActivityRequestQuery extends PageParams { - websiteId: string; - sessionId: string; - startAt: number; - endAt: number; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - startAt: yup.number().integer(), - endAt: yup.number().integer(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts deleted file mode 100644 index f627a208..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSession } from 'queries'; - -export interface WesiteSessionRequestQuery extends PageParams { - websiteId: string; - sessionId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getWebsiteSession(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts deleted file mode 100644 index c0c20064..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataRequestQuery { - sessionId: string; - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - sessionId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, sessionId } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getSessionData(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/_index.ts b/src/pages/api/websites/[websiteId]/sessions/_index.ts deleted file mode 100644 index 1809929c..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/_index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessions } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessions(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/_stats.ts b/src/pages/api/websites/[websiteId]/sessions/_stats.ts deleted file mode 100644 index fe92ce6f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/_stats.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSessionStats } from 'queries/analytics/sessions/getWebsiteSessionStats'; -import * as yup from 'yup'; - -export interface WebsiteSessionStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteSessionStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/_weekly.ts b/src/pages/api/websites/[websiteId]/sessions/_weekly.ts deleted file mode 100644 index b1c28c3f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/_weekly.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessionsWeekly } from 'queries'; -import { TimezoneTest } from 'lib/yup'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - timezone: TimezoneTest, - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt, timezone } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/_index.ts b/src/pages/api/websites/_index.ts deleted file mode 100644 index 483b77cd..00000000 --- a/src/pages/api/websites/_index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createWebsite } from 'queries'; -import userWebsitesRoute from 'pages/api/users/[userId]/_websites'; -import * as yup from 'yup'; -import { pageInfo } from 'lib/schema'; - -export interface WebsitesRequestQuery extends PageParams {} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - teamId: yup.string().nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - if (!req.query.userId) { - req.query.userId = userId; - } - - return userWebsitesRoute(req, res); - } - - if (req.method === 'POST') { - const { name, domain, shareId, teamId } = req.body; - - if ( - (teamId && !(await canCreateTeamWebsite(req.auth, teamId))) || - !(await canCreateWebsite(req.auth)) - ) { - return unauthorized(res); - } - - const data: any = { - id: uuid(), - createdBy: userId, - name, - domain, - shareId, - teamId, - }; - - if (!teamId) { - data.userId = userId; - } - - const website = await createWebsite(data); - - return ok(res, website); - } - - return methodNotAllowed(res); -}; From aaf8b1935f712995c1fe9acf4f96dab9bbd98203 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 31 Jan 2025 00:27:22 -0800 Subject: [PATCH 09/17] Fixed auth check. --- next-env.d.ts | 1 - src/lib/detect.ts | 2 +- src/lib/request.ts | 9 ++++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index 725dd6f2..40c3d680 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/src/lib/detect.ts b/src/lib/detect.ts index fe1c2124..83504095 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -86,7 +86,7 @@ function decodeHeader(s: string | undefined | null): string | undefined | null { return Buffer.from(s, 'latin1').toString('utf-8'); } -export async function getLocation(ip: string, headers: Headers) { +export async function getLocation(ip: string = '', headers: Headers) { // Ignore local ips if (await isLocalhost(ip)) { return; diff --git a/src/lib/request.ts b/src/lib/request.ts index dc721225..c71684b9 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -22,6 +22,7 @@ export async function parseRequest( let query = Object.fromEntries(url.searchParams); let body = await getJsonBody(request); let error: () => void | undefined; + let auth = null; if (schema) { const isGet = request.method === 'GET'; @@ -36,10 +37,12 @@ export async function parseRequest( } } - const auth = !error && !options?.skipAuth ? await checkAuth(request) : null; + if (!options?.skipAuth && !error) { + auth = await checkAuth(request); - if (!error && !auth) { - error = () => unauthorized(); + if (!auth) { + error = () => unauthorized(); + } } return { url, query, body, auth, error }; From edd72cd6e33b91065828941d6b6f92af41eebb44 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 31 Jan 2025 14:29:24 -0800 Subject: [PATCH 10/17] Fixed visit logic. --- src/app/api/send/route.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 864ac375..e70fed0b 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -118,12 +118,14 @@ export async function POST(request: Request) { } // Visit info + const now = Math.floor(new Date().getTime() / 1000); let visitId = cache?.visitId || uuid(sessionId, visitSalt()); - const iat = Math.floor(new Date().getTime() / 1000); + let iat = cache?.iat || now; // Expire visit after 30 minutes - if (cache?.iat && iat - cache?.iat > 1800) { + if (now - iat > 1800) { visitId = uuid(sessionId, visitSalt()); + iat = now; } if (type === COLLECTION_TYPE.event) { @@ -187,5 +189,5 @@ export async function POST(request: Request) { const token = createToken({ websiteId, sessionId, visitId, iat }, secret()); - return json(token); + return json({ cache: token }); } From f3e733dea32e197261e8e728ad9a1d87dbf784ae Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 31 Jan 2025 23:46:23 -0800 Subject: [PATCH 11/17] Updated report schemas. Removed yup. --- package.json | 1 - src/app/api/reports/funnel/route.ts | 9 ++--- src/app/api/reports/goals/route.ts | 7 ++-- src/app/api/reports/insights/route.ts | 7 ++-- src/app/api/reports/journey/route.ts | 7 ++-- src/app/api/reports/retention/route.ts | 8 ++--- src/app/api/reports/revenue/route.ts | 8 ++--- src/app/api/reports/utm/route.ts | 9 ++--- src/lib/detect.ts | 2 +- src/lib/schema.ts | 47 ++++++++++++-------------- src/lib/types.ts | 22 ------------ src/lib/yup.ts | 18 ---------- yarn.lock | 38 --------------------- 13 files changed, 37 insertions(+), 146 deletions(-) delete mode 100644 src/lib/yup.ts diff --git a/package.json b/package.json index 1b81c514..afab156b 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,6 @@ "serialize-error": "^12.0.0", "thenby": "^1.3.4", "uuid": "^9.0.0", - "yup": "^0.32.11", "zod": "^3.24.1", "zustand": "^4.5.5" }, diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts index 9a0cfd65..23a05014 100644 --- a/src/app/api/reports/funnel/route.ts +++ b/src/app/api/reports/funnel/route.ts @@ -3,10 +3,12 @@ import { canViewWebsite } from 'lib/auth'; import { unauthorized, json } from 'lib/response'; import { parseRequest } from 'lib/request'; import { getFunnel } from 'queries'; +import { reportParms } from 'lib/schema'; export async function POST(request: Request) { const schema = z.object({ - websiteId: z.string().uuid(), + ...reportParms, + window: z.number().positive(), steps: z .array( z.object({ @@ -15,11 +17,6 @@ export async function POST(request: Request) { }), ) .min(2), - window: z.number().positive(), - dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), - }), }); const { auth, body, error } = await parseRequest(request, schema); diff --git a/src/app/api/reports/goals/route.ts b/src/app/api/reports/goals/route.ts index ee102bc6..cad774bd 100644 --- a/src/app/api/reports/goals/route.ts +++ b/src/app/api/reports/goals/route.ts @@ -3,14 +3,11 @@ import { canViewWebsite } from 'lib/auth'; import { unauthorized, json } from 'lib/response'; import { parseRequest } from 'lib/request'; import { getGoals } from 'queries/analytics/reports/getGoals'; +import { reportParms } from 'lib/schema'; export async function POST(request: Request) { const schema = z.object({ - websiteId: z.string().uuid(), - dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), - }), + ...reportParms, goals: z .array( z.object({ diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts index f622de6a..a7ed1c15 100644 --- a/src/app/api/reports/insights/route.ts +++ b/src/app/api/reports/insights/route.ts @@ -3,6 +3,7 @@ import { canViewWebsite } from 'lib/auth'; import { unauthorized, json } from 'lib/response'; import { parseRequest } from 'lib/request'; import { getInsights } from 'queries'; +import { reportParms } from 'lib/schema'; function convertFilters(filters: any[]) { return filters.reduce((obj, filter) => { @@ -14,11 +15,7 @@ function convertFilters(filters: any[]) { export async function POST(request: Request) { const schema = z.object({ - websiteId: z.string().uuid(), - dateRange: z.object({ - startDate: z.coerce.date(), - endDate: z.coerce.date(), - }), + ...reportParms, fields: z .array( z.object({ diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts index 50b64952..f5121fdc 100644 --- a/src/app/api/reports/journey/route.ts +++ b/src/app/api/reports/journey/route.ts @@ -3,14 +3,11 @@ import { canViewWebsite } from 'lib/auth'; import { unauthorized, json } from 'lib/response'; import { parseRequest } from 'lib/request'; import { getJourney } from 'queries'; +import { reportParms } from 'lib/schema'; export async function POST(request: Request) { const schema = z.object({ - websiteId: z.string().uuid(), - dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), - }), + ...reportParms, steps: z.number().min(3).max(7), startStep: z.string(), endStep: z.string(), diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts index 794ebbe4..8e854fa9 100644 --- a/src/app/api/reports/retention/route.ts +++ b/src/app/api/reports/retention/route.ts @@ -3,15 +3,11 @@ import { canViewWebsite } from 'lib/auth'; import { unauthorized, json } from 'lib/response'; import { parseRequest } from 'lib/request'; import { getRetention } from 'queries'; -import { timezoneParam } from 'lib/schema'; +import { reportParms, timezoneParam } from 'lib/schema'; export async function POST(request: Request) { const schema = z.object({ - websiteId: z.string().uuid(), - dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), - }), + ...reportParms, timezone: timezoneParam, }); diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts index 0f8f7d55..d3c21b9c 100644 --- a/src/app/api/reports/revenue/route.ts +++ b/src/app/api/reports/revenue/route.ts @@ -2,17 +2,13 @@ import { z } from 'zod'; import { canViewWebsite } from 'lib/auth'; import { unauthorized, json } from 'lib/response'; import { parseRequest } from 'lib/request'; -import { timezoneParam, unitParam } from 'lib/schema'; +import { reportParms, timezoneParam, unitParam } from 'lib/schema'; import { getRevenue } from 'queries/analytics/reports/getRevenue'; import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues'; export async function GET(request: Request) { const schema = z.object({ - websiteId: z.string().uuid(), - dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), - }), + ...reportParms, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts index 0af8b419..2412134d 100644 --- a/src/app/api/reports/utm/route.ts +++ b/src/app/api/reports/utm/route.ts @@ -3,16 +3,11 @@ import { canViewWebsite } from 'lib/auth'; import { unauthorized, json } from 'lib/response'; import { parseRequest } from 'lib/request'; import { getUTM } from 'queries'; -import { timezoneParam } from 'lib/schema'; +import { reportParms } from 'lib/schema'; export async function POST(request: Request) { const schema = z.object({ - websiteId: z.string().uuid(), - dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), - timezone: timezoneParam, - }), + ...reportParms, }); const { auth, body, error } = await parseRequest(request, schema); diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 83504095..89e73a0b 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -148,7 +148,7 @@ export async function getClientInfo(request: Request, payload: Record; - 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 { - auth?: Auth; - headers: any; -} - export interface User { id: string; username: string; diff --git a/src/lib/yup.ts b/src/lib/yup.ts deleted file mode 100644 index d2652eda..00000000 --- a/src/lib/yup.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as yup from 'yup'; -import { isValidTimezone } from 'lib/date'; -import { UNIT_TYPES } from './constants'; - -export const TimezoneTest = yup - .string() - .default('UTC') - .test( - 'timezone', - () => `Invalid timezone`, - value => isValidTimezone(value), - ); - -export const UnitTypeTest = yup.string().test( - 'unit', - () => `Invalid unit`, - value => UNIT_TYPES.includes(value), -); diff --git a/yarn.lock b/yarn.lock index fb739910..91546c34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2968,11 +2968,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.14.175": - version "4.14.200" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149" - integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q== - "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -7610,11 +7605,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -8042,11 +8032,6 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoclone@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== - nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" @@ -9164,11 +9149,6 @@ prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -property-expr@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" - integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== - proxy-from-env@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" @@ -10592,11 +10572,6 @@ topojson-client@^3.1.0: dependencies: commander "2" -toposort@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" - integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== - tough-cookie@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" @@ -11197,19 +11172,6 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yup@^0.32.11: - version "0.32.11" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== - dependencies: - "@babel/runtime" "^7.15.4" - "@types/lodash" "^4.14.175" - lodash "^4.17.21" - lodash-es "^4.17.21" - nanoclone "^0.2.1" - property-expr "^2.0.4" - toposort "^2.0.2" - zod@^3.24.1: version "3.24.1" resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" From 3e9cb66db20d4f263f60671453f9bbf59b59975a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Feb 2025 00:09:07 -0800 Subject: [PATCH 12/17] Fixed index errors. --- .../(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx | 4 ++-- .../[websiteId]/sessions/[sessionId]/SessionActivity.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx index 3e15ddfa..b473a989 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx @@ -54,10 +54,10 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
{format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
- {day?.map((hour: number) => { + {day?.map((hour: number, n) => { const pct = hour / max; return ( -
+
{hour > 0 && ( + {showHeader && (
{formatTimezoneDate(createdAt, 'EEEE, PPP')}
)} @@ -44,7 +45,7 @@ export function SessionActivity({ {eventName ? : }
{eventName || urlPath}
- + ); })}
From 530d6fb323761db3d188746e929d9b0ae75bc54a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Feb 2025 16:20:22 -0800 Subject: [PATCH 13/17] Fixed reports. --- package.json | 2 +- .../[reportId]/FieldFilterEditForm.tsx | 2 +- .../[websiteId]/sessions/SessionsWeekly.tsx | 2 +- src/app/api/reports/revenue/route.ts | 15 ++----- src/app/api/reports/route.ts | 41 +++++++++++++++++-- src/app/api/teams/[teamId]/route.ts | 6 +-- .../[websiteId]/events/series/route.ts | 2 +- .../[websiteId]/sessions/stats/route.ts | 4 +- .../api/websites/[websiteId]/values/route.ts | 2 +- src/lib/schema.ts | 4 ++ src/queries/analytics/getValues.ts | 4 +- yarn.lock | 8 ++-- 12 files changed, 59 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index afab156b..31320109 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "npm-run-all": "^4.1.5", "prisma": "6.1.0", "react": "^19.0.0", - "react-basics": "^0.125.0", + "react-basics": "^0.126.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^19.0.0", "react-error-boundary": "^4.0.4", diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx index a1417780..bfe4ff5f 100644 --- a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx @@ -226,7 +226,7 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => { return ( - {values?.map((value: any) => { + {values?.map(({ value }) => { return {formatValue(value, type)}; })} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx index b473a989..4280b7e9 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx @@ -57,7 +57,7 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) { {day?.map((hour: number, n) => { const pct = hour / max; return ( -
+
{hour > 0 && ( }) { - const schema = z.object({ - teamId: z.string().uuid(), - }); - - const { auth, error } = await parseRequest(request, schema); + const { auth, error } = await parseRequest(request); if (error) { return error(); diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts index 99378612..413988af 100644 --- a/src/app/api/websites/[websiteId]/events/series/route.ts +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -25,7 +25,7 @@ export async function GET( const { websiteId } = await params; const { timezone } = query; - const { startDate, endDate, unit } = await getRequestDateRange(request); + const { startDate, endDate, unit } = await getRequestDateRange(query); if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts index 8149942a..016e9cf4 100644 --- a/src/app/api/websites/[websiteId]/sessions/stats/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -15,7 +15,7 @@ export async function GET( ...filterParams, }); - const { auth, error } = await parseRequest(request, schema); + const { auth, query, error } = await parseRequest(request, schema); if (error) { return error(); @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const { startDate, endDate } = await getRequestDateRange(request); + const { startDate, endDate } = await getRequestDateRange(query); const filters = getRequestFilters(request); diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts index 6e7b51d6..b81bdcc8 100644 --- a/src/app/api/websites/[websiteId]/values/route.ts +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -24,7 +24,7 @@ export async function GET( const { websiteId } = await params; const { type, search } = query; - const { startDate, endDate } = await getRequestDateRange(request); + const { startDate, endDate } = await getRequestDateRange(query); if (!(await canViewWebsite(auth, websiteId))) { return unauthorized(); diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 9978d9e5..21da4f42 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -51,5 +51,9 @@ export const reportParms = { dateRange: z.object({ startDate: z.coerce.date(), endDate: z.coerce.date(), + num: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + unit: z.string().optional(), + value: z.string().optional(), }), }; diff --git a/src/queries/analytics/getValues.ts b/src/queries/analytics/getValues.ts index f303faff..f98cca3a 100644 --- a/src/queries/analytics/getValues.ts +++ b/src/queries/analytics/getValues.ts @@ -42,7 +42,7 @@ async function relationalQuery( return rawQuery( ` - select ${column} as "value", count(*) + select ${column} as "value", count(*) as "count" from website_event inner join session on session.session_id = website_event.session_id @@ -98,7 +98,7 @@ async function clickhouseQuery( return rawQuery( ` - select ${column} as value, count(*) + select ${column} as "value", count(*) as "count" from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/yarn.lock b/yarn.lock index 91546c34..106e0f60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9206,10 +9206,10 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -react-basics@^0.125.0: - version "0.125.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.125.0.tgz#6baf3fea503fb4475f51877efa05d1a734b232c6" - integrity sha512-8swjTaKfenwb+NunwzQo16V+dCA/38Kd+PSYWpBFyNmlFzs3Ax2ZgnysxDhW9IgfFr4wR6/0gzD3S31WzXq6Kw== +react-basics@^0.126.0: + version "0.126.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.126.0.tgz#44e7f5e5ab9d411e91e697dd39c6cb53b6222ae0" + integrity sha512-TQtNZMeH5FtJjYxSN72rBmZWlIcs9jK3oVSCUUxfZq9LnFdoFSagTLCrihs3YCnX8vZEJXaJHQsp7lKEfyH5sw== dependencies: "@react-spring/web" "^9.7.3" classnames "^2.3.1" From 6ab7746b7da50deee2935c55bdc80001bbf7f537 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Feb 2025 19:55:53 -0800 Subject: [PATCH 14/17] Fixed report validations. --- src/app/api/reports/goals/route.ts | 27 +++++++++++++++++---------- src/app/api/reports/route.ts | 2 ++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/api/reports/goals/route.ts b/src/app/api/reports/goals/route.ts index cad774bd..7aceabc8 100644 --- a/src/app/api/reports/goals/route.ts +++ b/src/app/api/reports/goals/route.ts @@ -10,16 +10,23 @@ export async function POST(request: Request) { ...reportParms, goals: z .array( - z.object({ - type: z.string().regex(/url|event|event-data/), - value: z.string(), - goal: z.number(), - operator: z - .string() - .regex(/count|sum|average/) - .refine(data => data['type'] === 'event-data'), - property: z.string().refine(data => data['type'] === 'event-data'), - }), + z + .object({ + type: z.string().regex(/url|event|event-data/), + value: z.string(), + goal: z.coerce.number(), + operator: z + .string() + .regex(/count|sum|average/) + .optional(), + property: z.string().optional(), + }) + .refine(data => { + if (data['type'] === 'event-data') { + return data['operator'] && data['property']; + } + return true; + }), ) .min(1), }); diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts index 56c1130d..19c175bd 100644 --- a/src/app/api/reports/route.ts +++ b/src/app/api/reports/route.ts @@ -8,6 +8,8 @@ import { getReports, createReport } from 'queries'; export async function GET(request: Request) { const schema = z.object({ + websiteId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), ...pagingParams, }); From 7e2d25729335be0b8dd2b8e16c2ce0576267133d Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Feb 2025 20:04:43 -0800 Subject: [PATCH 15/17] Fixed tests. --- src/lib/__tests__/detect.test.ts | 8 ++++---- src/lib/detect.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts index 14c67dde..1cb558ad 100644 --- a/src/lib/__tests__/detect.test.ts +++ b/src/lib/__tests__/detect.test.ts @@ -6,17 +6,17 @@ const IP = '127.0.0.1'; test('getIpAddress: Custom header', () => { process.env.CLIENT_IP_HEADER = 'x-custom-ip-header'; - expect(detect.getIpAddress({ headers: { 'x-custom-ip-header': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP); }); test('getIpAddress: CloudFlare header', () => { - expect(detect.getIpAddress({ headers: { 'cf-connecting-ip': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP); }); test('getIpAddress: Standard header', () => { - expect(detect.getIpAddress({ headers: { 'x-forwarded-for': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP); }); test('getIpAddress: No header', () => { - expect(detect.getIpAddress({ headers: {} } as any)).toEqual(null); + expect(detect.getIpAddress(new Headers())).toEqual(null); }); diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 89e73a0b..9f1e04b7 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -28,7 +28,7 @@ export function getIpAddress(headers: Headers) { const ip = headers.get(header); if (header === 'x-forwarded-for') { - return ip?.split[',']?.[0]?.trim(); + return ip?.split(',')?.[0]?.trim(); } if (header === 'forwarded') { From 7d952029c16a58a4b5fb75c3dfc756380f0dfe3e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 3 Feb 2025 13:56:30 -0800 Subject: [PATCH 16/17] Fixed share url save. --- src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx | 8 ++++++-- src/components/hooks/usePagedQuery.ts | 2 +- src/lib/types.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx index 3f2a3ea0..465492b2 100644 --- a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx @@ -35,7 +35,11 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => }; const handleCheck = (checked: boolean) => { - const data = { name: website.name, shareId: checked ? generateId() : null }; + const data = { + name: website.name, + domain: website.domain, + shareId: checked ? generateId() : null, + }; mutate(data, { onSuccess: async () => { touch(`website:${website.id}`); @@ -47,7 +51,7 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => const handleSave = () => { mutate( - { shareId: id }, + { name: website.name, domain: website.domain, shareId: id }, { onSuccess: async () => { touch(`website:${website.id}`); diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts index e62ed0e1..a4cc68a6 100644 --- a/src/components/hooks/usePagedQuery.ts +++ b/src/components/hooks/usePagedQuery.ts @@ -12,7 +12,7 @@ export function usePagedQuery({ const { query: queryParams } = useNavigation(); const [params, setParams] = useState({ search: '', - page: +queryParams.page || 1, + page: queryParams.page || '1', }); const { useQuery } = useApi(); diff --git a/src/lib/types.ts b/src/lib/types.ts index 3f769799..39e52642 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -41,7 +41,7 @@ export interface PageResult { export interface PagedQueryResult { result: PageResult; - search: any; + query: any; params: PageParams; setParams: Dispatch>; } From fc4716a38dcbbce0ebb01cc62ecc0af2a7524d50 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 3 Feb 2025 19:34:09 -0800 Subject: [PATCH 17/17] Added missing user delete route. --- src/app/api/teams/route.ts | 2 +- src/app/api/users/[userId]/route.ts | 31 ++++++++++++++++++++++++++--- src/app/api/websites/route.ts | 10 +++++----- src/lib/response.ts | 6 +++--- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index cd71b24a..2eb0c8d8 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -29,7 +29,7 @@ export async function POST(request: Request) { name, accessCode: `team_${getRandomChars(16)}`, }, - auth.user.userId, + auth.user.id, ); return json(team); diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index 684be305..0955fc7c 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { canUpdateUser, canViewUser } from 'lib/auth'; -import { getUser, getUserByUsername, updateUser } from 'queries'; -import { json, unauthorized, badRequest } from 'lib/response'; +import { canUpdateUser, canViewUser, canDeleteUser } from 'lib/auth'; +import { getUser, getUserByUsername, updateUser, deleteUser } from 'queries'; +import { json, unauthorized, badRequest, ok } from 'lib/response'; import { hashPassword } from 'next-basics'; import { parseRequest } from 'lib/request'; @@ -74,3 +74,28 @@ export async function POST(request: Request, { params }: { params: Promise<{ use return json(updated); } + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ userId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!(await canDeleteUser(auth))) { + return unauthorized(); + } + + if (userId === auth.user.id) { + return badRequest('You cannot delete yourself.'); + } + + await deleteUser(userId); + + return ok(); +} diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index 3911d8d8..dfc48cee 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -15,7 +15,7 @@ export async function GET(request: Request) { return error(); } - const websites = await getUserWebsites(auth.user.userId, query); + const websites = await getUserWebsites(auth.user.id, query); return json(websites); } @@ -24,8 +24,8 @@ export async function POST(request: Request) { const schema = z.object({ name: z.string().max(100), domain: z.string().max(500), - shareId: z.string().max(50).nullable(), - teamId: z.string().nullable(), + shareId: z.string().max(50).nullable().optional(), + teamId: z.string().nullable().optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -42,7 +42,7 @@ export async function POST(request: Request) { const data: any = { id: uuid(), - createdBy: auth.user.userId, + createdBy: auth.user.id, name, domain, shareId, @@ -50,7 +50,7 @@ export async function POST(request: Request) { }; if (!teamId) { - data.userId = auth.user.userId; + data.userId = auth.user.id; } const website = await createWebsite(data); diff --git a/src/lib/response.ts b/src/lib/response.ts index 5e3b020f..7c99690f 100644 --- a/src/lib/response.ts +++ b/src/lib/response.ts @@ -17,13 +17,13 @@ export function unauthorized(message?: any) { } export function forbidden(message?: any) { - return Response.json({ error: 'Forbidden', message, status: 403 }); + return Response.json({ error: 'Forbidden', message }, { status: 403 }); } export function notFound(message?: any) { - return Response.json({ error: 'Not found', message, status: 404 }); + return Response.json({ error: 'Not found', message }, { status: 404 }); } export function serverError(error?: any) { - return Response.json({ error: 'Server error', message: serializeError(error), status: 500 }); + return Response.json({ error: 'Server error', message: serializeError(error) }, { status: 500 }); }