diff options
| author | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
| commit | 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch) | |
| tree | b9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/api/websites | |
| download | umami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.tar.xz umami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.zip | |
Created from https://vercel.com/new
Diffstat (limited to 'src/app/api/websites')
31 files changed, 1418 insertions, 0 deletions
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 0000000..233b97e --- /dev/null +++ b/src/app/api/websites/[websiteId]/active/route.ts @@ -0,0 +1,25 @@ +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canViewWebsite } from '@/permissions'; +import { getActiveVisitors } from '@/queries/sql'; + +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; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const visitors = await getActiveVisitors(websiteId); + + return json(visitors); +} 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 0000000..14a241f --- /dev/null +++ b/src/app/api/websites/[websiteId]/daterange/route.ts @@ -0,0 +1,25 @@ +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canViewWebsite } from '@/permissions'; +import { getWebsiteDateRange } from '@/queries/sql'; + +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; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const dateRange = await getWebsiteDateRange(websiteId); + + return json(dateRange); +} diff --git a/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts new file mode 100644 index 0000000..54afab2 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts @@ -0,0 +1,25 @@ +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canViewWebsite } from '@/permissions'; +import { getEventData } from '@/queries/sql/events/getEventData'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; eventId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, eventId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getEventData(websiteId, eventId); + + return json(data); +} 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 0000000..eb6ee6e --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataEvents } from '@/queries/sql/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(), + ...filterParams, + }); + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getEventDataEvents(websiteId, { + ...filters, + }); + + 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 0000000..bce6a97 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataFields } from '@/queries/sql'; + +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 { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getEventDataFields(websiteId, filters); + + 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 0000000..52d15cf --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataProperties } from '@/queries/sql'; + +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 { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getEventDataProperties(websiteId, filters); + + 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 0000000..042e989 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataStats } from '@/queries/sql'; + +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 { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getEventDataStats(websiteId, filters); + + 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 0000000..12e8f2d --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventDataValues } from '@/queries/sql'; + +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(), + propertyName: z.string(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName } = query; + const filters = await getQueryFilters(query, websiteId); + + const data = await getEventDataValues(websiteId, { + ...filters, + 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 0000000..74ec3ec --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, pagingParams, searchParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getWebsiteEvents } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().optional(), + endAt: z.coerce.number().optional(), + ...filterParams, + ...pagingParams, + ...searchParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getWebsiteEvents(websiteId, filters); + + 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 0000000..977e9c8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventStats } from '@/queries/sql'; + +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.optional(), + timezone: timezoneParam, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getEventStats(websiteId, filters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts new file mode 100644 index 0000000..eec81c6 --- /dev/null +++ b/src/app/api/websites/[websiteId]/export/route.ts @@ -0,0 +1,64 @@ +import JSZip from 'jszip'; +import Papa from 'papaparse'; +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { dateRangeParams, pagingParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + ...dateRangeParams, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([ + getEventMetrics(websiteId, { type: 'event' }, filters), + getPageviewMetrics(websiteId, { type: 'path' }, filters), + getPageviewMetrics(websiteId, { type: 'referrer' }, filters), + getSessionMetrics(websiteId, { type: 'browser' }, filters), + getSessionMetrics(websiteId, { type: 'os' }, filters), + getSessionMetrics(websiteId, { type: 'device' }, filters), + getSessionMetrics(websiteId, { type: 'country' }, filters), + ]); + + const zip = new JSZip(); + + const parse = (data: any) => { + return Papa.unparse(data, { + header: true, + skipEmptyLines: true, + }); + }; + + zip.file('events.csv', parse(events)); + zip.file('pages.csv', parse(pages)); + zip.file('referrers.csv', parse(referrers)); + zip.file('browsers.csv', parse(browsers)); + zip.file('os.csv', parse(os)); + zip.file('devices.csv', parse(devices)); + zip.file('countries.csv', parse(countries)); + + const content = await zip.generateAsync({ type: 'nodebuffer' }); + const base64 = content.toString('base64'); + + return json({ zip: base64 }); +} diff --git a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts new file mode 100644 index 0000000..d52c177 --- /dev/null +++ b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts @@ -0,0 +1,66 @@ +import { z } from 'zod'; +import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { badRequest, json, unauthorized } from '@/lib/response'; +import { dateRangeParams, filterParams, searchParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { + getChannelExpandedMetrics, + getEventExpandedMetrics, + getPageviewExpandedMetrics, + getSessionExpandedMetrics, +} from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: z.string(), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + ...dateRangeParams, + ...searchParams, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { type, limit, offset, search } = query; + const filters = await getQueryFilters(query, websiteId); + + if (search) { + filters[type] = `c.${search}`; + } + + if (SESSION_COLUMNS.includes(type)) { + const data = await getSessionExpandedMetrics(websiteId, { type, limit, offset }, filters); + + return json(data); + } + + if (EVENT_COLUMNS.includes(type)) { + if (type === 'event') { + filters.eventType = EVENT_TYPE.customEvent; + return json(await getEventExpandedMetrics(websiteId, { type, limit, offset }, filters)); + } else { + return json(await getPageviewExpandedMetrics(websiteId, { type, limit, offset }, filters)); + } + } + + if (type === 'channel') { + return json(await getChannelExpandedMetrics(websiteId, filters)); + } + + return badRequest(); +} 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 0000000..12784ad --- /dev/null +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -0,0 +1,66 @@ +import { z } from 'zod'; +import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { badRequest, json, unauthorized } from '@/lib/response'; +import { dateRangeParams, filterParams, searchParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { + getChannelMetrics, + getEventMetrics, + getPageviewMetrics, + getSessionMetrics, +} from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: z.string(), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + ...dateRangeParams, + ...searchParams, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { type, limit, offset, search } = query; + const filters = await getQueryFilters(query, websiteId); + + if (search) { + filters[type] = `c.${search}`; + } + + if (SESSION_COLUMNS.includes(type)) { + const data = await getSessionMetrics(websiteId, { type, limit, offset }, filters); + + return json(data); + } + + if (EVENT_COLUMNS.includes(type)) { + if (type === 'event') { + filters.eventType = EVENT_TYPE.customEvent; + return json(await getEventMetrics(websiteId, { type, limit, offset }, filters)); + } else { + return json(await getPageviewMetrics(websiteId, { type, limit, offset }, filters)); + } + } + + if (type === 'channel') { + return json(await getChannelMetrics(websiteId, filters)); + } + + 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 0000000..af59bce --- /dev/null +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -0,0 +1,72 @@ +import { z } from 'zod'; +import { getCompareDate } from '@/lib/date'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { dateRangeParams, filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getPageviewStats, getSessionStats } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + ...dateRangeParams, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const [pageviews, sessions] = await Promise.all([ + getPageviewStats(websiteId, filters), + getSessionStats(websiteId, filters), + ]); + + if (filters.compare) { + const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( + filters.compare, + filters.startDate, + filters.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: filters.startDate, + endDate: filters.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 0000000..93e7ab4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/reports/route.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, pagingParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getReports } from '@/queries/prisma'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, + filters: { type: string }, +) { + const schema = z.object({ + ...filterParams, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { page, pageSize, search } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getReports( + { + where: { + websiteId, + type: filters.type, + }, + }, + { + page, + 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 0000000..e0be5a5 --- /dev/null +++ b/src/app/api/websites/[websiteId]/reset/route.ts @@ -0,0 +1,25 @@ +import { parseRequest } from '@/lib/request'; +import { ok, unauthorized } from '@/lib/response'; +import { canUpdateWebsite } from '@/permissions'; +import { resetWebsite } from '@/queries/prisma'; + +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; + + if (!(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 0000000..b4c0e7e --- /dev/null +++ b/src/app/api/websites/[websiteId]/route.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; +import { SHARE_ID_REGEX } from '@/lib/constants'; +import { parseRequest } from '@/lib/request'; +import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response'; +import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions'; +import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma'; + +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; + + 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().optional(), + domain: z.string().optional(), + shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { name, domain, shareId } = body; + + if (!(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.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) { + return badRequest({ message: 'That share ID is already taken.' }); + } + + return serverError(e); + } +} + +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; + + if (!(await canDeleteWebsite(auth, websiteId))) { + return unauthorized(); + } + + await deleteWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts new file mode 100644 index 0000000..b51f783 --- /dev/null +++ b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { json, notFound, ok, unauthorized } from '@/lib/response'; +import { anyObjectParam, segmentTypeParam } from '@/lib/schema'; +import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions'; +import { deleteSegment, getSegment, updateSegment } from '@/queries/prisma'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; segmentId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, segmentId } = await params; + + const segment = await getSegment(segmentId); + + if (websiteId && !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + return json(segment); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string; segmentId: string }> }, +) { + const schema = z.object({ + type: segmentTypeParam, + name: z.string().max(200), + parameters: anyObjectParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId, segmentId } = await params; + const { type, name, parameters } = body; + + const segment = await getSegment(segmentId); + + if (!segment) { + return notFound(); + } + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await updateSegment(segmentId, { + type, + name, + parameters, + } as any); + + return json(result); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ websiteId: string; segmentId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, segmentId } = await params; + + const segment = await getSegment(segmentId); + + if (!segment) { + return notFound(); + } + + if (!(await canDeleteWebsite(auth, websiteId))) { + return unauthorized(); + } + + await deleteSegment(segmentId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/segments/route.ts b/src/app/api/websites/[websiteId]/segments/route.ts new file mode 100644 index 0000000..4592765 --- /dev/null +++ b/src/app/api/websites/[websiteId]/segments/route.ts @@ -0,0 +1,70 @@ +import { z } from 'zod'; +import { uuid } from '@/lib/crypto'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { anyObjectParam, searchParams, segmentTypeParam } from '@/lib/schema'; +import { canUpdateWebsite, canViewWebsite } from '@/permissions'; +import { createSegment, getWebsiteSegments } from '@/queries/prisma'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: segmentTypeParam, + ...searchParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { type } = query; + + if (websiteId && !(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query); + + const segments = await getWebsiteSegments(websiteId, type, filters); + + return json(segments); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: segmentTypeParam, + name: z.string().max(200), + parameters: anyObjectParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { type, name, parameters } = body; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await createSegment({ + id: uuid(), + websiteId, + type, + name, + parameters, + } as any); + + return json(result); +} 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 0000000..2d8db15 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataProperties } from '@/queries/sql'; + +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 { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getSessionDataProperties(websiteId, filters); + + 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 0000000..7d06870 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getSessionDataValues } from '@/queries/sql'; + +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(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { propertyName } = query; + const filters = await getQueryFilters(query, websiteId); + + const data = await getSessionDataValues(websiteId, { + ...filters, + propertyName, + }); + + 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 0000000..41b766d --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canViewWebsite } from '@/permissions'; +import { getSessionActivity } from '@/queries/sql'; + +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 { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId, sessionId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getSessionActivity(websiteId, sessionId, filters); + + 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 0000000..6b5c241 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts @@ -0,0 +1,25 @@ +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canViewWebsite } from '@/permissions'; +import { getSessionData } from '@/queries/sql'; + +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; + + if (!(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 0000000..1091663 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts @@ -0,0 +1,25 @@ +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canViewWebsite } from '@/permissions'; +import { getWebsiteSession } from '@/queries/sql'; + +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; + + if (!(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 0000000..ed4757a --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { dateRangeParams, filterParams, pagingParams, searchParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getWebsiteSessions } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + ...dateRangeParams, + ...filterParams, + ...pagingParams, + ...searchParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getWebsiteSessions(websiteId, filters); + + 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 0000000..459830e --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getWebsiteSessionStats } from '@/queries/sql'; + +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 { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const metrics = await getWebsiteSessionStats(websiteId, filters); + + 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 0000000..b9ccf3e --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, timezoneParam } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getWeeklyTraffic } from '@/queries/sql'; + +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, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getWeeklyTraffic(websiteId, filters); + + return json(data); +} 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 0000000..07c8b96 --- /dev/null +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { getCompareDate } from '@/lib/date'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { dateRangeParams, filterParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getWebsiteStats } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + ...dateRangeParams, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = await getQueryFilters(query, websiteId); + + const data = await getWebsiteStats(websiteId, filters); + + const { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate); + + const comparison = await getWebsiteStats(websiteId, { + ...filters, + startDate, + endDate, + }); + + return json({ ...data, comparison }); +} 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 0000000..df2fed2 --- /dev/null +++ b/src/app/api/websites/[websiteId]/transfer/route.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { badRequest, json, unauthorized } from '@/lib/response'; +import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/permissions'; +import { updateWebsite } from '@/queries/prisma'; + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + userId: z.uuid().optional(), + teamId: z.uuid().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { userId, teamId } = body; + + 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); + } + + return badRequest(); +} 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 0000000..172325e --- /dev/null +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { EVENT_COLUMNS, FILTER_COLUMNS, SEGMENT_TYPES, SESSION_COLUMNS } from '@/lib/constants'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { badRequest, json, unauthorized } from '@/lib/response'; +import { dateRangeParams, fieldsParam, searchParams } from '@/lib/schema'; +import { canViewWebsite } from '@/permissions'; +import { getWebsiteSegments } from '@/queries/prisma'; +import { getValues } from '@/queries/sql'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: fieldsParam, + ...dateRangeParams, + ...searchParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { type } = query; + + if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type) && !SEGMENT_TYPES[type]) { + return badRequest(); + } + + let values: any[]; + + if (SEGMENT_TYPES[type]) { + values = (await getWebsiteSegments(websiteId, type))?.data?.map(segment => ({ + value: segment.name, + })); + } else { + const filters = await getQueryFilters(query, websiteId); + values = await getValues(websiteId, FILTER_COLUMNS[type], filters); + } + + 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 0000000..e2b26c1 --- /dev/null +++ b/src/app/api/websites/route.ts @@ -0,0 +1,86 @@ +import { z } from 'zod'; +import { uuid } from '@/lib/crypto'; +import redis from '@/lib/redis'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { pagingParams, searchParams } from '@/lib/schema'; +import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; +import { createWebsite, getWebsiteCount } from '@/queries/prisma'; +import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website'; + +const CLOUD_WEBSITE_LIMIT = 3; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + ...searchParams, + includeTeams: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const userId = auth.user.id; + + const filters = await getQueryFilters(query); + + if (query.includeTeams) { + return json(await getAllUserWebsitesIncludingTeamOwner(userId, filters)); + } + + return json(await getUserWebsites(userId, filters)); +} + +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().optional(), + teamId: z.uuid().nullable().optional(), + id: z.uuid().nullable().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { id, name, domain, shareId, teamId } = body; + + if (process.env.CLOUD_MODE && !teamId) { + const account = await redis.client.get(`account:${auth.user.id}`); + + if (!account?.hasSubscription) { + const count = await getWebsiteCount(auth.user.id); + + if (count >= CLOUD_WEBSITE_LIMIT) { + return unauthorized({ message: 'Website limit reached.' }); + } + } + } + + if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { + return unauthorized(); + } + + const data: any = { + id: id ?? uuid(), + createdBy: auth.user.id, + name, + domain, + shareId, + teamId, + }; + + if (!teamId) { + data.userId = auth.user.id; + } + + const website = await createWebsite(data); + + return json(website); +} |