diff --git a/next.config.js b/next.config.js index 99791dff..7a65c472 100644 --- a/next.config.js +++ b/next.config.js @@ -63,7 +63,6 @@ const headers = [ { source: '/api/:path*', headers: [ - { key: 'Access-Control-Allow-Credentials', value: 'true' }, { key: 'Access-Control-Allow-Origin', value: '*' }, { key: 'Access-Control-Allow-Headers', value: '*' }, { key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' }, diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts index ae361934..f622de6a 100644 --- a/src/app/api/reports/insights/route.ts +++ b/src/app/api/reports/insights/route.ts @@ -16,8 +16,8 @@ export async function POST(request: Request) { const schema = z.object({ websiteId: z.string().uuid(), dateRange: z.object({ - startDate: z.date(), - endDate: z.date(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), }), fields: z .array( @@ -36,12 +36,6 @@ export async function POST(request: Request) { value: z.string(), }), ), - groups: z.array( - z.object({ - name: z.string(), - type: z.string(), - }), - ), }); const { auth, body, error } = await parseRequest(request, schema); diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts new file mode 100644 index 00000000..864ac375 --- /dev/null +++ b/src/app/api/send/route.ts @@ -0,0 +1,191 @@ +import { z } from 'zod'; +import { isbot } from 'isbot'; +import { serializeError } from 'serialize-error'; +import { createToken, parseToken, safeDecodeURI } from 'next-basics'; +import clickhouse from 'lib/clickhouse'; +import { parseRequest } from 'lib/request'; +import { badRequest, json, forbidden, serverError } from 'lib/response'; +import { fetchSession, fetchWebsite } from 'lib/load'; +import { getClientInfo, hasBlockedIp } from 'lib/detect'; +import { secret, uuid, visitSalt } from 'lib/crypto'; +import { createSession, saveEvent, saveSessionData } from 'queries'; +import { COLLECTION_TYPE } from 'lib/constants'; + +export async function POST(request: Request) { + // Bot check + if (!process.env.DISABLE_BOT_CHECK && isbot(request.headers.get('user-agent'))) { + return json({ beep: 'boop' }); + } + + const schema = z.object({ + type: z.enum(['event', 'identity']), + payload: z.object({ + website: z.string().uuid(), + data: z.object({}).passthrough().optional(), + hostname: z.string().max(100).optional(), + language: z.string().max(35).optional(), + referrer: z.string().optional(), + screen: z.string().max(11).optional(), + title: z.string().optional(), + url: z.string().optional(), + name: z.string().max(50).optional(), + tag: z.string().max(50).optional(), + ip: z.string().ip().optional(), + userAgent: z.string().optional(), + }), + }); + + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const { type, payload } = body; + + const { + website: websiteId, + hostname, + screen, + language, + url, + referrer, + name, + data, + title, + tag, + } = payload; + + // Cache check + let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null; + const cacheHeader = request.headers.get('x-umami-cache'); + + if (cacheHeader) { + const result = await parseToken(cacheHeader, secret()); + + if (result) { + cache = result; + } + } + + // Find website + if (!cache?.websiteId) { + const website = await fetchWebsite(websiteId); + + if (!website) { + return badRequest('Website not found.'); + } + } + + // Client info + const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } = + await getClientInfo(request, payload); + + // IP block + if (hasBlockedIp(ip)) { + return forbidden(); + } + + const sessionId = uuid(websiteId, hostname, ip, userAgent); + + // Find session + if (!cache?.sessionId) { + const session = await fetchSession(websiteId, sessionId); + + // Create a session if not found + if (!session && !clickhouse.enabled) { + try { + await createSession({ + id: sessionId, + websiteId, + hostname, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + }); + } catch (e: any) { + if (!e.message.toLowerCase().includes('unique constraint')) { + return serverError(serializeError(e)); + } + } + } + } + + // Visit info + let visitId = cache?.visitId || uuid(sessionId, visitSalt()); + const iat = Math.floor(new Date().getTime() / 1000); + + // Expire visit after 30 minutes + if (cache?.iat && iat - cache?.iat > 1800) { + visitId = uuid(sessionId, visitSalt()); + } + + if (type === COLLECTION_TYPE.event) { + // eslint-disable-next-line prefer-const + let [urlPath, urlQuery] = safeDecodeURI(url)?.split('?') || []; + let [referrerPath, referrerQuery] = safeDecodeURI(referrer)?.split('?') || []; + let referrerDomain = ''; + + if (!urlPath) { + urlPath = '/'; + } + + if (/^[\w-]+:\/\/\w+/.test(referrerPath)) { + const refUrl = new URL(referrer); + referrerPath = refUrl.pathname; + referrerQuery = refUrl.search.substring(1); + referrerDomain = refUrl.hostname.replace(/www\./, ''); + } + + if (process.env.REMOVE_TRAILING_SLASH) { + urlPath = urlPath.replace(/(.+)\/$/, '$1'); + } + + await saveEvent({ + websiteId, + sessionId, + visitId, + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle: title, + eventName: name, + eventData: data, + hostname, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + tag, + }); + } + + if (type === COLLECTION_TYPE.identify) { + if (!data) { + return badRequest('Data required.'); + } + + await saveSessionData({ + websiteId, + sessionId, + sessionData: data, + }); + } + + const token = createToken({ websiteId, sessionId, visitId, iat }, secret()); + + return json(token); +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a9e13c14..cbc954e3 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -324,6 +324,20 @@ export const BROWSERS = { yandexbrowser: 'Yandex', }; +export const IP_ADDRESS_HEADERS = [ + 'cf-connecting-ip', + 'x-client-ip', + 'x-forwarded-for', + 'do-connecting-ip', + 'fastly-client-ip', + 'true-client-ip', + 'x-real-ip', + 'x-cluster-client-ip', + 'x-forwarded', + 'forwarded', + 'x-appengine-user-ip', +]; + export const MAP_FILE = '/datamaps.world.json'; export const ISO_COUNTRIES = { diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 0bea4403..fe1c2124 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -1,34 +1,45 @@ import path from 'path'; -import { getClientIp } from 'request-ip'; import { browserName, detectOS } from 'detect-browser'; import isLocalhost from 'is-localhost-ip'; import ipaddr from 'ipaddr.js'; import maxmind from 'maxmind'; -import { safeDecodeURIComponent } from 'next-basics'; import { DESKTOP_OS, MOBILE_OS, DESKTOP_SCREEN_WIDTH, LAPTOP_SCREEN_WIDTH, MOBILE_SCREEN_WIDTH, + IP_ADDRESS_HEADERS, } from './constants'; -import { NextApiRequestCollect } from 'pages/api/send'; let lookup; -export function getIpAddress(req: NextApiRequestCollect) { - const customHeader = String(process.env.CLIENT_IP_HEADER).toLowerCase(); +export function getIpAddress(headers: Headers) { + const customHeader = process.env.CLIENT_IP_HEADER; - // Custom header - if (customHeader !== 'undefined' && req.headers[customHeader]) { - return req.headers[customHeader]; - } - // Cloudflare - else if (req.headers['cf-connecting-ip']) { - return req.headers['cf-connecting-ip']; + if (customHeader && headers.get(customHeader)) { + return headers.get(customHeader); } - return getClientIp(req); + const header = IP_ADDRESS_HEADERS.find(name => { + return headers.get(name); + }); + + const ip = headers.get(header); + + if (header === 'x-forwarded-for') { + return ip?.split[',']?.[0]?.trim(); + } + + if (header === 'forwarded') { + const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/); + + if (match) { + return match[1]; + } + } + + return ip; } export function getDevice(screen: string, os: string) { @@ -67,7 +78,7 @@ function getRegionCode(country: string, region: string) { return region.includes('-') ? region : `${country}-${region}`; } -function safeDecodeCfHeader(s: string | undefined | null): string | undefined | null { +function decodeHeader(s: string | undefined | null): string | undefined | null { if (s === undefined || s === null) { return s; } @@ -75,36 +86,38 @@ function safeDecodeCfHeader(s: string | undefined | null): string | undefined | return Buffer.from(s, 'latin1').toString('utf-8'); } -export async function getLocation(ip: string, req: NextApiRequestCollect) { +export async function getLocation(ip: string, headers: Headers) { // Ignore local ips if (await isLocalhost(ip)) { return; } - // Cloudflare headers - if (req.headers['cf-ipcountry']) { - const country = safeDecodeCfHeader(req.headers['cf-ipcountry']); - const subdivision1 = safeDecodeCfHeader(req.headers['cf-region-code']); - const city = safeDecodeCfHeader(req.headers['cf-ipcity']); + if (!process.env.SKIP_LOCATION_HEADERS) { + // Cloudflare headers + if (headers.get('cf-ipcountry')) { + const country = decodeHeader(headers.get('cf-ipcountry')); + const subdivision1 = decodeHeader(headers.get('cf-region-code')); + const city = decodeHeader(headers.get('cf-ipcity')); - return { - country, - subdivision1: getRegionCode(country, subdivision1), - city, - }; - } + return { + country, + subdivision1: getRegionCode(country, subdivision1), + city, + }; + } - // Vercel headers - if (req.headers['x-vercel-ip-country']) { - const country = safeDecodeURIComponent(req.headers['x-vercel-ip-country']); - const subdivision1 = safeDecodeURIComponent(req.headers['x-vercel-ip-country-region']); - const city = safeDecodeURIComponent(req.headers['x-vercel-ip-city']); + // Vercel headers + if (headers.get('x-vercel-ip-country')) { + const country = decodeHeader(headers.get('x-vercel-ip-country')); + const subdivision1 = decodeHeader(headers.get('x-vercel-ip-country-region')); + const city = decodeHeader(headers.get('x-vercel-ip-city')); - return { - country, - subdivision1: getRegionCode(country, subdivision1), - city, - }; + return { + country, + subdivision1: getRegionCode(country, subdivision1), + city, + }; + } } // Database lookup @@ -131,22 +144,22 @@ export async function getLocation(ip: string, req: NextApiRequestCollect) { } } -export async function getClientInfo(req: NextApiRequestCollect) { - const userAgent = req.body?.payload?.userAgent || req.headers['user-agent']; - const ip = req.body?.payload?.ip || getIpAddress(req); - const location = await getLocation(ip, req); +export async function getClientInfo(request: Request, payload: Record) { + const userAgent = payload?.userAgent || request.headers.get('user-agent'); + const ip = payload?.ip || getIpAddress(request.headers); + const location = await getLocation(ip, request.headers); const country = location?.country; const subdivision1 = location?.subdivision1; const subdivision2 = location?.subdivision2; const city = location?.city; const browser = browserName(userAgent); const os = detectOS(userAgent) as string; - const device = getDevice(req.body?.payload?.screen, os); + const device = getDevice(payload?.screen, os); return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device }; } -export function hasBlockedIp(req: NextApiRequestCollect) { +export function hasBlockedIp(clientIp: string) { const ignoreIps = process.env.IGNORE_IP; if (ignoreIps) { @@ -156,17 +169,19 @@ export function hasBlockedIp(req: NextApiRequestCollect) { ips.push(...ignoreIps.split(',').map(n => n.trim())); } - const clientIp = getIpAddress(req); - return ips.find(ip => { - if (ip === clientIp) return true; + if (ip === clientIp) { + return true; + } // CIDR notation if (ip.indexOf('/') > 0) { const addr = ipaddr.parse(clientIp); const range = ipaddr.parseCIDR(ip); - if (addr.kind() === range[0].kind() && addr.match(range)) return true; + if (addr.kind() === range[0].kind() && addr.match(range)) { + return true; + } } }); } diff --git a/src/lib/load.ts b/src/lib/load.ts index 3650f233..0cb09da4 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -1,26 +1,20 @@ -import { serializeError } from 'serialize-error'; -import { getWebsiteSession, getWebsite } from 'queries'; import { Website, Session } from '@prisma/client'; import { getClient, redisEnabled } from '@umami/redis-client'; +import { getWebsiteSession, getWebsite } from 'queries'; export async function fetchWebsite(websiteId: string): Promise { let website = null; - try { - if (redisEnabled) { - const redis = getClient(); + if (redisEnabled) { + const redis = getClient(); - website = await redis.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400); - } else { - website = await getWebsite(websiteId); - } + website = await redis.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400); + } else { + website = await getWebsite(websiteId); + } - if (!website || website.deletedAt) { - return null; - } - } catch (e) { - // eslint-disable-next-line no-console - console.log('FETCH WEBSITE ERROR:', serializeError(e)); + if (!website || website.deletedAt) { + return null; } return website; @@ -29,21 +23,16 @@ export async function fetchWebsite(websiteId: string): Promise { export async function fetchSession(websiteId: string, sessionId: string): Promise { let session = null; - try { - if (redisEnabled) { - const redis = getClient(); + if (redisEnabled) { + const redis = getClient(); - session = await redis.fetch( - `session:${sessionId}`, - () => getWebsiteSession(websiteId, sessionId), - 86400, - ); - } else { - session = await getWebsiteSession(websiteId, sessionId); - } - } catch (e) { - // eslint-disable-next-line no-console - console.log('FETCH SESSION ERROR:', serializeError(e)); + session = await redis.fetch( + `session:${sessionId}`, + () => getWebsiteSession(websiteId, sessionId), + 86400, + ); + } else { + session = await getWebsiteSession(websiteId, sessionId); } if (!session) { diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts deleted file mode 100644 index 3f7b9504..00000000 --- a/src/lib/middleware.ts +++ /dev/null @@ -1,105 +0,0 @@ -import cors from 'cors'; -import debug from 'debug'; -import { getClient, redisEnabled } from '@umami/redis-client'; -import { getAuthToken, parseShareToken } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { secret } from 'lib/crypto'; -import { getSession } from 'lib/session'; -import { - badRequest, - createMiddleware, - notFound, - parseSecureToken, - unauthorized, -} from 'next-basics'; -import { NextApiRequestCollect } from 'pages/api/send'; -import { getUser } from '../queries'; - -const log = debug('umami:middleware'); - -export const useCors = createMiddleware( - cors({ - // Cache CORS preflight request 24 hours by default - maxAge: Number(process.env.CORS_MAX_AGE) || 86400, - }), -); - -export const useSession = createMiddleware(async (req, res, next) => { - try { - const session = await getSession(req as NextApiRequestCollect); - - if (!session) { - log('useSession: Session not found'); - return badRequest(res, 'Session not found.'); - } - - (req as any).session = session; - } catch (e: any) { - if (e.message.startsWith('Website not found')) { - return notFound(res, e.message); - } - return badRequest(res, e.message); - } - - next(); -}); - -export const useAuth = createMiddleware(async (req, res, next) => { - const token = getAuthToken(req); - const payload = parseSecureToken(token, secret()); - const shareToken = await parseShareToken(req as any); - - let user = null; - const { userId, authKey, grant } = payload || {}; - - if (userId) { - user = await getUser(userId); - } else if (redisEnabled && authKey) { - const redis = getClient(); - - const key = await redis.get(authKey); - - if (key?.userId) { - user = await getUser(key.userId); - } - } - - if (process.env.NODE_ENV === 'development') { - log('useAuth:', { token, shareToken, payload, user, grant }); - } - - if (!user?.id && !shareToken) { - log('useAuth: User not authorized'); - return unauthorized(res); - } - - if (user) { - user.isAdmin = user.role === ROLES.admin; - } - - (req as any).auth = { - user, - grant, - token, - shareToken, - authKey, - }; - - next(); -}); - -export const useValidate = async (schema, req, res) => { - return createMiddleware(async (req: any, res, next) => { - try { - const rules = schema[req.method]; - - if (rules) { - rules.validateSync({ ...req.query, ...req.body }); - } - } catch (e: any) { - return badRequest(res, e.message); - } - - next(); - })(req, res); -}; diff --git a/src/lib/session.ts b/src/lib/session.ts deleted file mode 100644 index 0bfe0302..00000000 --- a/src/lib/session.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { secret, uuid, visitSalt } from 'lib/crypto'; -import { getClientInfo } from 'lib/detect'; -import { parseToken } from 'next-basics'; -import { NextApiRequestCollect } from 'pages/api/send'; -import { createSession } from 'queries'; -import clickhouse from './clickhouse'; -import { fetchSession, fetchWebsite } from './load'; -import { SessionData } from 'lib/types'; - -export async function getSession(req: NextApiRequestCollect): Promise { - const { payload } = req.body; - - if (!payload) { - throw new Error('Invalid payload.'); - } - - // Check if cache token is passed - const cacheToken = req.headers['x-umami-cache']; - - if (cacheToken) { - const result = await parseToken(cacheToken, secret()); - - // Token is valid - if (result) { - return result; - } - } - - // Verify payload - const { website: websiteId, hostname, screen, language } = payload; - - // Find website - const website = await fetchWebsite(websiteId); - - if (!website) { - throw new Error(`Website not found: ${websiteId}.`); - } - - const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } = - await getClientInfo(req); - - const sessionId = uuid(websiteId, hostname, ip, userAgent); - const visitId = uuid(sessionId, visitSalt()); - - // Clickhouse does not require session lookup - if (clickhouse.enabled) { - return { - id: sessionId, - websiteId, - visitId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - ip, - userAgent, - }; - } - - // Find session - let session = await fetchSession(websiteId, sessionId); - - // Create a session if not found - if (!session) { - try { - session = await createSession({ - id: sessionId, - websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - }); - } catch (e: any) { - if (!e.message.toLowerCase().includes('unique constraint')) { - throw e; - } - } - } - - return { ...session, visitId }; -} diff --git a/src/pages/api/_config.ts b/src/pages/api/_config.ts deleted file mode 100644 index adba894a..00000000 --- a/src/pages/api/_config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; - -export interface ConfigResponse { - telemetryDisabled: boolean; - trackerScriptName: string; - uiDisabled: boolean; - updatesDisabled: boolean; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - telemetryDisabled: !!process.env.DISABLE_TELEMETRY, - trackerScriptName: process.env.TRACKER_SCRIPT_NAME, - uiDisabled: !!process.env.DISABLE_UI, - updatesDisabled: !!process.env.DISABLE_UPDATES, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/_version.ts b/src/pages/api/_version.ts deleted file mode 100644 index 4453b56f..00000000 --- a/src/pages/api/_version.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; -import { CURRENT_VERSION } from 'lib/constants'; - -export interface VersionResponse { - version: string; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - version: CURRENT_VERSION, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/admin/_users.ts b/src/pages/api/admin/_users.ts deleted file mode 100644 index 4f03ec9f..00000000 --- a/src/pages/api/admin/_users.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { canViewUsers } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUsers } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid(), - username: yup.string().max(255).required(), - password: yup.string().required(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewUsers(req.auth))) { - return unauthorized(res); - } - - const users = await getUsers( - { - include: { - _count: { - select: { - websiteUser: { - where: { deletedAt: null }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/admin/_websites.ts b/src/pages/api/admin/_websites.ts deleted file mode 100644 index d7dd6b74..00000000 --- a/src/pages/api/admin/_websites.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { canViewAllWebsites } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsites } from 'queries'; -import * as yup from 'yup'; - -export interface WebsitesRequestQuery extends PageParams { - userId?: string; - includeOwnedTeams?: boolean; - includeAllTeams?: boolean; -} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewAllWebsites(req.auth))) { - return unauthorized(res); - } - - const { userId, includeOwnedTeams, includeAllTeams } = req.query; - - const websites = await getWebsites( - { - where: { - OR: [ - ...(userId && [{ userId }]), - ...(userId && includeOwnedTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - role: ROLES.teamOwner, - userId, - }, - }, - }, - }, - ] - : []), - ...(userId && includeAllTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - userId, - }, - }, - }, - }, - ] - : []), - ], - }, - include: { - user: { - select: { - username: true, - id: true, - }, - }, - team: { - where: { - deletedAt: null, - }, - include: { - teamUser: { - where: { - role: ROLES.teamOwner, - }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/_login.ts b/src/pages/api/auth/_login.ts deleted file mode 100644 index fc671785..00000000 --- a/src/pages/api/auth/_login.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; -import { secret } from 'lib/crypto'; -import { useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - checkPassword, - createSecureToken, - forbidden, - methodNotAllowed, - ok, - unauthorized, -} from 'next-basics'; -import { getUserByUsername } from 'queries'; -import * as yup from 'yup'; -import { ROLES } from 'lib/constants'; - -export interface LoginRequestBody { - username: string; - password: string; -} - -export interface LoginResponse { - token: string; - user: User; -} - -const schema = { - POST: yup.object().shape({ - username: yup.string().required(), - password: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.disableLogin) { - return forbidden(res); - } - - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { username, password } = req.body; - - const user = await getUserByUsername(username, { includePassword: true }); - - if (user && checkPassword(password, user.password)) { - if (redisEnabled) { - const token = await saveAuth({ userId: user.id }); - - return ok(res, { token, user }); - } - - const token = createSecureToken({ userId: user.id }, secret()); - const { id, username, role, createdAt } = user; - - return ok(res, { - token, - user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, - }); - } - - return unauthorized(res, 'message.incorrect-username-password'); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/_logout.ts b/src/pages/api/auth/_logout.ts deleted file mode 100644 index f1604989..00000000 --- a/src/pages/api/auth/_logout.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { methodNotAllowed, ok } from 'next-basics'; -import { getClient, redisEnabled } from '@umami/redis-client'; -import { useAuth } from 'lib/middleware'; -import { getAuthToken } from 'lib/auth'; -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async (req: NextApiRequest, res: NextApiResponse) => { - await useAuth(req, res); - - if (req.method === 'POST') { - if (redisEnabled) { - const redis = getClient(); - - await redis.del(getAuthToken(req)); - } - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/_sso.ts b/src/pages/api/auth/_sso.ts deleted file mode 100644 index c5560cb1..00000000 --- a/src/pages/api/auth/_sso.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, ok } from 'next-basics'; -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - if (redisEnabled && req.auth.user) { - const token = await saveAuth({ userId: req.auth.user.id }, 86400); - - return ok(res, { user: req.auth.user, token }); - } - - return badRequest(res); -}; diff --git a/src/pages/api/auth/_verify.ts b/src/pages/api/auth/_verify.ts deleted file mode 100644 index 3cc78ed3..00000000 --- a/src/pages/api/auth/_verify.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { ok } from 'next-basics'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - const { user } = req.auth; - - return ok(res, user); -}; diff --git a/src/pages/api/me/_index.ts b/src/pages/api/me/_index.ts deleted file mode 100644 index 93e97067..00000000 --- a/src/pages/api/me/_index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextApiResponse } from 'next'; -import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { ok } from 'next-basics'; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - - return ok(res, req.auth.user); -}; diff --git a/src/pages/api/me/_password.ts b/src/pages/api/me/_password.ts deleted file mode 100644 index 2ba91d86..00000000 --- a/src/pages/api/me/_password.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - badRequest, - checkPassword, - forbidden, - hashPassword, - methodNotAllowed, - ok, -} from 'next-basics'; -import { getUser, updateUser } from 'queries'; -import * as yup from 'yup'; - -export interface UserPasswordRequestBody { - currentPassword: string; - newPassword: string; -} - -const schema = { - POST: yup.object().shape({ - currentPassword: yup.string().required(), - newPassword: yup.string().min(8).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.CLOUD_MODE) { - return forbidden(res); - } - - await useAuth(req, res); - await useValidate(schema, req, res); - - const { currentPassword, newPassword } = req.body; - const { id: userId } = req.auth.user; - - if (req.method === 'POST') { - const user = await getUser(userId, { includePassword: true }); - - if (!checkPassword(currentPassword, user.password)) { - return badRequest(res, 'Current password is incorrect'); - } - - const password = hashPassword(newPassword); - - const updated = await updateUser(userId, { password }); - - return ok(res, updated); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/_teams.ts b/src/pages/api/me/_teams.ts deleted file mode 100644 index e40e548c..00000000 --- a/src/pages/api/me/_teams.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userTeamsRoute from 'pages/api/users/[userId]/_teams'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userTeamsRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/_websites.ts b/src/pages/api/me/_websites.ts deleted file mode 100644 index 5c3030e6..00000000 --- a/src/pages/api/me/_websites.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userWebsitesRoute from 'pages/api/users/[userId]/_websites'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userWebsitesRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/realtime/_[websiteId].ts b/src/pages/api/realtime/_[websiteId].ts deleted file mode 100644 index 08e9bc47..00000000 --- a/src/pages/api/realtime/_[websiteId].ts +++ /dev/null @@ -1,43 +0,0 @@ -import { startOfMinute, subMinutes } from 'date-fns'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRealtimeData } from 'queries'; -import * as yup from 'yup'; -import { REALTIME_RANGE } from 'lib/constants'; -import { TimezoneTest } from 'lib/yup'; - -export interface RealtimeRequestQuery { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, timezone } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - - const data = await getRealtimeData(websiteId, { startDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_[reportId].ts b/src/pages/api/reports/_[reportId].ts deleted file mode 100644 index 91b5fb51..00000000 --- a/src/pages/api/reports/_[reportId].ts +++ /dev/null @@ -1,102 +0,0 @@ -import { canDeleteReport, canUpdateReport, canViewReport } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, ReportType, YupRequest } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteReport, getReport, updateReport } from 'queries'; -import * as yup from 'yup'; - -export interface ReportRequestQuery { - reportId: string; -} - -export interface ReportRequestBody { - websiteId: string; - type: ReportType; - name: string; - description: string; - parameters: string; -} - -const schema: YupRequest = { - GET: yup.object().shape({ - reportId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - reportId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/i) - .required(), - name: yup.string().max(200).required(), - description: yup.string().max(500), - parameters: yup - .object() - .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), - }), - DELETE: yup.object().shape({ - reportId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { reportId } = req.query; - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const report = await getReport(reportId); - - if (!(await canViewReport(req.auth, report))) { - return unauthorized(res); - } - - report.parameters = JSON.parse(report.parameters); - - return ok(res, report); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - const report = await getReport(reportId); - - if (!(await canUpdateReport(req.auth, report))) { - return unauthorized(res); - } - - const result = await updateReport(reportId, { - websiteId, - userId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - if (req.method === 'DELETE') { - const report = await getReport(reportId); - - if (!(await canDeleteReport(req.auth, report))) { - return unauthorized(res); - } - - await deleteReport(reportId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_funnel.ts b/src/pages/api/reports/_funnel.ts deleted file mode 100644 index 35759a30..00000000 --- a/src/pages/api/reports/_funnel.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getFunnel } from 'queries'; -import * as yup from 'yup'; - -export interface FunnelRequestBody { - websiteId: string; - steps: { type: string; value: string }[]; - window: number; - dateRange: { - startDate: string; - endDate: string; - }; -} - -export interface FunnelResponse { - steps: { type: string; value: string }[]; - window: number; - startAt: number; - endAt: number; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - steps: yup - .array() - .of( - yup.object().shape({ - type: yup.string().required(), - value: yup.string().required(), - }), - ) - .min(2) - .required(), - window: yup.number().positive().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - steps, - window, - dateRange: { startDate, endDate }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getFunnel(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - windowMinutes: +window, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_goals.ts b/src/pages/api/reports/_goals.ts deleted file mode 100644 index f775dc3c..00000000 --- a/src/pages/api/reports/_goals.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getGoals } from 'queries/analytics/reports/getGoals'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; - goals: { type: string; value: string; goal: number }[]; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - goals: yup - .array() - .of( - yup.object().shape({ - type: yup - .string() - .matches(/url|event|event-data/i) - .required(), - value: yup.string().required(), - goal: yup.number().required(), - operator: yup - .string() - .matches(/count|sum|average/i) - .when('type', { - is: 'eventData', - then: yup.string().required(), - }), - property: yup.string().when('type', { - is: 'eventData', - then: yup.string().required(), - }), - }), - ) - .min(1) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - goals, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getGoals(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - goals, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_index.ts b/src/pages/api/reports/_index.ts deleted file mode 100644 index 38996b7a..00000000 --- a/src/pages/api/reports/_index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getReports } from 'queries'; -import * as yup from 'yup'; -import { canUpdateWebsite, canViewTeam, canViewWebsite } from 'lib/auth'; - -export interface ReportRequestBody { - websiteId: string; - name: string; - type: string; - description: string; - parameters: { - [key: string]: any; - }; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string().max(200).required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/i) - .required(), - description: yup.string().max(500), - parameters: yup - .object() - .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const { page, query, pageSize, websiteId, teamId } = req.query; - const filters = { - page, - pageSize, - query, - }; - - if ( - (websiteId && !(await canViewWebsite(req.auth, websiteId))) || - (teamId && !(await canViewTeam(req.auth, teamId))) - ) { - return unauthorized(res); - } - - const data = await getReports( - { - where: { - OR: [ - ...(websiteId ? [{ websiteId }] : []), - ...(teamId - ? [ - { - website: { - deletedAt: null, - teamId, - }, - }, - ] - : []), - ...(userId && !websiteId && !teamId - ? [ - { - website: { - deletedAt: null, - userId, - }, - }, - ] - : []), - ], - }, - include: { - website: { - select: { - domain: true, - }, - }, - }, - }, - filters, - ); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await createReport({ - id: uuid(), - userId, - websiteId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_insights.ts b/src/pages/api/reports/_insights.ts deleted file mode 100644 index ba4f643e..00000000 --- a/src/pages/api/reports/_insights.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getInsights } from 'queries'; -import * as yup from 'yup'; - -export interface InsightsRequestBody { - websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; - fields: { name: string; type: string; label: string }[]; - filters: { name: string; type: string; operator: string; value: string }[]; - groups: { name: string; type: string }[]; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - fields: yup - .array() - .of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - label: yup.string().required(), - }), - ) - .min(1) - .required(), - filters: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - operator: yup.string().required(), - value: yup.string().required(), - }), - ), - groups: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - }), - ), - }), -}; - -function convertFilters(filters: any[]) { - return filters.reduce((obj, filter) => { - obj[filter.name] = filter; - - return obj; - }, {}); -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - fields, - filters, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getInsights(websiteId, fields, { - ...convertFilters(filters), - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_journey.ts b/src/pages/api/reports/_journey.ts deleted file mode 100644 index dd3bd57b..00000000 --- a/src/pages/api/reports/_journey.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getJourney } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - steps: number; - startStep?: string; - endStep?: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - steps: yup.number().min(3).max(7).required(), - startStep: yup.string(), - endStep: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - steps, - startStep, - endStep, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getJourney(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - startStep, - endStep, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_retention.ts b/src/pages/api/reports/_retention.ts deleted file mode 100644 index f4d9b7df..00000000 --- a/src/pages/api/reports/_retention.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRetention } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - timezone: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - timezone: TimezoneTest, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - timezone, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRetention(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_revenue.ts b/src/pages/api/reports/_revenue.ts deleted file mode 100644 index d23ce55a..00000000 --- a/src/pages/api/reports/_revenue.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRevenue } from 'queries/analytics/reports/getRevenue'; -import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues'; -import * as yup from 'yup'; - -export interface RevenueRequestBody { - websiteId: string; - currency?: string; - timezone?: string; - dateRange: { startDate: string; endDate: string; unit?: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - unit: UnitTypeTest, - }) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startDate, endDate } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenueValues(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { - websiteId, - currency, - timezone, - dateRange: { startDate, endDate, unit }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenue(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - unit, - timezone, - currency, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/_utm.ts b/src/pages/api/reports/_utm.ts deleted file mode 100644 index 59399ee4..00000000 --- a/src/pages/api/reports/_utm.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUTM } from 'queries'; -import * as yup from 'yup'; - -export interface UTMRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate, timezone }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getUTM(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts deleted file mode 100644 index a47b91d2..00000000 --- a/src/pages/api/send.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* eslint-disable no-console */ -import { isbot } from 'isbot'; -import { NextApiRequest, NextApiResponse } from 'next'; -import { - badRequest, - createToken, - forbidden, - methodNotAllowed, - ok, - safeDecodeURI, -} from 'next-basics'; -import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants'; -import { secret, visitSalt, uuid } from 'lib/crypto'; -import { hasBlockedIp } from 'lib/detect'; -import { useCors, useSession, useValidate } from 'lib/middleware'; -import { CollectionType, YupRequest } from 'lib/types'; -import { saveEvent, saveSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface CollectRequestBody { - payload: { - website: string; - data?: { [key: string]: any }; - hostname?: string; - language?: string; - name?: string; - referrer?: string; - screen?: string; - tag?: string; - title?: string; - url: string; - ip?: string; - userAgent?: string; - }; - type: CollectionType; -} - -export interface NextApiRequestCollect extends NextApiRequest { - body: CollectRequestBody; - session: { - id: string; - websiteId: string; - visitId: string; - hostname: string; - browser: string; - os: string; - device: string; - screen: string; - language: string; - country: string; - subdivision1: string; - subdivision2: string; - city: string; - iat: number; - }; - headers: { [key: string]: any }; - yup: YupRequest; -} - -const schema = { - POST: yup.object().shape({ - payload: yup - .object() - .shape({ - data: yup.object(), - hostname: yup.string().matches(HOSTNAME_REGEX).max(100), - language: yup.string().max(35), - referrer: yup.string(), - screen: yup.string().max(11), - title: yup.string(), - url: yup.string(), - website: yup.string().uuid().required(), - name: yup.string().max(50), - tag: yup.string().max(50).nullable(), - ip: yup.string().matches(IP_REGEX), - userAgent: yup.string(), - }) - .required(), - type: yup - .string() - .matches(/event|identify/i) - .required(), - }), -}; - -export default async (req: NextApiRequestCollect, res: NextApiResponse) => { - await useCors(req, res); - - if (req.method === 'POST') { - if (!process.env.DISABLE_BOT_CHECK && isbot(req.headers['user-agent'])) { - return ok(res, { beep: 'boop' }); - } - - await useValidate(schema, req, res); - - if (hasBlockedIp(req)) { - return forbidden(res); - } - - const { type, payload } = req.body; - const { url, referrer, name, data, title, tag } = payload; - - await useSession(req, res); - - const session = req.session; - - if (!session?.id || !session?.websiteId) { - return ok(res, {}); - } - - const iat = Math.floor(new Date().getTime() / 1000); - - // expire visitId after 30 minutes - if (session.iat && iat - session.iat > 1800) { - session.visitId = uuid(session.id, visitSalt()); - } - - session.iat = iat; - - if (type === COLLECTION_TYPE.event) { - // eslint-disable-next-line prefer-const - let [urlPath, urlQuery] = safeDecodeURI(url)?.split('?') || []; - let [referrerPath, referrerQuery] = safeDecodeURI(referrer)?.split('?') || []; - let referrerDomain = ''; - - if (!urlPath) { - urlPath = '/'; - } - - if (/^[\w-]+:\/\/\w+/.test(referrerPath)) { - const refUrl = new URL(referrer); - referrerPath = refUrl.pathname; - referrerQuery = refUrl.search.substring(1); - referrerDomain = refUrl.hostname.replace(/www\./, ''); - } - - if (process.env.REMOVE_TRAILING_SLASH) { - urlPath = urlPath.replace(/(.+)\/$/, '$1'); - } - - await saveEvent({ - urlPath, - urlQuery, - referrerPath, - referrerQuery, - referrerDomain, - pageTitle: title, - eventName: name, - eventData: data, - ...session, - sessionId: session.id, - tag, - }); - } else if (type === COLLECTION_TYPE.identify) { - if (!data) { - return badRequest(res, 'Data required.'); - } - - await saveSessionData({ - websiteId: session.websiteId, - sessionId: session.id, - sessionData: data, - }); - } - - const cache = createToken(session, secret()); - - return ok(res, { cache }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/share/_[shareId].ts b/src/pages/api/share/_[shareId].ts deleted file mode 100644 index 26ac4cdc..00000000 --- a/src/pages/api/share/_[shareId].ts +++ /dev/null @@ -1,46 +0,0 @@ -import { secret } from 'lib/crypto'; -import { useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { createToken, methodNotAllowed, notFound, ok } from 'next-basics'; -import { getSharedWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface ShareRequestQuery { - shareId: string; -} - -export interface ShareResponse { - shareId: string; - token: string; -} - -const schema = { - GET: yup.object().shape({ - shareId: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useValidate(schema, req, res); - - const { shareId } = req.query; - - if (req.method === 'GET') { - const website = await getSharedWebsite(shareId); - - if (website) { - const data = { websiteId: website.id }; - const token = createToken(data, secret()); - - return ok(res, { ...data, token }); - } - - return notFound(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/_index.ts b/src/pages/api/teams/[teamId]/_index.ts deleted file mode 100644 index b731ee0c..00000000 --- a/src/pages/api/teams/[teamId]/_index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Team } from '@prisma/client'; -import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, notFound, ok, unauthorized } from 'next-basics'; -import { deleteTeam, getTeam, updateTeam } from 'queries'; -import * as yup from 'yup'; - -export interface TeamRequestQuery { - teamId: string; -} - -export interface TeamRequestBody { - name: string; - accessCode: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - id: yup.string().uuid().required(), - name: yup.string().max(50), - accessCode: yup.string().max(50), - }), - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const team = await getTeam(teamId, { includeMembers: true }); - - if (!team) { - return notFound(res); - } - - return ok(res, team); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const { name, accessCode } = req.body; - const data = { name, accessCode }; - - const updated = await updateTeam(teamId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - await deleteTeam(teamId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/_[userId].ts b/src/pages/api/teams/[teamId]/users/_[userId].ts deleted file mode 100644 index c1e80b1a..00000000 --- a/src/pages/api/teams/[teamId]/users/_[userId].ts +++ /dev/null @@ -1,85 +0,0 @@ -import { canDeleteTeamUser, canUpdateTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteTeamUser, getTeamUser, updateTeamUser } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery { - teamId: string; - userId: string; -} - -export interface TeamUserRequestBody { - role: string; -} - -const schema = { - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId, userId } = req.query; - - if (req.method === 'GET') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - return ok(res, teamUser); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - const { role } = req.body; - - await updateTeamUser(teamUser.id, { role }); - - return ok(res); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeamUser(req.auth, teamId, userId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - await deleteTeamUser(teamId, userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/_index.ts b/src/pages/api/teams/[teamId]/users/_index.ts deleted file mode 100644 index f25b99da..00000000 --- a/src/pages/api/teams/[teamId]/users/_index.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { canAddUserToTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getTeamUser, getTeamUsers } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery extends PageParams { - teamId: string; -} - -export interface TeamUserRequestBody { - userId: string; - role: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const users = await getTeamUsers( - { - where: { - teamId, - user: { - deletedAt: null, - }, - }, - include: { - user: { - select: { - id: true, - username: true, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - // admin function only - if (req.method === 'POST') { - if (!(await canAddUserToTeam(req.auth))) { - return unauthorized(res); - } - - const { userId, role } = req.body; - - const teamUser = await getTeamUser(teamId, userId); - - if (teamUser) { - return badRequest(res, 'User is already a member of the Team.'); - } - - const users = await createTeamUser(userId, teamId, role); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/websites/_index.ts b/src/pages/api/teams/[teamId]/websites/_index.ts deleted file mode 100644 index 75020fa4..00000000 --- a/src/pages/api/teams/[teamId]/websites/_index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as yup from 'yup'; -import { canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { ok, unauthorized } from 'next-basics'; -import { getTeamWebsites } from 'queries'; - -export interface TeamWebsiteRequestQuery extends PageParams { - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const websites = await getTeamWebsites(teamId, req.query); - - return ok(res, websites); - } -}; diff --git a/src/pages/api/teams/_index.ts b/src/pages/api/teams/_index.ts deleted file mode 100644 index 1e683469..00000000 --- a/src/pages/api/teams/_index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as yup from 'yup'; -import { Team } from '@prisma/client'; -import { canCreateTeam } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeam } from 'queries'; - -export interface TeamsRequestQuery extends PageParams {} -export interface TeamsRequestBody { - name: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(50).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'POST') { - if (!(await canCreateTeam(req.auth))) { - return unauthorized(res); - } - - const { name } = req.body; - - const team = await createTeam( - { - id: uuid(), - name, - accessCode: getRandomChars(16), - }, - userId, - ); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/_join.ts b/src/pages/api/teams/_join.ts deleted file mode 100644 index a9943f64..00000000 --- a/src/pages/api/teams/_join.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Team } from '@prisma/client'; -import { ROLES } from 'lib/constants'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, notFound, ok } from 'next-basics'; -import { createTeamUser, findTeam, getTeamUser } from 'queries'; -import * as yup from 'yup'; - -export interface TeamsJoinRequestBody { - accessCode: string; -} - -const schema = { - POST: yup.object().shape({ - accessCode: yup.string().max(50).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { accessCode } = req.body; - - const team = await findTeam({ - where: { - accessCode, - }, - }); - - if (!team) { - return notFound(res, 'message.team-not-found'); - } - - const teamUser = await getTeamUser(team.id, req.auth.user.id); - - if (teamUser) { - return methodNotAllowed(res, 'message.team-already-member'); - } - - await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_index.ts b/src/pages/api/users/[userId]/_index.ts deleted file mode 100644 index d69cad3c..00000000 --- a/src/pages/api/users/[userId]/_index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as yup from 'yup'; -import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteUser, getUser, getUserByUsername, updateUser } from 'queries'; - -export interface UserRequestQuery { - userId: string; -} - -export interface UserRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - username: yup.string().max(255), - password: yup.string(), - role: yup.string().matches(/admin|user|view-only/i), - }), - DELETE: yup.object().shape({ - userId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { isAdmin }, - } = req.auth; - const userId: string = req.query.userId; - - if (req.method === 'GET') { - if (!(await canViewUser(req.auth, userId))) { - return unauthorized(res); - } - - const user = await getUser(userId); - - return ok(res, user); - } - - if (req.method === 'POST') { - if (!(await canUpdateUser(req.auth, userId))) { - return unauthorized(res); - } - - const { username, password, role } = req.body; - - const user = await getUser(userId); - - const data: any = {}; - - if (password) { - data.password = hashPassword(password); - } - - // Only admin can change these fields - if (role && isAdmin) { - data.role = role; - } - - if (username && isAdmin) { - data.username = username; - } - - // Check when username changes - if (data.username && user.username !== data.username) { - const user = await getUserByUsername(username); - - if (user) { - return badRequest(res, 'User already exists'); - } - } - - const updated = await updateUser(userId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteUser(req.auth))) { - return unauthorized(res); - } - - if (userId === req.auth.user.id) { - return badRequest(res, 'You cannot delete yourself.'); - } - - await deleteUser(userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_teams.ts b/src/pages/api/users/[userId]/_teams.ts deleted file mode 100644 index 3f2af9e2..00000000 --- a/src/pages/api/users/[userId]/_teams.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserTeams } from 'queries'; - -export interface UserTeamsRequestQuery extends PageParams { - userId: string; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && (!userId || user.id !== userId)) { - return unauthorized(res); - } - - const teams = await getUserTeams(userId as string, req.query); - - return ok(res, teams); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_usage.ts b/src/pages/api/users/[userId]/_usage.ts deleted file mode 100644 index b5000395..00000000 --- a/src/pages/api/users/[userId]/_usage.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getAllUserWebsitesIncludingTeamOwner, getEventDataUsage, getEventUsage } from 'queries'; -import * as yup from 'yup'; - -export interface UserUsageRequestQuery { - userId: string; - startAt: string; - endAt: string; -} - -export interface UserUsageRequestResponse { - websiteEventUsage: number; - eventDataUsage: number; - websites: { - websiteEventUsage: number; - eventDataUsage: number; - websiteId: string; - websiteName: string; - }[]; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - - if (req.method === 'GET') { - if (!user.isAdmin) { - return unauthorized(res); - } - - const { userId, startAt, endAt } = req.query; - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const websites = await getAllUserWebsitesIncludingTeamOwner(userId); - - const websiteIds = websites.map(a => a.id); - - const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); - const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); - - const websiteUsage = websites.map(a => ({ - websiteId: a.id, - websiteName: a.name, - websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, - eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, - deletedAt: a.deletedAt, - })); - - const usage = websiteUsage.reduce( - (acc, cv) => { - acc.websiteEventUsage += cv.websiteEventUsage; - acc.eventDataUsage += cv.eventDataUsage; - - return acc; - }, - { websiteEventUsage: 0, eventDataUsage: 0 }, - ); - - const filteredWebsiteUsage = websiteUsage.filter( - a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), - ); - - return ok(res, { - ...usage, - websites: filteredWebsiteUsage, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/_websites.ts b/src/pages/api/users/[userId]/_websites.ts deleted file mode 100644 index 88a2bad1..00000000 --- a/src/pages/api/users/[userId]/_websites.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserWebsites } from 'queries'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId, page = 1, pageSize, query = '', ...rest } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && user.id !== userId) { - return unauthorized(res); - } - - const websites = await getUserWebsites(userId, { - page, - pageSize, - query, - ...rest, - }); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/_index.ts b/src/pages/api/users/_index.ts deleted file mode 100644 index 333670a9..00000000 --- a/src/pages/api/users/_index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canCreateUser } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createUser, getUserByUsername } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - username: string; - password: string; - id: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - username: yup.string().max(255).required(), - password: yup.string().required(), - id: yup.string().uuid(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - if (!(await canCreateUser(req.auth))) { - return unauthorized(res); - } - - const { username, password, role, id } = req.body; - - const existingUser = await getUserByUsername(username, { showDeleted: true }); - - if (existingUser) { - return badRequest(res, 'User already exists'); - } - - const created = await createUser({ - id: id || uuid(), - username, - password: hashPassword(password), - role: role ?? ROLES.user, - }); - - return ok(res, created); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_active.ts b/src/pages/api/websites/[websiteId]/_active.ts deleted file mode 100644 index d87a7818..00000000 --- a/src/pages/api/websites/[websiteId]/_active.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getActiveVisitors } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteActiveRequestQuery { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId as string))) { - return unauthorized(res); - } - - const result = await getActiveVisitors(websiteId as string); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_daterange.ts b/src/pages/api/websites/[websiteId]/_daterange.ts deleted file mode 100644 index 1aeb76cb..00000000 --- a/src/pages/api/websites/[websiteId]/_daterange.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteDateRange } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteDateRangeRequestQuery { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await getWebsiteDateRange(websiteId); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_index.ts b/src/pages/api/websites/[websiteId]/_index.ts deleted file mode 100644 index c60a8399..00000000 --- a/src/pages/api/websites/[websiteId]/_index.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; -import { Website, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; -import { SHARE_ID_REGEX } from 'lib/constants'; - -export interface WebsiteRequestQuery { - websiteId: string; -} - -export interface WebsiteRequestBody { - name: string; - domain: string; - shareId: string; -} - -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string(), - domain: yup.string(), - shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }).nullable(), - }), - DELETE: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const website = await getWebsite(websiteId); - - return ok(res, website); - } - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { name, domain, shareId } = req.body; - - try { - const website = await updateWebsite(websiteId, { name, domain, shareId }); - - return ok(res, website); - } catch (e: any) { - if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { - return serverError(res, 'That share ID is already taken.'); - } - - return serverError(res, e); - } - } - - if (req.method === 'DELETE') { - if (!(await canDeleteWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await deleteWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_metrics.ts b/src/pages/api/websites/[websiteId]/_metrics.ts deleted file mode 100644 index 1996a61a..00000000 --- a/src/pages/api/websites/[websiteId]/_metrics.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants'; -import { getPageviewMetrics, getSessionMetrics } from 'queries'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface WebsiteMetricsRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - language?: string; - event?: string; - limit?: number; - offset?: number; - search?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - language: yup.string(), - event: yup.string(), - limit: yup.number(), - offset: yup.number(), - search: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, limit, offset, search } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const column = FILTER_COLUMNS[type] || type; - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - }; - - if (search) { - filters[type] = { - name: type, - column, - operator: OPERATORS.contains, - value: search, - }; - } - - if (SESSION_COLUMNS.includes(type)) { - const data = await getSessionMetrics(websiteId, type, filters, limit, offset); - - if (type === 'language') { - const combined = {}; - - for (const { x, y } of data) { - const key = String(x).toLowerCase().split('-')[0]; - - if (combined[key] === undefined) { - combined[key] = { x: key, y }; - } else { - combined[key].y += y; - } - } - - return ok(res, Object.values(combined)); - } - - return ok(res, data); - } - - if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); - - return ok(res, data); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_pageviews.ts b/src/pages/api/websites/[websiteId]/_pageviews.ts deleted file mode 100644 index c3b6b797..00000000 --- a/src/pages/api/websites/[websiteId]/_pageviews.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getPageviewStats, getSessionStats } from 'queries'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { getCompareDate } from 'lib/date'; - -export interface WebsitePageviewRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - unit?: string; - timezone?: string; - url?: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate, unit } = await getRequestDateRange(req); - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, filters), - getSessionStats(websiteId, filters), - ]); - - if (compare) { - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const [comparePageviews, compareSessions] = await Promise.all([ - getPageviewStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - getSessionStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - ]); - - return ok(res, { - pageviews, - sessions, - startDate, - endDate, - compare: { - pageviews: comparePageviews, - sessions: compareSessions, - startDate: compareStartDate, - endDate: compareEndDate, - }, - }); - } - - return ok(res, { pageviews, sessions }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_reports.ts b/src/pages/api/websites/[websiteId]/_reports.ts deleted file mode 100644 index 86260634..00000000 --- a/src/pages/api/websites/[websiteId]/_reports.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteReports } from 'queries'; -import { pageInfo } from 'lib/schema'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { page, search, pageSize } = req.query; - - const data = await getWebsiteReports(websiteId, { - page, - pageSize, - search, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_reset.ts b/src/pages/api/websites/[websiteId]/_reset.ts deleted file mode 100644 index 82e769dc..00000000 --- a/src/pages/api/websites/[websiteId]/_reset.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canUpdateWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { resetWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteResetRequestQuery { - websiteId: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await resetWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_stats.ts b/src/pages/api/websites/[websiteId]/_stats.ts deleted file mode 100644 index dfc9198d..00000000 --- a/src/pages/api/websites/[websiteId]/_stats.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as yup from 'yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { getWebsiteStats } from 'queries'; -import { getCompareDate } from 'lib/date'; - -export interface WebsiteStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const prevPeriod = await getWebsiteStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - prev: Number(prevPeriod[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_transfer.ts b/src/pages/api/websites/[websiteId]/_transfer.ts deleted file mode 100644 index 56cf6bac..00000000 --- a/src/pages/api/websites/[websiteId]/_transfer.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { updateWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteTransferRequestQuery { - websiteId: string; -} - -export interface WebsiteTransferRequestBody { - userId?: string; - teamId?: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - userId: yup.string().uuid(), - teamId: yup.string().uuid(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - const { userId, teamId } = req.body; - - if (req.method === 'POST') { - if (userId) { - if (!(await canTransferWebsiteToUser(req.auth, websiteId, userId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId, - teamId: null, - }); - - return ok(res, website); - } else if (teamId) { - if (!(await canTransferWebsiteToTeam(req.auth, websiteId, teamId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId: null, - teamId, - }); - - return ok(res, website); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/_values.ts b/src/pages/api/websites/[websiteId]/_values.ts deleted file mode 100644 index 53d717a5..00000000 --- a/src/pages/api/websites/[websiteId]/_values.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { - badRequest, - methodNotAllowed, - ok, - safeDecodeURIComponent, - unauthorized, -} from 'next-basics'; -import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { getValues } from 'queries'; -import { getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface ValuesRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - search?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - search: yup.string(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, search } = req.query; - const { startDate, endDate } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!SESSION_COLUMNS.includes(type as string) && !EVENT_COLUMNS.includes(type as string)) { - return badRequest(res); - } - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); - - return ok( - res, - values - .map(({ value }) => safeDecodeURIComponent(value)) - .filter(n => n) - .sort(), - ); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_events.ts b/src/pages/api/websites/[websiteId]/event-data/_events.ts deleted file mode 100644 index bf0f409a..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_events.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataEvents } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - event?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - event: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, event } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataEvents(websiteId, { - startDate, - endDate, - event, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_fields.ts b/src/pages/api/websites/[websiteId]/event-data/_fields.ts deleted file mode 100644 index c5075c5e..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_fields.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataFields } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataFields(websiteId, { - startDate, - endDate, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_properties.ts b/src/pages/api/websites/[websiteId]/event-data/_properties.ts deleted file mode 100644 index 19e9bbb8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_stats.ts b/src/pages/api/websites/[websiteId]/event-data/_stats.ts deleted file mode 100644 index 7e440b88..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_stats.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataStats } from 'queries/index'; -import * as yup from 'yup'; - -export interface EventDataStatsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataStats(websiteId, { startDate, endDate }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/_values.ts b/src/pages/api/websites/[websiteId]/event-data/_values.ts deleted file mode 100644 index e5bb4ab8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/_values.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - eventName?: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - eventName: yup.string(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, eventName, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataValues(websiteId, { - startDate, - endDate, - eventName, - propertyName, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/_index.ts b/src/pages/api/websites/[websiteId]/events/_index.ts deleted file mode 100644 index 13b31fc0..00000000 --- a/src/pages/api/websites/[websiteId]/events/_index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteEvents } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteEvents(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/_series.ts b/src/pages/api/websites/[websiteId]/events/_series.ts deleted file mode 100644 index 6d67a264..00000000 --- a/src/pages/api/websites/[websiteId]/events/_series.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventMetrics } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteEventsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - unit?: string; - timezone?: string; - url: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone } = req.query; - const { startDate, endDate, unit } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const events = await getEventMetrics(websiteId, filters); - - return ok(res, events); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/_properties.ts b/src/pages/api/websites/[websiteId]/session-data/_properties.ts deleted file mode 100644 index 92e182d2..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/_properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/_values.ts b/src/pages/api/websites/[websiteId]/session-data/_values.ts deleted file mode 100644 index 98463f15..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/_values.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataValues(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts deleted file mode 100644 index 2b0fc084..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_activity.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionActivity } from 'queries'; - -export interface SessionActivityRequestQuery extends PageParams { - websiteId: string; - sessionId: string; - startAt: number; - endAt: number; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - startAt: yup.number().integer(), - endAt: yup.number().integer(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts deleted file mode 100644 index f627a208..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSession } from 'queries'; - -export interface WesiteSessionRequestQuery extends PageParams { - websiteId: string; - sessionId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getWebsiteSession(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts deleted file mode 100644 index c0c20064..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/_properties.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataRequestQuery { - sessionId: string; - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - sessionId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, sessionId } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getSessionData(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/_index.ts b/src/pages/api/websites/[websiteId]/sessions/_index.ts deleted file mode 100644 index 1809929c..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/_index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessions } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessions(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/_stats.ts b/src/pages/api/websites/[websiteId]/sessions/_stats.ts deleted file mode 100644 index fe92ce6f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/_stats.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSessionStats } from 'queries/analytics/sessions/getWebsiteSessionStats'; -import * as yup from 'yup'; - -export interface WebsiteSessionStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteSessionStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/_weekly.ts b/src/pages/api/websites/[websiteId]/sessions/_weekly.ts deleted file mode 100644 index b1c28c3f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/_weekly.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessionsWeekly } from 'queries'; -import { TimezoneTest } from 'lib/yup'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - timezone: TimezoneTest, - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt, timezone } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/_index.ts b/src/pages/api/websites/_index.ts deleted file mode 100644 index 483b77cd..00000000 --- a/src/pages/api/websites/_index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createWebsite } from 'queries'; -import userWebsitesRoute from 'pages/api/users/[userId]/_websites'; -import * as yup from 'yup'; -import { pageInfo } from 'lib/schema'; - -export interface WebsitesRequestQuery extends PageParams {} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - teamId: yup.string().nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - if (!req.query.userId) { - req.query.userId = userId; - } - - return userWebsitesRoute(req, res); - } - - if (req.method === 'POST') { - const { name, domain, shareId, teamId } = req.body; - - if ( - (teamId && !(await canCreateTeamWebsite(req.auth, teamId))) || - !(await canCreateWebsite(req.auth)) - ) { - return unauthorized(res); - } - - const data: any = { - id: uuid(), - createdBy: userId, - name, - domain, - shareId, - teamId, - }; - - if (!teamId) { - data.userId = userId; - } - - const website = await createWebsite(data); - - return ok(res, website); - } - - return methodNotAllowed(res); -};