mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Converted reports and share routes.
This commit is contained in:
parent
dcac7b7c96
commit
6c9f1ad06b
23 changed files with 574 additions and 5 deletions
91
src/app/api/reports/[reportId]/route.ts
Normal file
91
src/app/api/reports/[reportId]/route.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
50
src/app/api/reports/funnel/route.ts
Normal file
50
src/app/api/reports/funnel/route.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { getFunnel } from 'queries';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
)
|
||||
.min(2),
|
||||
window: z.number().positive(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
steps,
|
||||
window,
|
||||
dateRange: { startDate, endDate },
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getFunnel(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
steps,
|
||||
windowMinutes: +window,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
53
src/app/api/reports/goals/route.ts
Normal file
53
src/app/api/reports/goals/route.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { getGoals } from 'queries/analytics/reports/getGoals';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
}),
|
||||
goals: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.string().regex(/url|event|event-data/),
|
||||
value: z.string(),
|
||||
goal: z.number(),
|
||||
operator: z
|
||||
.string()
|
||||
.regex(/count|sum|average/)
|
||||
.refine(data => data['type'] === 'event-data'),
|
||||
property: z.string().refine(data => data['type'] === 'event-data'),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
goals,
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getGoals(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
goals,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
71
src/app/api/reports/insights/route.ts
Normal file
71
src/app/api/reports/insights/route.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { getInsights } from 'queries';
|
||||
|
||||
function convertFilters(filters: any[]) {
|
||||
return filters.reduce((obj, filter) => {
|
||||
obj[filter.name] = filter;
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
}),
|
||||
fields: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
label: z.string(),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
filters: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
operator: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
fields,
|
||||
filters,
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getInsights(websiteId, fields, {
|
||||
...convertFilters(filters),
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
46
src/app/api/reports/journey/route.ts
Normal file
46
src/app/api/reports/journey/route.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { getJourney } from 'queries';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
}),
|
||||
steps: z.number().min(3).max(7),
|
||||
startStep: z.string(),
|
||||
endStep: z.string(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
steps,
|
||||
startStep,
|
||||
endStep,
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getJourney(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
steps,
|
||||
startStep,
|
||||
endStep,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
41
src/app/api/reports/retention/route.ts
Normal file
41
src/app/api/reports/retention/route.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { getRetention } from 'queries';
|
||||
import { timezoneParam } from 'lib/schema';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
}),
|
||||
timezone: timezoneParam,
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
timezone,
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getRetention(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
timezone,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
75
src/app/api/reports/revenue/route.ts
Normal file
75
src/app/api/reports/revenue/route.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { timezoneParam, unitParam } from 'lib/schema';
|
||||
import { getRevenue } from 'queries/analytics/reports/getRevenue';
|
||||
import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId, startDate, endDate } = query;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getRevenueValues(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
unit: unitParam,
|
||||
}),
|
||||
timezone: timezoneParam,
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
currency,
|
||||
timezone,
|
||||
dateRange: { startDate, endDate, unit },
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getRevenue(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
unit,
|
||||
timezone,
|
||||
currency,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
73
src/app/api/reports/route.ts
Normal file
73
src/app/api/reports/route.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { z } from 'zod';
|
||||
import { pagingParams } from 'lib/schema';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { canViewTeam, canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { getReports } from 'queries/prisma/report';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { page, search, pageSize, websiteId, teamId } = query;
|
||||
const userId = auth.user.id;
|
||||
const filters = {
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
};
|
||||
|
||||
if (
|
||||
(websiteId && !(await canViewWebsite(auth, websiteId))) ||
|
||||
(teamId && !(await canViewTeam(auth, teamId)))
|
||||
) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getReports(
|
||||
{
|
||||
where: {
|
||||
OR: [
|
||||
...(websiteId ? [{ websiteId }] : []),
|
||||
...(teamId
|
||||
? [
|
||||
{
|
||||
website: {
|
||||
deletedAt: null,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(userId && !websiteId && !teamId
|
||||
? [
|
||||
{
|
||||
website: {
|
||||
deletedAt: null,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
include: {
|
||||
website: {
|
||||
select: {
|
||||
domain: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filters,
|
||||
);
|
||||
|
||||
return json(data);
|
||||
}
|
||||
40
src/app/api/reports/utm/route.ts
Normal file
40
src/app/api/reports/utm/route.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { unauthorized, json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { getUTM } from 'queries';
|
||||
import { timezoneParam } from 'lib/schema';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
timezone: timezoneParam,
|
||||
}),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate, timezone },
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getUTM(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
timezone,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
19
src/app/api/share/[shareId]/route.ts
Normal file
19
src/app/api/share/[shareId]/route.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ async function pagedQuery<T>(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 = [
|
||||
|
|
|
|||
|
|
@ -30,7 +30,17 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value),
|
|||
message: 'Invalid unit',
|
||||
});
|
||||
|
||||
export const roleParam = z.string().regex(/team-member|team-view-only|team-manager/);
|
||||
export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']);
|
||||
|
||||
export const reportTypeParam = z.enum([
|
||||
'funnel',
|
||||
'insights',
|
||||
'retention',
|
||||
'utm',
|
||||
'goals',
|
||||
'journey',
|
||||
'revenue',
|
||||
]);
|
||||
|
||||
export const filterParams = {
|
||||
url: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ export type ReportType = ObjectValues<typeof REPORT_TYPES>;
|
|||
|
||||
export interface PageParams {
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
page?: string;
|
||||
pageSize?: string;
|
||||
orderBy?: string;
|
||||
sortDescending?: boolean;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue