mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Refactored send. Purged pages api routes.
This commit is contained in:
parent
5205551ca8
commit
85382e25af
69 changed files with 286 additions and 4118 deletions
|
|
@ -63,7 +63,6 @@ const headers = [
|
|||
{
|
||||
source: '/api/:path*',
|
||||
headers: [
|
||||
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
|
||||
{ key: 'Access-Control-Allow-Origin', value: '*' },
|
||||
{ key: 'Access-Control-Allow-Headers', value: '*' },
|
||||
{ key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' },
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ export async function POST(request: Request) {
|
|||
const schema = z.object({
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.date(),
|
||||
endDate: z.date(),
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
}),
|
||||
fields: z
|
||||
.array(
|
||||
|
|
@ -36,12 +36,6 @@ export async function POST(request: Request) {
|
|||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
|
|
|||
191
src/app/api/send/route.ts
Normal file
191
src/app/api/send/route.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { z } from 'zod';
|
||||
import { isbot } from 'isbot';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { createToken, parseToken, safeDecodeURI } from 'next-basics';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { badRequest, json, forbidden, serverError } from 'lib/response';
|
||||
import { fetchSession, fetchWebsite } from 'lib/load';
|
||||
import { getClientInfo, hasBlockedIp } from 'lib/detect';
|
||||
import { secret, uuid, visitSalt } from 'lib/crypto';
|
||||
import { createSession, saveEvent, saveSessionData } from 'queries';
|
||||
import { COLLECTION_TYPE } from 'lib/constants';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Bot check
|
||||
if (!process.env.DISABLE_BOT_CHECK && isbot(request.headers.get('user-agent'))) {
|
||||
return json({ beep: 'boop' });
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
type: z.enum(['event', 'identity']),
|
||||
payload: z.object({
|
||||
website: z.string().uuid(),
|
||||
data: z.object({}).passthrough().optional(),
|
||||
hostname: z.string().max(100).optional(),
|
||||
language: z.string().max(35).optional(),
|
||||
referrer: z.string().optional(),
|
||||
screen: z.string().max(11).optional(),
|
||||
title: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
name: z.string().max(50).optional(),
|
||||
tag: z.string().max(50).optional(),
|
||||
ip: z.string().ip().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const { body, error } = await parseRequest(request, schema, { skipAuth: true });
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { type, payload } = body;
|
||||
|
||||
const {
|
||||
website: websiteId,
|
||||
hostname,
|
||||
screen,
|
||||
language,
|
||||
url,
|
||||
referrer,
|
||||
name,
|
||||
data,
|
||||
title,
|
||||
tag,
|
||||
} = payload;
|
||||
|
||||
// Cache check
|
||||
let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null;
|
||||
const cacheHeader = request.headers.get('x-umami-cache');
|
||||
|
||||
if (cacheHeader) {
|
||||
const result = await parseToken(cacheHeader, secret());
|
||||
|
||||
if (result) {
|
||||
cache = result;
|
||||
}
|
||||
}
|
||||
|
||||
// Find website
|
||||
if (!cache?.websiteId) {
|
||||
const website = await fetchWebsite(websiteId);
|
||||
|
||||
if (!website) {
|
||||
return badRequest('Website not found.');
|
||||
}
|
||||
}
|
||||
|
||||
// Client info
|
||||
const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } =
|
||||
await getClientInfo(request, payload);
|
||||
|
||||
// IP block
|
||||
if (hasBlockedIp(ip)) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const sessionId = uuid(websiteId, hostname, ip, userAgent);
|
||||
|
||||
// Find session
|
||||
if (!cache?.sessionId) {
|
||||
const session = await fetchSession(websiteId, sessionId);
|
||||
|
||||
// Create a session if not found
|
||||
if (!session && !clickhouse.enabled) {
|
||||
try {
|
||||
await createSession({
|
||||
id: sessionId,
|
||||
websiteId,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||
return serverError(serializeError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visit info
|
||||
let visitId = cache?.visitId || uuid(sessionId, visitSalt());
|
||||
const iat = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
// Expire visit after 30 minutes
|
||||
if (cache?.iat && iat - cache?.iat > 1800) {
|
||||
visitId = uuid(sessionId, visitSalt());
|
||||
}
|
||||
|
||||
if (type === COLLECTION_TYPE.event) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [urlPath, urlQuery] = safeDecodeURI(url)?.split('?') || [];
|
||||
let [referrerPath, referrerQuery] = safeDecodeURI(referrer)?.split('?') || [];
|
||||
let referrerDomain = '';
|
||||
|
||||
if (!urlPath) {
|
||||
urlPath = '/';
|
||||
}
|
||||
|
||||
if (/^[\w-]+:\/\/\w+/.test(referrerPath)) {
|
||||
const refUrl = new URL(referrer);
|
||||
referrerPath = refUrl.pathname;
|
||||
referrerQuery = refUrl.search.substring(1);
|
||||
referrerDomain = refUrl.hostname.replace(/www\./, '');
|
||||
}
|
||||
|
||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||
urlPath = urlPath.replace(/(.+)\/$/, '$1');
|
||||
}
|
||||
|
||||
await saveEvent({
|
||||
websiteId,
|
||||
sessionId,
|
||||
visitId,
|
||||
urlPath,
|
||||
urlQuery,
|
||||
referrerPath,
|
||||
referrerQuery,
|
||||
referrerDomain,
|
||||
pageTitle: title,
|
||||
eventName: name,
|
||||
eventData: data,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
tag,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === COLLECTION_TYPE.identify) {
|
||||
if (!data) {
|
||||
return badRequest('Data required.');
|
||||
}
|
||||
|
||||
await saveSessionData({
|
||||
websiteId,
|
||||
sessionId,
|
||||
sessionData: data,
|
||||
});
|
||||
}
|
||||
|
||||
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());
|
||||
|
||||
return json(token);
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,45 @@
|
|||
import path from 'path';
|
||||
import { getClientIp } from 'request-ip';
|
||||
import { browserName, detectOS } from 'detect-browser';
|
||||
import isLocalhost from 'is-localhost-ip';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import maxmind from 'maxmind';
|
||||
import { safeDecodeURIComponent } from 'next-basics';
|
||||
import {
|
||||
DESKTOP_OS,
|
||||
MOBILE_OS,
|
||||
DESKTOP_SCREEN_WIDTH,
|
||||
LAPTOP_SCREEN_WIDTH,
|
||||
MOBILE_SCREEN_WIDTH,
|
||||
IP_ADDRESS_HEADERS,
|
||||
} from './constants';
|
||||
import { NextApiRequestCollect } from 'pages/api/send';
|
||||
|
||||
let lookup;
|
||||
|
||||
export function getIpAddress(req: NextApiRequestCollect) {
|
||||
const customHeader = String(process.env.CLIENT_IP_HEADER).toLowerCase();
|
||||
export function getIpAddress(headers: Headers) {
|
||||
const customHeader = process.env.CLIENT_IP_HEADER;
|
||||
|
||||
// Custom header
|
||||
if (customHeader !== 'undefined' && req.headers[customHeader]) {
|
||||
return req.headers[customHeader];
|
||||
}
|
||||
// Cloudflare
|
||||
else if (req.headers['cf-connecting-ip']) {
|
||||
return req.headers['cf-connecting-ip'];
|
||||
if (customHeader && headers.get(customHeader)) {
|
||||
return headers.get(customHeader);
|
||||
}
|
||||
|
||||
return getClientIp(req);
|
||||
const header = IP_ADDRESS_HEADERS.find(name => {
|
||||
return headers.get(name);
|
||||
});
|
||||
|
||||
const ip = headers.get(header);
|
||||
|
||||
if (header === 'x-forwarded-for') {
|
||||
return ip?.split[',']?.[0]?.trim();
|
||||
}
|
||||
|
||||
if (header === 'forwarded') {
|
||||
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
|
||||
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
export function getDevice(screen: string, os: string) {
|
||||
|
|
@ -67,7 +78,7 @@ function getRegionCode(country: string, region: string) {
|
|||
return region.includes('-') ? region : `${country}-${region}`;
|
||||
}
|
||||
|
||||
function safeDecodeCfHeader(s: string | undefined | null): string | undefined | null {
|
||||
function decodeHeader(s: string | undefined | null): string | undefined | null {
|
||||
if (s === undefined || s === null) {
|
||||
return s;
|
||||
}
|
||||
|
|
@ -75,36 +86,38 @@ function safeDecodeCfHeader(s: string | undefined | null): string | undefined |
|
|||
return Buffer.from(s, 'latin1').toString('utf-8');
|
||||
}
|
||||
|
||||
export async function getLocation(ip: string, req: NextApiRequestCollect) {
|
||||
export async function getLocation(ip: string, headers: Headers) {
|
||||
// Ignore local ips
|
||||
if (await isLocalhost(ip)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cloudflare headers
|
||||
if (req.headers['cf-ipcountry']) {
|
||||
const country = safeDecodeCfHeader(req.headers['cf-ipcountry']);
|
||||
const subdivision1 = safeDecodeCfHeader(req.headers['cf-region-code']);
|
||||
const city = safeDecodeCfHeader(req.headers['cf-ipcity']);
|
||||
if (!process.env.SKIP_LOCATION_HEADERS) {
|
||||
// Cloudflare headers
|
||||
if (headers.get('cf-ipcountry')) {
|
||||
const country = decodeHeader(headers.get('cf-ipcountry'));
|
||||
const subdivision1 = decodeHeader(headers.get('cf-region-code'));
|
||||
const city = decodeHeader(headers.get('cf-ipcity'));
|
||||
|
||||
return {
|
||||
country,
|
||||
subdivision1: getRegionCode(country, subdivision1),
|
||||
city,
|
||||
};
|
||||
}
|
||||
return {
|
||||
country,
|
||||
subdivision1: getRegionCode(country, subdivision1),
|
||||
city,
|
||||
};
|
||||
}
|
||||
|
||||
// Vercel headers
|
||||
if (req.headers['x-vercel-ip-country']) {
|
||||
const country = safeDecodeURIComponent(req.headers['x-vercel-ip-country']);
|
||||
const subdivision1 = safeDecodeURIComponent(req.headers['x-vercel-ip-country-region']);
|
||||
const city = safeDecodeURIComponent(req.headers['x-vercel-ip-city']);
|
||||
// Vercel headers
|
||||
if (headers.get('x-vercel-ip-country')) {
|
||||
const country = decodeHeader(headers.get('x-vercel-ip-country'));
|
||||
const subdivision1 = decodeHeader(headers.get('x-vercel-ip-country-region'));
|
||||
const city = decodeHeader(headers.get('x-vercel-ip-city'));
|
||||
|
||||
return {
|
||||
country,
|
||||
subdivision1: getRegionCode(country, subdivision1),
|
||||
city,
|
||||
};
|
||||
return {
|
||||
country,
|
||||
subdivision1: getRegionCode(country, subdivision1),
|
||||
city,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Database lookup
|
||||
|
|
@ -131,22 +144,22 @@ export async function getLocation(ip: string, req: NextApiRequestCollect) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getClientInfo(req: NextApiRequestCollect) {
|
||||
const userAgent = req.body?.payload?.userAgent || req.headers['user-agent'];
|
||||
const ip = req.body?.payload?.ip || getIpAddress(req);
|
||||
const location = await getLocation(ip, req);
|
||||
export async function getClientInfo(request: Request, payload: Record<string, any>) {
|
||||
const userAgent = payload?.userAgent || request.headers.get('user-agent');
|
||||
const ip = payload?.ip || getIpAddress(request.headers);
|
||||
const location = await getLocation(ip, request.headers);
|
||||
const country = location?.country;
|
||||
const subdivision1 = location?.subdivision1;
|
||||
const subdivision2 = location?.subdivision2;
|
||||
const city = location?.city;
|
||||
const browser = browserName(userAgent);
|
||||
const os = detectOS(userAgent) as string;
|
||||
const device = getDevice(req.body?.payload?.screen, os);
|
||||
const device = getDevice(payload?.screen, os);
|
||||
|
||||
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
|
||||
}
|
||||
|
||||
export function hasBlockedIp(req: NextApiRequestCollect) {
|
||||
export function hasBlockedIp(clientIp: string) {
|
||||
const ignoreIps = process.env.IGNORE_IP;
|
||||
|
||||
if (ignoreIps) {
|
||||
|
|
@ -156,17 +169,19 @@ export function hasBlockedIp(req: NextApiRequestCollect) {
|
|||
ips.push(...ignoreIps.split(',').map(n => n.trim()));
|
||||
}
|
||||
|
||||
const clientIp = getIpAddress(req);
|
||||
|
||||
return ips.find(ip => {
|
||||
if (ip === clientIp) return true;
|
||||
if (ip === clientIp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CIDR notation
|
||||
if (ip.indexOf('/') > 0) {
|
||||
const addr = ipaddr.parse(clientIp);
|
||||
const range = ipaddr.parseCIDR(ip);
|
||||
|
||||
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
|
||||
if (addr.kind() === range[0].kind() && addr.match(range)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Website> {
|
||||
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<Website> {
|
|||
export async function fetchSession(websiteId: string, sessionId: string): Promise<Session> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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<SessionData> {
|
||||
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 };
|
||||
}
|
||||
|
|
@ -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<ConfigResponse>) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<VersionResponse>) => {
|
||||
if (req.method === 'GET') {
|
||||
return ok(res, {
|
||||
version: CURRENT_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
||||
|
|
@ -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<UsersRequestQuery, UsersRequestBody>,
|
||||
res: NextApiResponse<User[] | User>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsitesRequestQuery, WebsitesRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, LoginRequestBody>,
|
||||
res: NextApiResponse<LoginResponse>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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<unknown, unknown>,
|
||||
res: NextApiResponse<User>,
|
||||
) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
return ok(res, req.auth.user);
|
||||
};
|
||||
|
|
@ -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<any, UserPasswordRequestBody>,
|
||||
res: NextApiResponse<User>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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<RealtimeRequestQuery>, 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);
|
||||
};
|
||||
|
|
@ -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<ReportRequestQuery, ReportRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, FunnelRequestBody>,
|
||||
res: NextApiResponse<FunnelResponse>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, RetentionRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, ReportRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, InsightsRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, RetentionRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, RetentionRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, RevenueRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, UTMRequestBody>, 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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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<ShareRequestQuery>,
|
||||
res: NextApiResponse<ShareResponse>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<TeamRequestQuery, TeamRequestBody>,
|
||||
res: NextApiResponse<Team>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<TeamUserRequestQuery, TeamUserRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<TeamUserRequestQuery, TeamUserRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<TeamWebsiteRequestQuery, any>,
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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<TeamsRequestQuery, TeamsRequestBody>,
|
||||
res: NextApiResponse<Team[] | Team>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<any, TeamsJoinRequestBody>,
|
||||
res: NextApiResponse<Team>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<UserRequestQuery, UserRequestBody>,
|
||||
res: NextApiResponse<User>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<UserTeamsRequestQuery, any>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<number>('startAt')).required(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<UserUsageRequestQuery>,
|
||||
res: NextApiResponse<UserUsageRequestResponse>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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<UsersRequestQuery, UsersRequestBody>,
|
||||
res: NextApiResponse<User[] | User>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteActiveRequestQuery>,
|
||||
res: NextApiResponse<WebsiteActive>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteDateRangeRequestQuery>,
|
||||
res: NextApiResponse<WebsiteActive>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteRequestQuery, WebsiteRequestBody>,
|
||||
res: NextApiResponse<Website>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteMetricsRequestQuery>,
|
||||
res: NextApiResponse<WebsiteMetric[]>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsitePageviewRequestQuery>,
|
||||
res: NextApiResponse<WebsitePageviews>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<ReportsRequestQuery, any>,
|
||||
res: NextApiResponse,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
const { websiteId } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { page, search, pageSize } = req.query;
|
||||
|
||||
const data = await getWebsiteReports(websiteId, {
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
});
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
||||
|
|
@ -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<WebsiteResetRequestQuery>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteStatsRequestQuery>,
|
||||
res: NextApiResponse<WebsiteStats>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteTransferRequestQuery, WebsiteTransferRequestBody>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<ValuesRequestQuery>, 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);
|
||||
};
|
||||
|
|
@ -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<EventDataFieldsRequestQuery, any>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<EventDataFieldsRequestQuery>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<EventDataFieldsRequestQuery>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<EventDataStatsRequestQuery>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<EventDataFieldsRequestQuery>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<ReportsRequestQuery, any>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteEventsRequestQuery>,
|
||||
res: NextApiResponse<WebsiteMetric>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<SessionDataFieldsRequestQuery>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<EventDataFieldsRequestQuery>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<SessionActivityRequestQuery, any>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WesiteSessionRequestQuery, any>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<SessionDataRequestQuery, any>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<ReportsRequestQuery, any>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsiteSessionStatsRequestQuery>,
|
||||
res: NextApiResponse<WebsiteStats>,
|
||||
) => {
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<ReportsRequestQuery, any>,
|
||||
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);
|
||||
};
|
||||
|
|
@ -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<WebsitesRequestQuery, WebsitesRequestBody>,
|
||||
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);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue