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 (
diff --git a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx
index e5673346..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 = { 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/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx
index 3e15ddfa..4280b7e9 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}
- >
+
);
})}
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/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/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/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/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..23a05014
--- /dev/null
+++ b/src/app/api/reports/funnel/route.ts
@@ -0,0 +1,47 @@
+import { z } from 'zod';
+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({
+ ...reportParms,
+ window: z.number().positive(),
+ steps: z
+ .array(
+ z.object({
+ type: z.string(),
+ value: z.string(),
+ }),
+ )
+ .min(2),
+ });
+
+ 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..7aceabc8
--- /dev/null
+++ b/src/app/api/reports/goals/route.ts
@@ -0,0 +1,57 @@
+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';
+import { reportParms } from 'lib/schema';
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ ...reportParms,
+ goals: z
+ .array(
+ 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),
+ });
+
+ 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..a7ed1c15
--- /dev/null
+++ b/src/app/api/reports/insights/route.ts
@@ -0,0 +1,62 @@
+import { z } from 'zod';
+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) => {
+ obj[filter.name] = filter;
+
+ return obj;
+ }, {});
+}
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ ...reportParms,
+ 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(),
+ }),
+ ),
+ });
+
+ 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..f5121fdc
--- /dev/null
+++ b/src/app/api/reports/journey/route.ts
@@ -0,0 +1,43 @@
+import { z } from 'zod';
+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({
+ ...reportParms,
+ 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..8e854fa9
--- /dev/null
+++ b/src/app/api/reports/retention/route.ts
@@ -0,0 +1,37 @@
+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 { reportParms, timezoneParam } from 'lib/schema';
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ ...reportParms,
+ 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..abece8a1
--- /dev/null
+++ b/src/app/api/reports/revenue/route.ts
@@ -0,0 +1,62 @@
+import { z } from 'zod';
+import { canViewWebsite } from 'lib/auth';
+import { unauthorized, json } from 'lib/response';
+import { parseRequest } from 'lib/request';
+import { reportParms, timezoneParam } from 'lib/schema';
+import { getRevenue } from 'queries/analytics/reports/getRevenue';
+import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues';
+
+export async function GET(request: Request) {
+ const { auth, query, error } = await parseRequest(request);
+
+ 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({
+ ...reportParms,
+ 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..19c175bd
--- /dev/null
+++ b/src/app/api/reports/route.ts
@@ -0,0 +1,110 @@
+import { z } from 'zod';
+import { uuid } from 'lib/crypto';
+import { pagingParams, reportTypeParam } from 'lib/schema';
+import { parseRequest } from 'lib/request';
+import { canViewTeam, canViewWebsite, canUpdateWebsite } from 'lib/auth';
+import { unauthorized, json } from 'lib/response';
+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,
+ });
+
+ 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);
+}
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ websiteId: z.string().uuid(),
+ name: z.string().max(200),
+ type: reportTypeParam,
+ description: z.string().max(500),
+ parameters: z.object({}).passthrough(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, type, name, description, parameters } = body;
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const result = await createReport({
+ id: uuid(),
+ userId: auth.user.id,
+ websiteId,
+ type,
+ name,
+ description,
+ parameters: JSON.stringify(parameters),
+ } as any);
+
+ return json(result);
+}
diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts
new file mode 100644
index 00000000..2412134d
--- /dev/null
+++ b/src/app/api/reports/utm/route.ts
@@ -0,0 +1,35 @@
+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 { reportParms } from 'lib/schema';
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ ...reportParms,
+ });
+
+ 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/send/route.ts b/src/app/api/send/route.ts
new file mode 100644
index 00000000..0db93e85
--- /dev/null
+++ b/src/app/api/send/route.ts
@@ -0,0 +1,192 @@
+import { z } from 'zod';
+import { isbot } from 'isbot';
+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(e);
+ }
+ }
+ }
+ }
+
+ // Visit info
+ const now = Math.floor(new Date().getTime() / 1000);
+ let visitId = cache?.visitId || uuid(sessionId, visitSalt());
+ let iat = cache?.iat || now;
+
+ // Expire visit after 30 minutes
+ if (now - iat > 1800) {
+ visitId = uuid(sessionId, visitSalt());
+ iat = now;
+ }
+
+ 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({ cache: token });
+}
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/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts
new file mode 100644
index 00000000..0c5db380
--- /dev/null
+++ b/src/app/api/teams/[teamId]/route.ts
@@ -0,0 +1,71 @@
+import { z } from 'zod';
+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 }> }) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { teamId } = await params;
+
+ if (!(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 { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { teamId } = await params;
+
+ if (!(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 { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { teamId } = await params;
+
+ if (!(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..9276c194
--- /dev/null
+++ b/src/app/api/teams/[teamId]/users/[userId]/route.ts
@@ -0,0 +1,78 @@
+import { z } from 'zod';
+import { unauthorized, json, badRequest, ok } from 'lib/response';
+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 { auth, error } = await parseRequest(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.');
+ }
+
+ 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 { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { teamId, userId } = await params;
+
+ 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 { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { teamId } = await params;
+
+ if (!(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..a69a6b85
--- /dev/null
+++ b/src/app/api/teams/[teamId]/users/route.ts
@@ -0,0 +1,79 @@
+import { z } from 'zod';
+import { unauthorized, json, badRequest } from 'lib/response';
+import { canAddUserToTeam, canUpdateTeam } from 'lib/auth';
+import { parseRequest } 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { teamId } = await params;
+
+ 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 { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { teamId } = await params;
+
+ if (!(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..9f800e0e
--- /dev/null
+++ b/src/app/api/teams/[teamId]/websites/route.ts
@@ -0,0 +1,26 @@
+import { z } from 'zod';
+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';
+
+export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
+ const schema = z.object({
+ ...pagingParams,
+ });
+ const { teamId } = await params;
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!(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..76d4a83f
--- /dev/null
+++ b/src/app/api/teams/join/route.ts
@@ -0,0 +1,44 @@
+import { z } from 'zod';
+import { unauthorized, json, badRequest, notFound } from 'lib/response';
+import { canCreateTeam } from 'lib/auth';
+import { parseRequest } 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 { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!(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..2eb0c8d8
--- /dev/null
+++ b/src/app/api/teams/route.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+import { getRandomChars } from 'next-basics';
+import { unauthorized, json } from 'lib/response';
+import { canCreateTeam } from 'lib/auth';
+import { uuid } from 'lib/crypto';
+import { parseRequest } from 'lib/request';
+import { createTeam } from 'queries';
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ name: z.string().max(50),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!(await canCreateTeam(auth))) {
+ return unauthorized();
+ }
+
+ const { name } = body;
+
+ const team = await createTeam(
+ {
+ id: uuid(),
+ name,
+ accessCode: `team_${getRandomChars(16)}`,
+ },
+ auth.user.id,
+ );
+
+ return json(team);
+}
diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts
new file mode 100644
index 00000000..0955fc7c
--- /dev/null
+++ b/src/app/api/users/[userId]/route.ts
@@ -0,0 +1,101 @@
+import { z } from 'zod';
+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';
+
+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;
+
+ if (!(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 { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { userId } = await params;
+
+ if (!(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);
+}
+
+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/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts
new file mode 100644
index 00000000..329e7413
--- /dev/null
+++ b/src/app/api/users/[userId]/teams/route.ts
@@ -0,0 +1,27 @@
+import { z } from 'zod';
+import { pagingParams } from 'lib/schema';
+import { getUserTeams } from 'queries';
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { userId } = await params;
+
+ if (auth.user.id !== userId && !auth.user.isAdmin) {
+ 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..72510bd3
--- /dev/null
+++ b/src/app/api/users/[userId]/usage/route.ts
@@ -0,0 +1,63 @@
+import { z } from 'zod';
+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 { parseRequest } from 'lib/request';
+
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!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..22aced51
--- /dev/null
+++ b/src/app/api/users/[userId]/websites/route.ts
@@ -0,0 +1,27 @@
+import { z } from 'zod';
+import { unauthorized, json } from 'lib/response';
+import { getUserWebsites } from 'queries/prisma/website';
+import { pagingParams } from 'lib/schema';
+import { parseRequest } from 'lib/request';
+
+export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
+ const schema = z.object({
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { userId } = await params;
+
+ if (!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..8f9e5723
--- /dev/null
+++ b/src/app/api/users/route.ts
@@ -0,0 +1,44 @@
+import { z } from 'zod';
+import { hashPassword } from 'next-basics';
+import { canCreateUser } from 'lib/auth';
+import { ROLES } from 'lib/constants';
+import { uuid } from 'lib/crypto';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json, badRequest } from 'lib/response';
+import { createUser, getUserByUsername } from 'queries';
+
+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 { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!(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..9e25cea6
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/active/route.ts
@@ -0,0 +1,25 @@
+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;
+
+ 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..75423454
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/daterange/route.ts
@@ -0,0 +1,25 @@
+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;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const result = await getWebsiteDateRange(websiteId);
+
+ return json(result);
+}
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..4937482a
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts
@@ -0,0 +1,39 @@
+import { z } from 'zod';
+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(
+ 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt, event } = query;
+
+ if (!(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..e95998ed
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts
@@ -0,0 +1,38 @@
+import { z } from 'zod';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json } from 'lib/response';
+import { canViewWebsite } 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt } = query;
+
+ 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..25f915b1
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json } from 'lib/response';
+import { canViewWebsite } 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt, propertyName } = query;
+
+ if (!(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..a8093e61
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json } from 'lib/response';
+import { canViewWebsite } 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt } = query;
+
+ if (!(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..7734d920
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts
@@ -0,0 +1,42 @@
+import { z } from 'zod';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json } from 'lib/response';
+import { canViewWebsite } 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt, eventName, propertyName } = query;
+
+ if (!(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..71046b3c
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/events/route.ts
@@ -0,0 +1,37 @@
+import { z } from 'zod';
+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';
+
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt } = query;
+
+ if (!(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..413988af
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/events/series/route.ts
@@ -0,0 +1,45 @@
+import { z } from 'zod';
+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';
+
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { timezone } = query;
+ const { startDate, endDate, unit } = await getRequestDateRange(query);
+
+ if (!(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
new file mode 100644
index 00000000..44312429
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/metrics/route.ts
@@ -0,0 +1,82 @@
+import { z } from 'zod';
+import { canViewWebsite } from 'lib/auth';
+import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants';
+import { getRequestFilters, getRequestDateRange, parseRequest } from 'lib/request';
+import { json, unauthorized, badRequest } from 'lib/response';
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { type, limit, offset, search } = query;
+
+ if (!(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..020e139b
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/pageviews/route.ts
@@ -0,0 +1,85 @@
+import { z } from 'zod';
+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 { unauthorized, json } from 'lib/response';
+import { getPageviewStats, getSessionStats } 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,
+ compare: z.string().optional(),
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { timezone, compare } = query;
+
+ if (!(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..44f62625
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/reports/route.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+import { canViewWebsite } from 'lib/auth';
+import { getWebsiteReports } from 'queries';
+import { pagingParams } from 'lib/schema';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json } from 'lib/response';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...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 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..8f2df289
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/reset/route.ts
@@ -0,0 +1,25 @@
+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;
+
+ 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 00000000..b72a5e1d
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/route.ts
@@ -0,0 +1,84 @@
+import { z } from 'zod';
+import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from 'lib/auth';
+import { SHARE_ID_REGEX } from 'lib/constants';
+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;
+
+ 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 { 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.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, 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]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts
new file mode 100644
index 00000000..5729ddbd
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts
@@ -0,0 +1,36 @@
+import { z } from 'zod';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json } from 'lib/response';
+import { canViewWebsite } 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { startAt, endAt, propertyName } = query;
+ const { websiteId } = await params;
+
+ if (!(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..f14b1916
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts
@@ -0,0 +1,40 @@
+import { z } from 'zod';
+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(
+ 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { startAt, endAt, event } = query;
+ const { websiteId } = await params;
+
+ if (!(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..ccd05d4d
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod';
+import { parseRequest } from 'lib/request';
+import { unauthorized, json } from 'lib/response';
+import { canViewWebsite } 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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, sessionId } = await params;
+ const { startAt, endAt } = query;
+
+ if (!(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..a95a0233
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts
@@ -0,0 +1,25 @@
+import { unauthorized, json } from 'lib/response';
+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;
+
+ 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 00000000..0a968eb2
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts
@@ -0,0 +1,25 @@
+import { unauthorized, json } from 'lib/response';
+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;
+
+ 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 00000000..da8d9d09
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/route.ts
@@ -0,0 +1,37 @@
+import { z } from 'zod';
+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';
+
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt } = query;
+
+ if (!(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..016e9cf4
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts
@@ -0,0 +1,48 @@
+import { z } from 'zod';
+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';
+
+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 { startDate, endDate } = await getRequestDateRange(query);
+
+ 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..b57bb95c
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts
@@ -0,0 +1,38 @@
+import { z } from 'zod';
+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';
+
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { startAt, endAt, timezone } = query;
+
+ if (!(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
new file mode 100644
index 00000000..55dc4e3e
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/stats/route.ts
@@ -0,0 +1,63 @@
+import { z } from 'zod';
+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';
+
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { compare } = query;
+
+ if (!(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..d3e497cd
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/transfer/route.ts
@@ -0,0 +1,50 @@
+import { z } from 'zod';
+import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from 'lib/auth';
+import { updateWebsite } from 'queries';
+import { parseRequest } from 'lib/request';
+import { badRequest, unauthorized, json } from 'lib/response';
+
+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 { 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 00000000..b81bdcc8
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/values/route.ts
@@ -0,0 +1,40 @@
+import { z } from 'zod';
+import { canViewWebsite } from 'lib/auth';
+import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants';
+import { getValues } from 'queries';
+import { parseRequest, getRequestDateRange } from 'lib/request';
+import { badRequest, json, unauthorized } from 'lib/response';
+
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { type, search } = query;
+ const { startDate, endDate } = await getRequestDateRange(query);
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) {
+ return badRequest('Invalid type.');
+ }
+
+ 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..dfc48cee
--- /dev/null
+++ b/src/app/api/websites/route.ts
@@ -0,0 +1,59 @@
+import { z } from 'zod';
+import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth';
+import { json, unauthorized } from 'lib/response';
+import { uuid } from 'lib/crypto';
+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 { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const websites = await getUserWebsites(auth.user.id, 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().optional(),
+ teamId: z.string().nullable().optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ 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.id,
+ name,
+ domain,
+ shareId,
+ teamId,
+ };
+
+ if (!teamId) {
+ data.userId = auth.user.id;
+ }
+
+ 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/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/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts
index 19471432..a4cc68a6 100644
--- a/src/components/hooks/usePagedQuery.ts
+++ b/src/components/hooks/usePagedQuery.ts
@@ -11,8 +11,8 @@ export function usePagedQuery({
}: Omit & { queryFn: (params?: object) => any }): PagedQueryResult {
const { query: queryParams } = useNavigation();
const [params, setParams] = useState({
- query: '',
- page: +queryParams.page || 1,
+ search: '',
+ page: queryParams.page || '1',
});
const { useQuery } = useApi();
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/__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/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/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..9f1e04b7 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);
- const country = location?.country;
+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 = payload?.userAgent || 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/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/request.ts b/src/lib/request.ts
index 5e2be2fe..c71684b9 100644
--- a/src/lib/request.ts
+++ b/src/lib/request.ts
@@ -1,10 +1,55 @@
-import { NextApiRequest } from 'next';
+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 getRequestDateRange(req: NextApiRequest) {
- const { websiteId, startAt, endAt, unit } = req.query;
+export async function getJsonBody(request: Request) {
+ try {
+ return await request.clone().json();
+ } catch {
+ return undefined;
+ }
+}
+
+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);
+ let error: () => void | undefined;
+ let auth = null;
+
+ if (schema) {
+ const isGet = request.method === 'GET';
+ const result = schema.safeParse(isGet ? query : body);
+
+ if (!result.success) {
+ error = () => badRequest(result.error);
+ } else if (isGet) {
+ query = result.data;
+ } else {
+ body = result.data;
+ }
+ }
+
+ if (!options?.skipAuth && !error) {
+ auth = await checkAuth(request);
+
+ if (!auth) {
+ error = () => unauthorized();
+ }
+ }
+
+ return { url, query, body, auth, error };
+}
+
+export async function getRequestDateRange(query: Record) {
+ const { websiteId, startAt, endAt, unit } = query;
// All-time
if (+startAt === 0 && +endAt === 1) {
@@ -31,9 +76,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..7c99690f
--- /dev/null
+++ b/src/lib/response.ts
@@ -0,0 +1,29 @@
+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(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/lib/schema.ts b/src/lib/schema.ts
index 5218af10..21da4f42 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -1,13 +1,59 @@
-import * as yup from 'yup';
+import { z } from 'zod';
+import { isValidTimezone } from 'lib/date';
+import { UNIT_TYPES } from './constants';
-export const dateRange = {
- startAt: yup.number().integer().required(),
- endAt: yup.number().integer().min(yup.ref('startAt')).required(),
+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(),
};
-export const pageInfo = {
- query: yup.string(),
- page: yup.number().integer().positive(),
- pageSize: yup.number().integer().positive().min(1).max(200),
- orderBy: yup.string(),
+export const pagingParams = {
+ page: z.coerce.number().int().positive().optional(),
+ pageSize: z.coerce.number().int().positive().optional(),
+ orderBy: z.string().optional(),
+ query: z.string().optional(),
+};
+
+export const timezoneParam = z.string().refine(value => isValidTimezone(value), {
+ message: 'Invalid timezone',
+});
+
+export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), {
+ message: 'Invalid unit',
+});
+
+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 reportParms = {
+ websiteId: z.string().uuid(),
+ 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/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/lib/types.ts b/src/lib/types.ts
index d7a12068..39e52642 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,4 +1,3 @@
-import { NextApiRequest } from 'next';
import {
COLLECTION_TYPE,
DATA_TYPE,
@@ -8,7 +7,6 @@ import {
REPORT_TYPES,
ROLES,
} from './constants';
-import * as yup from 'yup';
import { TIME_UNIT } from './date';
import { Dispatch, SetStateAction } from 'react';
@@ -25,9 +23,9 @@ export type KafkaTopic = ObjectValues;
export type ReportType = ObjectValues;
export interface PageParams {
- query?: string;
- page?: number;
- pageSize?: number;
+ search?: string;
+ page?: string;
+ pageSize?: string;
orderBy?: string;
sortDescending?: boolean;
}
@@ -65,26 +63,6 @@ export interface Auth {
};
}
-export interface YupRequest {
- GET?: yup.ObjectSchema;
- POST?: yup.ObjectSchema;
- PUT?: yup.ObjectSchema;
- DELETE?: yup.ObjectSchema;
-}
-
-export interface NextApiRequestQueryBody extends NextApiRequest {
- auth?: Auth;
- query: TQuery & { [key: string]: string | string[] };
- body: TBody;
- headers: any;
- yup: YupRequest;
-}
-
-export interface NextApiRequestAuth extends NextApiRequest {
- 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/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/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/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/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 3b88689d..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 48800f90..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/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/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]/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]/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 72e5b0f2..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, query, pageSize } = req.query;
-
- const data = await getWebsiteReports(websiteId, {
- page,
- pageSize,
- query,
- });
-
- 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]/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/[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/index.ts b/src/pages/api/websites/index.ts
deleted file mode 100644
index c5eb7200..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);
-};
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/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/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/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';
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 9fe7d6e2..1aab5657 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1117,7 +1117,7 @@
dependencies:
regenerator-runtime "^0.14.0"
-"@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4":
+"@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
@@ -2980,11 +2980,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"
@@ -7617,11 +7612,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"
@@ -8054,11 +8044,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"
@@ -9176,11 +9161,6 @@ 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"
@@ -9238,10 +9218,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"
@@ -10130,7 +10110,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -10225,7 +10214,14 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -10564,11 +10560,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"
@@ -11016,7 +11007,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11034,6 +11025,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -11160,18 +11160,10 @@ 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"
+ integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
zustand@^4.5.5:
version "4.5.6"