diff --git a/.eslintrc.json b/.eslintrc.json index 691ae90c..82f6a122 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,13 +13,6 @@ "ecmaVersion": 11, "sourceType": "module" }, - "settings": { - "import/resolver": { - "node": { - "moduleDirectory": ["node_modules", "src/"] - } - } - }, "extends": [ "plugin:@typescript-eslint/recommended", "eslint:recommended", diff --git a/next-env.d.ts b/next-env.d.ts index 3cd7048e..40c3d680 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/next.config.js b/next.config.js index e4e55ab7..7a65c472 100644 --- a/next.config.js +++ b/next.config.js @@ -8,6 +8,7 @@ const basePath = process.env.BASE_PATH; const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT; const cloudMode = process.env.CLOUD_MODE; const cloudUrl = process.env.CLOUD_URL; +const corsMaxAge = process.env.CORS_MAX_AGE; const defaultLocale = process.env.DEFAULT_LOCALE; const disableLogin = process.env.DISABLE_LOGIN; const disableUI = process.env.DISABLE_UI; @@ -59,6 +60,15 @@ const trackerHeaders = [ ]; const headers = [ + { + source: '/api/:path*', + headers: [ + { key: 'Access-Control-Allow-Origin', value: '*' }, + { key: 'Access-Control-Allow-Headers', value: '*' }, + { key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' }, + { key: 'Access-Control-Max-Age', value: corsMaxAge || '86400' }, + ], + }, { source: '/:path*', headers: defaultHeaders, diff --git a/package.json b/package.json index 80c9446e..5a3546ee 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "url": "https://github.com/umami-software/umami.git" }, "scripts": { - "dev": "next dev -p 3000", + "dev": "next dev -p 3000 --turbo", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", @@ -76,6 +76,7 @@ "@tanstack/react-query": "^5.28.6", "@umami/prisma-client": "^0.14.0", "@umami/redis-client": "^0.24.0", + "bcryptjs": "^2.4.3", "chalk": "^4.1.1", "chart.js": "^4.4.2", "chartjs-adapter-date-fns": "^3.0.0", @@ -97,16 +98,17 @@ "is-docker": "^3.0.0", "is-localhost-ip": "^1.4.0", "isbot": "^5.1.16", + "jsonwebtoken": "^9.0.2", "kafkajs": "^2.1.0", - "maxmind": "^4.3.6", + "maxmind": "^4.3.24", "md5": "^2.3.0", "next": "15.0.4", - "next-basics": "^0.39.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "prisma": "6.1.0", + "pure-rand": "^6.1.0", "react": "^19.0.0", - "react-basics": "^0.125.0", + "react-basics": "^0.126.0", "react-dom": "^19.0.0", "react-error-boundary": "^4.0.4", "react-intl": "^6.5.5", @@ -118,7 +120,7 @@ "serialize-error": "^12.0.0", "thenby": "^1.3.4", "uuid": "^9.0.0", - "yup": "^0.32.11", + "zod": "^3.24.1", "zustand": "^4.5.5" }, "devDependencies": { @@ -136,6 +138,7 @@ "@types/node": "^22.10.5", "@types/react": "^19.0.4", "@types/react-dom": "^19.0.2", + "@types/react-intl": "^3.0.0", "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs index 9be07390..afeaf83c 100644 --- a/rollup.components.config.mjs +++ b/rollup.components.config.mjs @@ -19,13 +19,8 @@ const customResolver = resolve({ const aliasConfig = { entries: [ - { find: /^app/, replacement: path.resolve('./src/app') }, - { find: /^components/, replacement: path.resolve('./src/components') }, - { find: /^hooks/, replacement: path.resolve('./src/hooks') }, - { find: /^lib/, replacement: path.resolve('./src/lib') }, - { find: /^store/, replacement: path.resolve('./src/store') }, + { find: /^@/, replacement: path.resolve('./src/') }, { find: /^public/, replacement: path.resolve('./public') }, - { find: /^assets/, replacement: path.resolve('./src/assets') }, ], customResolver, }; diff --git a/scripts/change-password.js b/scripts/change-password.js deleted file mode 100644 index b12373a9..00000000 --- a/scripts/change-password.js +++ /dev/null @@ -1,94 +0,0 @@ -/* eslint-disable no-console */ -require('dotenv').config(); -const { hashPassword } = require('next-basics'); -const chalk = require('chalk'); -const prompts = require('prompts'); -const { PrismaClient } = require('@prisma/client'); - -const prisma = new PrismaClient(); - -const runQuery = async query => { - return query.catch(e => { - throw e; - }); -}; - -const updateUserByUsername = (username, data) => { - return runQuery( - prisma.user.update({ - where: { - username, - }, - data, - }), - ); -}; - -const changePassword = async (username, newPassword) => { - const password = hashPassword(newPassword); - return updateUserByUsername(username, { password }); -}; - -const getUsernameAndPassword = async () => { - let [username, password] = process.argv.slice(2); - if (username && password) { - return { username, password }; - } - - const questions = []; - if (!username) { - questions.push({ - type: 'text', - name: 'username', - message: 'Enter user to change password', - }); - } - if (!password) { - questions.push( - { - type: 'password', - name: 'password', - message: 'Enter new password', - }, - { - type: 'password', - name: 'confirmation', - message: 'Confirm new password', - }, - ); - } - - const answers = await prompts(questions); - if (answers.password !== answers.confirmation) { - throw new Error(`Passwords don't match`); - } - - return { - username: username || answers.username, - password: answers.password, - }; -}; - -(async () => { - let username, password; - - try { - ({ username, password } = await getUsernameAndPassword()); - } catch (error) { - console.log(chalk.redBright(error.message)); - return; - } - - try { - await changePassword(username, password); - console.log('Password changed for user', chalk.greenBright(username)); - } catch (error) { - if (error.meta.cause.includes('Record to update not found')) { - console.log('User not found:', chalk.redBright(username)); - } else { - throw error; - } - } - - prisma.$disconnect(); -})(); diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index efb38043..4cbb1c80 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -2,7 +2,7 @@ import { Loading } from 'react-basics'; import Script from 'next/script'; import { usePathname } from 'next/navigation'; -import { useLogin, useConfig } from 'components/hooks'; +import { useLogin, useConfig } from '@/components/hooks'; import UpdateNotice from './UpdateNotice'; export function App({ children }) { @@ -22,6 +22,10 @@ export function App({ children }) { return null; } + if (config.uiDisabled) { + return null; + } + return ( <> {children} diff --git a/src/app/(main)/NavBar.tsx b/src/app/(main)/NavBar.tsx index 5c8bba01..147f7085 100644 --- a/src/app/(main)/NavBar.tsx +++ b/src/app/(main)/NavBar.tsx @@ -1,17 +1,17 @@ 'use client'; +import { useEffect } from 'react'; import { Icon, Text } from 'react-basics'; import Link from 'next/link'; import classNames from 'classnames'; -import HamburgerButton from 'components/common/HamburgerButton'; -import ThemeButton from 'components/input/ThemeButton'; -import LanguageButton from 'components/input/LanguageButton'; -import ProfileButton from 'components/input/ProfileButton'; -import TeamsButton from 'components/input/TeamsButton'; -import Icons from 'components/icons'; -import { useMessages, useNavigation, useTeamUrl } from 'components/hooks'; +import HamburgerButton from '@/components/common/HamburgerButton'; +import ThemeButton from '@/components/input/ThemeButton'; +import LanguageButton from '@/components/input/LanguageButton'; +import ProfileButton from '@/components/input/ProfileButton'; +import TeamsButton from '@/components/input/TeamsButton'; +import Icons from '@/components/icons'; +import { useMessages, useNavigation, useTeamUrl } from '@/components/hooks'; +import { getItem, setItem } from '@/lib/storage'; import styles from './NavBar.module.css'; -import { useEffect } from 'react'; -import { getItem, setItem } from 'next-basics'; export function NavBar() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/UpdateNotice.tsx b/src/app/(main)/UpdateNotice.tsx index 553e1138..7504943b 100644 --- a/src/app/(main)/UpdateNotice.tsx +++ b/src/app/(main)/UpdateNotice.tsx @@ -1,10 +1,10 @@ import { useEffect, useCallback, useState } from 'react'; import { createPortal } from 'react-dom'; import { Button } from 'react-basics'; -import { setItem } from 'next-basics'; -import useStore, { checkVersion } from 'store/version'; -import { REPO_URL, VERSION_CHECK } from 'lib/constants'; -import { useMessages } from 'components/hooks'; +import { setItem } from '@/lib/storage'; +import useStore, { checkVersion } from '@/store/version'; +import { REPO_URL, VERSION_CHECK } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; import { usePathname } from 'next/navigation'; import styles from './UpdateNotice.module.css'; diff --git a/src/app/(main)/console/TestConsole.tsx b/src/app/(main)/console/TestConsole.tsx index 6842b26d..21b98df6 100644 --- a/src/app/(main)/console/TestConsole.tsx +++ b/src/app/(main)/console/TestConsole.tsx @@ -1,12 +1,12 @@ import { Button } from 'react-basics'; import Link from 'next/link'; import Script from 'next/script'; -import WebsiteSelect from 'components/input/WebsiteSelect'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import EventsChart from 'components/metrics/EventsChart'; +import WebsiteSelect from '@/components/input/WebsiteSelect'; +import Page from '@/components/layout/Page'; +import PageHeader from '@/components/layout/PageHeader'; +import EventsChart from '@/components/metrics/EventsChart'; import WebsiteChart from '../websites/[websiteId]/WebsiteChart'; -import { useApi, useNavigation } from 'components/hooks'; +import { useApi, useNavigation } from '@/components/hooks'; import styles from './TestConsole.module.css'; export function TestConsole({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/dashboard/DashboardEdit.tsx b/src/app/(main)/dashboard/DashboardEdit.tsx index 3360aaee..d15ae197 100644 --- a/src/app/(main)/dashboard/DashboardEdit.tsx +++ b/src/app/(main)/dashboard/DashboardEdit.tsx @@ -3,8 +3,8 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import classNames from 'classnames'; import { Button, Loading, Toggle, SearchField } from 'react-basics'; import { firstBy } from 'thenby'; -import useDashboard, { saveDashboard } from 'store/dashboard'; -import { useMessages, useWebsites } from 'components/hooks'; +import useDashboard, { saveDashboard } from '@/store/dashboard'; +import { useMessages, useWebsites } from '@/components/hooks'; import styles from './DashboardEdit.module.css'; const DRAG_ID = 'dashboard-website-ordering'; diff --git a/src/app/(main)/dashboard/DashboardPage.tsx b/src/app/(main)/dashboard/DashboardPage.tsx index 2a4bb916..83b27e09 100644 --- a/src/app/(main)/dashboard/DashboardPage.tsx +++ b/src/app/(main)/dashboard/DashboardPage.tsx @@ -1,14 +1,14 @@ 'use client'; import { Icon, Icons, Loading, Text } from 'react-basics'; -import PageHeader from 'components/layout/PageHeader'; -import Pager from 'components/common/Pager'; +import PageHeader from '@/components/layout/PageHeader'; +import Pager from '@/components/common/Pager'; import WebsiteChartList from '../websites/[websiteId]/WebsiteChartList'; -import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton'; -import DashboardEdit from 'app/(main)/dashboard/DashboardEdit'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { useMessages, useLocale, useTeamUrl, useWebsites } from 'components/hooks'; -import useDashboard from 'store/dashboard'; -import LinkButton from 'components/common/LinkButton'; +import DashboardSettingsButton from '@/app/(main)/dashboard/DashboardSettingsButton'; +import DashboardEdit from '@/app/(main)/dashboard/DashboardEdit'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { useMessages, useLocale, useTeamUrl, useWebsites } from '@/components/hooks'; +import useDashboard from '@/store/dashboard'; +import LinkButton from '@/components/common/LinkButton'; export function DashboardPage() { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/dashboard/DashboardSettingsButton.tsx b/src/app/(main)/dashboard/DashboardSettingsButton.tsx index 9e1d3dbc..1c473a22 100644 --- a/src/app/(main)/dashboard/DashboardSettingsButton.tsx +++ b/src/app/(main)/dashboard/DashboardSettingsButton.tsx @@ -1,7 +1,7 @@ import { TooltipPopup, Icon, Text, Flexbox, Button } from 'react-basics'; -import Icons from 'components/icons'; -import { saveDashboard } from 'store/dashboard'; -import { useMessages } from 'components/hooks'; +import Icons from '@/components/icons'; +import { saveDashboard } from '@/store/dashboard'; +import { useMessages } from '@/components/hooks'; export function DashboardSettingsButton() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index ba221990..dd1baec8 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,14 +1,10 @@ import { Metadata } from 'next'; import App from './App'; import NavBar from './NavBar'; -import Page from 'components/layout/Page'; +import Page from '@/components/layout/Page'; import styles from './layout.module.css'; -export default function ({ children }) { - if (process.env.DISABLE_UI) { - return null; - } - +export default async function ({ children }) { return (
diff --git a/src/app/(main)/profile/DateRangeSetting.tsx b/src/app/(main)/profile/DateRangeSetting.tsx index 25b5afbd..37d2ca43 100644 --- a/src/app/(main)/profile/DateRangeSetting.tsx +++ b/src/app/(main)/profile/DateRangeSetting.tsx @@ -1,8 +1,8 @@ -import DateFilter from 'components/input/DateFilter'; +import DateFilter from '@/components/input/DateFilter'; import { Button, Flexbox } from 'react-basics'; -import { useDateRange, useMessages } from 'components/hooks'; -import { DEFAULT_DATE_RANGE } from 'lib/constants'; -import { DateRange } from 'lib/types'; +import { useDateRange, useMessages } from '@/components/hooks'; +import { DEFAULT_DATE_RANGE } from '@/lib/constants'; +import { DateRange } from '@/lib/types'; import styles from './DateRangeSetting.module.css'; export function DateRangeSetting() { diff --git a/src/app/(main)/profile/LanguageSetting.tsx b/src/app/(main)/profile/LanguageSetting.tsx index 41ff3dde..a47394b3 100644 --- a/src/app/(main)/profile/LanguageSetting.tsx +++ b/src/app/(main)/profile/LanguageSetting.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; import { Button, Dropdown, Item, Flexbox } from 'react-basics'; -import { useLocale, useMessages } from 'components/hooks'; -import { DEFAULT_LOCALE } from 'lib/constants'; -import { languages } from 'lib/lang'; +import { useLocale, useMessages } from '@/components/hooks'; +import { DEFAULT_LOCALE } from '@/lib/constants'; +import { languages } from '@/lib/lang'; import styles from './LanguageSetting.module.css'; export function LanguageSetting() { diff --git a/src/app/(main)/profile/PasswordChangeButton.tsx b/src/app/(main)/profile/PasswordChangeButton.tsx index 552663ca..63249a2b 100644 --- a/src/app/(main)/profile/PasswordChangeButton.tsx +++ b/src/app/(main)/profile/PasswordChangeButton.tsx @@ -1,7 +1,7 @@ import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics'; -import PasswordEditForm from 'app/(main)/profile/PasswordEditForm'; -import Icons from 'components/icons'; -import { useMessages } from 'components/hooks'; +import PasswordEditForm from '@/app/(main)/profile/PasswordEditForm'; +import Icons from '@/components/icons'; +import { useMessages } from '@/components/hooks'; export function PasswordChangeButton() { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/profile/PasswordEditForm.tsx b/src/app/(main)/profile/PasswordEditForm.tsx index 1402efa2..c352d516 100644 --- a/src/app/(main)/profile/PasswordEditForm.tsx +++ b/src/app/(main)/profile/PasswordEditForm.tsx @@ -1,6 +1,6 @@ import { useRef } from 'react'; import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics'; -import { useApi, useMessages } from 'components/hooks'; +import { useApi, useMessages } from '@/components/hooks'; export function PasswordEditForm({ onSave, onClose }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/profile/ProfileHeader.tsx b/src/app/(main)/profile/ProfileHeader.tsx index e2d69cc7..05871fba 100644 --- a/src/app/(main)/profile/ProfileHeader.tsx +++ b/src/app/(main)/profile/ProfileHeader.tsx @@ -1,5 +1,5 @@ -import PageHeader from 'components/layout/PageHeader'; -import { useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; export function ProfileHeader() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/profile/ProfileSettings.tsx b/src/app/(main)/profile/ProfileSettings.tsx index 9c7db39f..f9dfe06d 100644 --- a/src/app/(main)/profile/ProfileSettings.tsx +++ b/src/app/(main)/profile/ProfileSettings.tsx @@ -1,11 +1,11 @@ import { Form, FormRow } from 'react-basics'; -import TimezoneSetting from 'app/(main)/profile/TimezoneSetting'; -import DateRangeSetting from 'app/(main)/profile/DateRangeSetting'; -import LanguageSetting from 'app/(main)/profile/LanguageSetting'; -import ThemeSetting from 'app/(main)/profile/ThemeSetting'; +import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting'; +import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting'; +import LanguageSetting from '@/app/(main)/profile/LanguageSetting'; +import ThemeSetting from '@/app/(main)/profile/ThemeSetting'; import PasswordChangeButton from './PasswordChangeButton'; -import { useLogin, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useLogin, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; export function ProfileSettings() { const { user } = useLogin(); diff --git a/src/app/(main)/profile/ThemeSetting.tsx b/src/app/(main)/profile/ThemeSetting.tsx index 577728b7..49ea7161 100644 --- a/src/app/(main)/profile/ThemeSetting.tsx +++ b/src/app/(main)/profile/ThemeSetting.tsx @@ -1,8 +1,8 @@ import classNames from 'classnames'; import { Button, Icon } from 'react-basics'; -import { useTheme } from 'components/hooks'; -import Sun from 'assets/sun.svg'; -import Moon from 'assets/moon.svg'; +import { useTheme } from '@/components/hooks'; +import Sun from '@/assets/sun.svg'; +import Moon from '@/assets/moon.svg'; import styles from './ThemeSetting.module.css'; export function ThemeSetting() { diff --git a/src/app/(main)/profile/TimezoneSetting.tsx b/src/app/(main)/profile/TimezoneSetting.tsx index 00858ac7..56c85813 100644 --- a/src/app/(main)/profile/TimezoneSetting.tsx +++ b/src/app/(main)/profile/TimezoneSetting.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Dropdown, Item, Button, Flexbox } from 'react-basics'; -import { useTimezone, useMessages } from 'components/hooks'; -import { getTimezone } from 'lib/date'; +import { useTimezone, useMessages } from '@/components/hooks'; +import { getTimezone } from '@/lib/date'; import styles from './TimezoneSetting.module.css'; const timezones = Intl.supportedValuesOf('timeZone'); diff --git a/src/app/(main)/reports/ReportDeleteButton.tsx b/src/app/(main)/reports/ReportDeleteButton.tsx index d51f7144..efd1da3c 100644 --- a/src/app/(main)/reports/ReportDeleteButton.tsx +++ b/src/app/(main)/reports/ReportDeleteButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import ConfirmationForm from 'components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; export function ReportDeleteButton({ reportId, @@ -11,7 +11,7 @@ export function ReportDeleteButton({ reportName: string; onDelete?: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const { del, useMutation } = useApi(); const { mutate, isPending, error } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`), @@ -39,12 +39,7 @@ export function ReportDeleteButton({ {(close: () => void) => ( {reportName} }} - /> - } + message={formatMessage(messages.confirmDelete, { target: {reportName} })} isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/reports/ReportsDataTable.tsx b/src/app/(main)/reports/ReportsDataTable.tsx index b03edfc2..0cc5a96c 100644 --- a/src/app/(main)/reports/ReportsDataTable.tsx +++ b/src/app/(main)/reports/ReportsDataTable.tsx @@ -1,6 +1,6 @@ -import { useReports } from 'components/hooks'; +import { useReports } from '@/components/hooks'; import ReportsTable from './ReportsTable'; -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import { ReactNode } from 'react'; export default function ReportsDataTable({ diff --git a/src/app/(main)/reports/ReportsHeader.tsx b/src/app/(main)/reports/ReportsHeader.tsx index 92f538ea..ff9cb294 100644 --- a/src/app/(main)/reports/ReportsHeader.tsx +++ b/src/app/(main)/reports/ReportsHeader.tsx @@ -1,8 +1,8 @@ -import PageHeader from 'components/layout/PageHeader'; +import PageHeader from '@/components/layout/PageHeader'; import { Icon, Icons, Text } from 'react-basics'; -import { useLogin, useMessages, useTeamUrl } from 'components/hooks'; -import LinkButton from 'components/common/LinkButton'; -import { ROLES } from 'lib/constants'; +import { useLogin, useMessages, useTeamUrl } from '@/components/hooks'; +import LinkButton from '@/components/common/LinkButton'; +import { ROLES } from '@/lib/constants'; export function ReportsHeader() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/reports/ReportsPage.tsx b/src/app/(main)/reports/ReportsPage.tsx index 5b031cfa..64d43c70 100644 --- a/src/app/(main)/reports/ReportsPage.tsx +++ b/src/app/(main)/reports/ReportsPage.tsx @@ -2,7 +2,7 @@ import { Metadata } from 'next'; import ReportsHeader from './ReportsHeader'; import ReportsDataTable from './ReportsDataTable'; -import { useTeamUrl } from 'components/hooks'; +import { useTeamUrl } from '@/components/hooks'; export default function ReportsPage() { const { teamId } = useTeamUrl(); diff --git a/src/app/(main)/reports/ReportsTable.tsx b/src/app/(main)/reports/ReportsTable.tsx index c35468aa..a891b6d0 100644 --- a/src/app/(main)/reports/ReportsTable.tsx +++ b/src/app/(main)/reports/ReportsTable.tsx @@ -1,7 +1,7 @@ import { GridColumn, GridTable, Icon, Icons, Text } from 'react-basics'; -import LinkButton from 'components/common/LinkButton'; -import { useMessages, useLogin, useTeamUrl } from 'components/hooks'; -import { REPORT_TYPES } from 'lib/constants'; +import LinkButton from '@/components/common/LinkButton'; +import { useMessages, useLogin, useTeamUrl } from '@/components/hooks'; +import { REPORT_TYPES } from '@/lib/constants'; import ReportDeleteButton from './ReportDeleteButton'; export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) { diff --git a/src/app/(main)/reports/[reportId]/BaseParameters.tsx b/src/app/(main)/reports/[reportId]/BaseParameters.tsx index 77de51f3..1f4881be 100644 --- a/src/app/(main)/reports/[reportId]/BaseParameters.tsx +++ b/src/app/(main)/reports/[reportId]/BaseParameters.tsx @@ -1,9 +1,9 @@ import { useContext } from 'react'; import { FormRow } from 'react-basics'; -import { parseDateRange } from 'lib/date'; -import DateFilter from 'components/input/DateFilter'; -import WebsiteSelect from 'components/input/WebsiteSelect'; -import { useMessages, useTeamUrl, useWebsite } from 'components/hooks'; +import { parseDateRange } from '@/lib/date'; +import DateFilter from '@/components/input/DateFilter'; +import WebsiteSelect from '@/components/input/WebsiteSelect'; +import { useMessages, useTeamUrl, useWebsite } from '@/components/hooks'; import { ReportContext } from './Report'; import styles from './BaseParameters.module.css'; diff --git a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx b/src/app/(main)/reports/[reportId]/FieldAddForm.tsx index 9217ce4d..6560a947 100644 --- a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldAddForm.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { createPortal } from 'react-dom'; -import { REPORT_PARAMETERS } from 'lib/constants'; +import { REPORT_PARAMETERS } from '@/lib/constants'; import PopupForm from './PopupForm'; import FieldSelectForm from './FieldSelectForm'; diff --git a/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx b/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx index 6b5bf636..5db0e580 100644 --- a/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx @@ -1,5 +1,5 @@ import { Form, FormRow, Menu, Item } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export default function FieldAggregateForm({ name, diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx index a1417780..d171c780 100644 --- a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx @@ -1,6 +1,12 @@ -import { useFilters, useFormat, useLocale, useMessages, useWebsiteValues } from 'components/hooks'; -import { OPERATORS } from 'lib/constants'; -import { isEqualsOperator } from 'lib/params'; +import { + useFilters, + useFormat, + useLocale, + useMessages, + useWebsiteValues, +} from '@/components/hooks'; +import { OPERATORS } from '@/lib/constants'; +import { isEqualsOperator } from '@/lib/params'; import { useMemo, useState } from 'react'; import { Button, @@ -226,7 +232,7 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => { return ( - {values?.map((value: any) => { + {values?.map(({ value }) => { return {formatValue(value, type)}; })} diff --git a/src/app/(main)/reports/[reportId]/FieldParameters.tsx b/src/app/(main)/reports/[reportId]/FieldParameters.tsx index 36cfbda9..de80cc69 100644 --- a/src/app/(main)/reports/[reportId]/FieldParameters.tsx +++ b/src/app/(main)/reports/[reportId]/FieldParameters.tsx @@ -1,5 +1,5 @@ -import { useFields, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; +import { useFields, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; import { useContext } from 'react'; import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; import FieldSelectForm from '../[reportId]/FieldSelectForm'; diff --git a/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx b/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx index dfd402cf..f73d59f7 100644 --- a/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx @@ -1,5 +1,5 @@ import { Menu, Item, Form, FormRow } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './FieldSelectForm.module.css'; import { Key } from 'react'; diff --git a/src/app/(main)/reports/[reportId]/FilterParameters.tsx b/src/app/(main)/reports/[reportId]/FilterParameters.tsx index ddbe4d1e..538c4ce5 100644 --- a/src/app/(main)/reports/[reportId]/FilterParameters.tsx +++ b/src/app/(main)/reports/[reportId]/FilterParameters.tsx @@ -1,13 +1,13 @@ import { useContext } from 'react'; -import { useMessages, useFormat, useFilters, useFields } from 'components/hooks'; -import Icons from 'components/icons'; +import { useMessages, useFormat, useFilters, useFields } from '@/components/hooks'; +import Icons from '@/components/icons'; import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; import FilterSelectForm from '../[reportId]/FilterSelectForm'; import ParameterList from '../[reportId]/ParameterList'; import PopupForm from '../[reportId]/PopupForm'; import { ReportContext } from './Report'; import FieldFilterEditForm from '../[reportId]/FieldFilterEditForm'; -import { isSearchOperator } from 'lib/params'; +import { isSearchOperator } from '@/lib/params'; import styles from './FilterParameters.module.css'; export function FilterParameters() { diff --git a/src/app/(main)/reports/[reportId]/ParameterList.tsx b/src/app/(main)/reports/[reportId]/ParameterList.tsx index d4b6477b..3c0401a0 100644 --- a/src/app/(main)/reports/[reportId]/ParameterList.tsx +++ b/src/app/(main)/reports/[reportId]/ParameterList.tsx @@ -1,8 +1,8 @@ import { ReactNode } from 'react'; import { Icon } from 'react-basics'; -import Icons from 'components/icons'; -import Empty from 'components/common/Empty'; -import { useMessages } from 'components/hooks'; +import Icons from '@/components/icons'; +import Empty from '@/components/common/Empty'; +import { useMessages } from '@/components/hooks'; import styles from './ParameterList.module.css'; import classNames from 'classnames'; diff --git a/src/app/(main)/reports/[reportId]/Report.tsx b/src/app/(main)/reports/[reportId]/Report.tsx index d6de9d42..1aed007c 100644 --- a/src/app/(main)/reports/[reportId]/Report.tsx +++ b/src/app/(main)/reports/[reportId]/Report.tsx @@ -1,7 +1,7 @@ import { createContext, ReactNode } from 'react'; import { Loading } from 'react-basics'; import classNames from 'classnames'; -import { useReport } from 'components/hooks'; +import { useReport } from '@/components/hooks'; import styles from './Report.module.css'; export const ReportContext = createContext(null); diff --git a/src/app/(main)/reports/[reportId]/ReportHeader.tsx b/src/app/(main)/reports/[reportId]/ReportHeader.tsx index 7ab80fcd..816a2df3 100644 --- a/src/app/(main)/reports/[reportId]/ReportHeader.tsx +++ b/src/app/(main)/reports/[reportId]/ReportHeader.tsx @@ -1,10 +1,10 @@ import { useContext } from 'react'; import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics'; -import { useMessages, useApi, useNavigation, useTeamUrl } from 'components/hooks'; +import { useMessages, useApi, useNavigation, useTeamUrl } from '@/components/hooks'; import { ReportContext } from './Report'; import styles from './ReportHeader.module.css'; -import { REPORT_TYPES } from 'lib/constants'; -import Breadcrumb from 'components/common/Breadcrumb'; +import { REPORT_TYPES } from '@/lib/constants'; +import Breadcrumb from '@/components/common/Breadcrumb'; export function ReportHeader({ icon }) { const { report, updateReport } = useContext(ReportContext); diff --git a/src/app/(main)/reports/[reportId]/ReportPage.tsx b/src/app/(main)/reports/[reportId]/ReportPage.tsx index da1a0342..8a3a94ad 100644 --- a/src/app/(main)/reports/[reportId]/ReportPage.tsx +++ b/src/app/(main)/reports/[reportId]/ReportPage.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useReport } from 'components/hooks'; +import { useReport } from '@/components/hooks'; import EventDataReport from '../event-data/EventDataReport'; import FunnelReport from '../funnel/FunnelReport'; import GoalReport from '../goals/GoalsReport'; diff --git a/src/app/(main)/reports/create/ReportTemplates.tsx b/src/app/(main)/reports/create/ReportTemplates.tsx index af19d4aa..c26e3a91 100644 --- a/src/app/(main)/reports/create/ReportTemplates.tsx +++ b/src/app/(main)/reports/create/ReportTemplates.tsx @@ -1,12 +1,12 @@ -import Funnel from 'assets/funnel.svg'; -import Money from 'assets/money.svg'; -import Lightbulb from 'assets/lightbulb.svg'; -import Magnet from 'assets/magnet.svg'; -import Path from 'assets/path.svg'; -import Tag from 'assets/tag.svg'; -import Target from 'assets/target.svg'; -import { useMessages, useTeamUrl } from 'components/hooks'; -import PageHeader from 'components/layout/PageHeader'; +import Funnel from '@/assets/funnel.svg'; +import Money from '@/assets/money.svg'; +import Lightbulb from '@/assets/lightbulb.svg'; +import Magnet from '@/assets/magnet.svg'; +import Path from '@/assets/path.svg'; +import Tag from '@/assets/tag.svg'; +import Target from '@/assets/target.svg'; +import { useMessages, useTeamUrl } from '@/components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; import Link from 'next/link'; import { Button, Icon, Icons, Text } from 'react-basics'; import styles from './ReportTemplates.module.css'; diff --git a/src/app/(main)/reports/event-data/EventDataParameters.tsx b/src/app/(main)/reports/event-data/EventDataParameters.tsx index 7b61c112..9e931cf5 100644 --- a/src/app/(main)/reports/event-data/EventDataParameters.tsx +++ b/src/app/(main)/reports/event-data/EventDataParameters.tsx @@ -1,9 +1,9 @@ import { useContext } from 'react'; import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; -import Empty from 'components/common/Empty'; -import Icons from 'components/icons'; -import { useApi, useMessages } from 'components/hooks'; -import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants'; +import Empty from '@/components/common/Empty'; +import Icons from '@/components/icons'; +import { useApi, useMessages } from '@/components/hooks'; +import { DATA_TYPES, REPORT_PARAMETERS } from '@/lib/constants'; import { ReportContext } from '../[reportId]/Report'; import FieldAddForm from '../[reportId]/FieldAddForm'; import ParameterList from '../[reportId]/ParameterList'; diff --git a/src/app/(main)/reports/event-data/EventDataReport.tsx b/src/app/(main)/reports/event-data/EventDataReport.tsx index fb786b31..8205a488 100644 --- a/src/app/(main)/reports/event-data/EventDataReport.tsx +++ b/src/app/(main)/reports/event-data/EventDataReport.tsx @@ -4,7 +4,7 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import EventDataParameters from './EventDataParameters'; import EventDataTable from './EventDataTable'; -import Nodes from 'assets/nodes.svg'; +import Nodes from '@/assets/nodes.svg'; const defaultParameters = { type: 'event-data', diff --git a/src/app/(main)/reports/event-data/EventDataTable.tsx b/src/app/(main)/reports/event-data/EventDataTable.tsx index 740cbce4..f42e792d 100644 --- a/src/app/(main)/reports/event-data/EventDataTable.tsx +++ b/src/app/(main)/reports/event-data/EventDataTable.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { GridTable, GridColumn } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; export function EventDataTable() { diff --git a/src/app/(main)/reports/funnel/FunnelChart.tsx b/src/app/(main)/reports/funnel/FunnelChart.tsx index 0da71d6f..be3da614 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.tsx +++ b/src/app/(main)/reports/funnel/FunnelChart.tsx @@ -1,8 +1,8 @@ import { useContext } from 'react'; import classNames from 'classnames'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; import styles from './FunnelChart.module.css'; export interface FunnelChartProps { diff --git a/src/app/(main)/reports/funnel/FunnelParameters.tsx b/src/app/(main)/reports/funnel/FunnelParameters.tsx index ef4ffbfb..3db57135 100644 --- a/src/app/(main)/reports/funnel/FunnelParameters.tsx +++ b/src/app/(main)/reports/funnel/FunnelParameters.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Icon, Form, @@ -12,7 +12,7 @@ import { TextField, Button, } from 'react-basics'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import FunnelStepAddForm from './FunnelStepAddForm'; import { ReportContext } from '../[reportId]/Report'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/funnel/FunnelReport.tsx b/src/app/(main)/reports/funnel/FunnelReport.tsx index 850bbd90..e0c90e4a 100644 --- a/src/app/(main)/reports/funnel/FunnelReport.tsx +++ b/src/app/(main)/reports/funnel/FunnelReport.tsx @@ -4,8 +4,8 @@ import Report from '../[reportId]/Report'; import ReportHeader from '../[reportId]/ReportHeader'; import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; -import Funnel from 'assets/funnel.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Funnel from '@/assets/funnel.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.funnel, diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx index 7d77b0c7..d7917d7d 100644 --- a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx +++ b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics'; import styles from './FunnelStepAddForm.module.css'; diff --git a/src/app/(main)/reports/goals/GoalsAddForm.tsx b/src/app/(main)/reports/goals/GoalsAddForm.tsx index a82eea28..b7354533 100644 --- a/src/app/(main)/reports/goals/GoalsAddForm.tsx +++ b/src/app/(main)/reports/goals/GoalsAddForm.tsx @@ -1,4 +1,4 @@ -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { useState } from 'react'; import { Button, Dropdown, Flexbox, FormRow, Item, TextField } from 'react-basics'; import styles from './GoalsAddForm.module.css'; diff --git a/src/app/(main)/reports/goals/GoalsChart.tsx b/src/app/(main)/reports/goals/GoalsChart.tsx index dedbd07c..34ea485e 100644 --- a/src/app/(main)/reports/goals/GoalsChart.tsx +++ b/src/app/(main)/reports/goals/GoalsChart.tsx @@ -1,8 +1,8 @@ import { useContext } from 'react'; import classNames from 'classnames'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; import styles from './GoalsChart.module.css'; export function GoalsChart({ className }: { className?: string; isLoading?: boolean }) { diff --git a/src/app/(main)/reports/goals/GoalsParameters.tsx b/src/app/(main)/reports/goals/GoalsParameters.tsx index 8d85dc20..51866645 100644 --- a/src/app/(main)/reports/goals/GoalsParameters.tsx +++ b/src/app/(main)/reports/goals/GoalsParameters.tsx @@ -1,6 +1,6 @@ -import { useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import { formatNumber } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { formatNumber } from '@/lib/format'; import { useContext } from 'react'; import { Button, diff --git a/src/app/(main)/reports/goals/GoalsReport.tsx b/src/app/(main)/reports/goals/GoalsReport.tsx index 020d7d09..ae540f3b 100644 --- a/src/app/(main)/reports/goals/GoalsReport.tsx +++ b/src/app/(main)/reports/goals/GoalsReport.tsx @@ -4,8 +4,8 @@ import Report from '../[reportId]/Report'; import ReportHeader from '../[reportId]/ReportHeader'; import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; -import Target from 'assets/target.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Target from '@/assets/target.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.goals, diff --git a/src/app/(main)/reports/insights/InsightsParameters.tsx b/src/app/(main)/reports/insights/InsightsParameters.tsx index 7f58de6a..6b3402fb 100644 --- a/src/app/(main)/reports/insights/InsightsParameters.tsx +++ b/src/app/(main)/reports/insights/InsightsParameters.tsx @@ -1,4 +1,4 @@ -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { useContext } from 'react'; import { Form, FormButtons, SubmitButton } from 'react-basics'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/insights/InsightsReport.tsx b/src/app/(main)/reports/insights/InsightsReport.tsx index adfbbd20..d43576fa 100644 --- a/src/app/(main)/reports/insights/InsightsReport.tsx +++ b/src/app/(main)/reports/insights/InsightsReport.tsx @@ -4,8 +4,8 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import InsightsParameters from './InsightsParameters'; import InsightsTable from './InsightsTable'; -import Lightbulb from 'assets/lightbulb.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Lightbulb from '@/assets/lightbulb.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.insights, diff --git a/src/app/(main)/reports/insights/InsightsTable.tsx b/src/app/(main)/reports/insights/InsightsTable.tsx index 692d7824..6864d919 100644 --- a/src/app/(main)/reports/insights/InsightsTable.tsx +++ b/src/app/(main)/reports/insights/InsightsTable.tsx @@ -1,9 +1,9 @@ import { useContext, useEffect, useState } from 'react'; import { GridTable, GridColumn } from 'react-basics'; -import { useFormat, useMessages } from 'components/hooks'; +import { useFormat, useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { formatShortTime } from 'lib/format'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { formatShortTime } from '@/lib/format'; export function InsightsTable() { const [fields, setFields] = useState([]); diff --git a/src/app/(main)/reports/journey/JourneyParameters.tsx b/src/app/(main)/reports/journey/JourneyParameters.tsx index eca9c42d..ffa5df89 100644 --- a/src/app/(main)/reports/journey/JourneyParameters.tsx +++ b/src/app/(main)/reports/journey/JourneyParameters.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Dropdown, Form, diff --git a/src/app/(main)/reports/journey/JourneyReport.tsx b/src/app/(main)/reports/journey/JourneyReport.tsx index 9048b3d1..4322fa2a 100644 --- a/src/app/(main)/reports/journey/JourneyReport.tsx +++ b/src/app/(main)/reports/journey/JourneyReport.tsx @@ -5,8 +5,8 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import JourneyParameters from './JourneyParameters'; import JourneyView from './JourneyView'; -import Path from 'assets/path.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Path from '@/assets/path.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.journey, diff --git a/src/app/(main)/reports/journey/JourneyView.tsx b/src/app/(main)/reports/journey/JourneyView.tsx index ae980a6e..abddf023 100644 --- a/src/app/(main)/reports/journey/JourneyView.tsx +++ b/src/app/(main)/reports/journey/JourneyView.tsx @@ -2,11 +2,11 @@ import { useContext, useMemo, useState } from 'react'; import { TextOverflow, TooltipPopup } from 'react-basics'; import { firstBy } from 'thenby'; import classNames from 'classnames'; -import { useEscapeKey, useMessages } from 'components/hooks'; -import { objectToArray } from 'lib/data'; +import { useEscapeKey, useMessages } from '@/components/hooks'; +import { objectToArray } from '@/lib/data'; import { ReportContext } from '../[reportId]/Report'; import styles from './JourneyView.module.css'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; const NODE_HEIGHT = 60; const NODE_GAP = 10; diff --git a/src/app/(main)/reports/retention/RetentionParameters.tsx b/src/app/(main)/reports/retention/RetentionParameters.tsx index f441177c..56cbdbd3 100644 --- a/src/app/(main)/reports/retention/RetentionParameters.tsx +++ b/src/app/(main)/reports/retention/RetentionParameters.tsx @@ -1,10 +1,10 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics'; import { ReportContext } from '../[reportId]/Report'; -import { MonthSelect } from 'components/input/MonthSelect'; +import { MonthSelect } from '@/components/input/MonthSelect'; import BaseParameters from '../[reportId]/BaseParameters'; -import { parseDateRange } from 'lib/date'; +import { parseDateRange } from '@/lib/date'; export function RetentionParameters() { const { report, runReport, isRunning, updateReport } = useContext(ReportContext); diff --git a/src/app/(main)/reports/retention/RetentionReport.tsx b/src/app/(main)/reports/retention/RetentionReport.tsx index 81b565dc..054a1a66 100644 --- a/src/app/(main)/reports/retention/RetentionReport.tsx +++ b/src/app/(main)/reports/retention/RetentionReport.tsx @@ -4,9 +4,9 @@ import Report from '../[reportId]/Report'; import ReportHeader from '../[reportId]/ReportHeader'; import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; -import Magnet from 'assets/magnet.svg'; -import { REPORT_TYPES } from 'lib/constants'; -import { parseDateRange } from 'lib/date'; +import Magnet from '@/assets/magnet.svg'; +import { REPORT_TYPES } from '@/lib/constants'; +import { parseDateRange } from '@/lib/date'; import { endOfMonth, startOfMonth } from 'date-fns'; const defaultParameters = { diff --git a/src/app/(main)/reports/retention/RetentionTable.tsx b/src/app/(main)/reports/retention/RetentionTable.tsx index 6d825567..23f0a8b0 100644 --- a/src/app/(main)/reports/retention/RetentionTable.tsx +++ b/src/app/(main)/reports/retention/RetentionTable.tsx @@ -1,9 +1,9 @@ import { useContext } from 'react'; import classNames from 'classnames'; import { ReportContext } from '../[reportId]/Report'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { useMessages, useLocale } from 'components/hooks'; -import { formatDate } from 'lib/date'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { useMessages, useLocale } from '@/components/hooks'; +import { formatDate } from '@/lib/date'; import styles from './RetentionTable.module.css'; const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28]; diff --git a/src/app/(main)/reports/revenue/RevenueParameters.tsx b/src/app/(main)/reports/revenue/RevenueParameters.tsx index 8e344e4d..5cee14de 100644 --- a/src/app/(main)/reports/revenue/RevenueParameters.tsx +++ b/src/app/(main)/reports/revenue/RevenueParameters.tsx @@ -1,5 +1,5 @@ -import { useMessages } from 'components/hooks'; -import useRevenueValues from 'components/hooks/queries/useRevenueValues'; +import { useMessages } from '@/components/hooks'; +import useRevenueValues from '@/components/hooks/queries/useRevenueValues'; import { useContext } from 'react'; import { Dropdown, Form, FormButtons, FormInput, FormRow, Item, SubmitButton } from 'react-basics'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/revenue/RevenueReport.tsx b/src/app/(main)/reports/revenue/RevenueReport.tsx index 7b75ebd2..8400c651 100644 --- a/src/app/(main)/reports/revenue/RevenueReport.tsx +++ b/src/app/(main)/reports/revenue/RevenueReport.tsx @@ -1,5 +1,5 @@ -import Money from 'assets/money.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Money from '@/assets/money.svg'; +import { REPORT_TYPES } from '@/lib/constants'; import Report from '../[reportId]/Report'; import ReportBody from '../[reportId]/ReportBody'; import ReportHeader from '../[reportId]/ReportHeader'; diff --git a/src/app/(main)/reports/revenue/RevenueTable.tsx b/src/app/(main)/reports/revenue/RevenueTable.tsx index 0b9fcdc3..184797e9 100644 --- a/src/app/(main)/reports/revenue/RevenueTable.tsx +++ b/src/app/(main)/reports/revenue/RevenueTable.tsx @@ -1,9 +1,9 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { useMessages } from 'components/hooks'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { useMessages } from '@/components/hooks'; import { useContext } from 'react'; import { GridColumn, GridTable } from 'react-basics'; import { ReportContext } from '../[reportId]/Report'; -import { formatLongCurrency } from 'lib/format'; +import { formatLongCurrency } from '@/lib/format'; export function RevenueTable() { const { report } = useContext(ReportContext); diff --git a/src/app/(main)/reports/revenue/RevenueView.tsx b/src/app/(main)/reports/revenue/RevenueView.tsx index 2d559893..bd3d6c63 100644 --- a/src/app/(main)/reports/revenue/RevenueView.tsx +++ b/src/app/(main)/reports/revenue/RevenueView.tsx @@ -1,16 +1,16 @@ import classNames from 'classnames'; import { colord } from 'colord'; -import BarChart from 'components/charts/BarChart'; -import PieChart from 'components/charts/PieChart'; -import TypeIcon from 'components/common/TypeIcon'; -import { useCountryNames, useLocale, useMessages } from 'components/hooks'; -import { GridRow } from 'components/layout/Grid'; -import ListTable from 'components/metrics/ListTable'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { renderDateLabels } from 'lib/charts'; -import { CHART_COLORS } from 'lib/constants'; -import { formatLongCurrency, formatLongNumber } from 'lib/format'; +import BarChart from '@/components/charts/BarChart'; +import PieChart from '@/components/charts/PieChart'; +import TypeIcon from '@/components/common/TypeIcon'; +import { useCountryNames, useLocale, useMessages } from '@/components/hooks'; +import { GridRow } from '@/components/layout/Grid'; +import ListTable from '@/components/metrics/ListTable'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { renderDateLabels } from '@/lib/charts'; +import { CHART_COLORS } from '@/lib/constants'; +import { formatLongCurrency, formatLongNumber } from '@/lib/format'; import { useCallback, useContext, useMemo } from 'react'; import { ReportContext } from '../[reportId]/Report'; import RevenueTable from './RevenueTable'; diff --git a/src/app/(main)/reports/utm/UTMParameters.tsx b/src/app/(main)/reports/utm/UTMParameters.tsx index c76df77d..5ae6017f 100644 --- a/src/app/(main)/reports/utm/UTMParameters.tsx +++ b/src/app/(main)/reports/utm/UTMParameters.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Form, FormButtons, SubmitButton } from 'react-basics'; import { ReportContext } from '../[reportId]/Report'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/utm/UTMReport.tsx b/src/app/(main)/reports/utm/UTMReport.tsx index 7183b9f7..d9d2f579 100644 --- a/src/app/(main)/reports/utm/UTMReport.tsx +++ b/src/app/(main)/reports/utm/UTMReport.tsx @@ -5,8 +5,8 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import UTMParameters from './UTMParameters'; import UTMView from './UTMView'; -import Tag from 'assets/tag.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Tag from '@/assets/tag.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.utm, diff --git a/src/app/(main)/reports/utm/UTMView.tsx b/src/app/(main)/reports/utm/UTMView.tsx index f10a68d8..ba025824 100644 --- a/src/app/(main)/reports/utm/UTMView.tsx +++ b/src/app/(main)/reports/utm/UTMView.tsx @@ -1,11 +1,11 @@ import { useContext } from 'react'; import { firstBy } from 'thenby'; import { ReportContext } from '../[reportId]/Report'; -import { CHART_COLORS, UTM_PARAMS } from 'lib/constants'; -import PieChart from 'components/charts/PieChart'; -import ListTable from 'components/metrics/ListTable'; +import { CHART_COLORS, UTM_PARAMS } from '@/lib/constants'; +import PieChart from '@/components/charts/PieChart'; +import ListTable from '@/components/metrics/ListTable'; import styles from './UTMView.module.css'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; function toArray(data: { [key: string]: number } = {}) { return Object.keys(data) diff --git a/src/app/(main)/settings/SettingsLayout.tsx b/src/app/(main)/settings/SettingsLayout.tsx index 28e9a074..08dcc3eb 100644 --- a/src/app/(main)/settings/SettingsLayout.tsx +++ b/src/app/(main)/settings/SettingsLayout.tsx @@ -1,7 +1,7 @@ 'use client'; import { ReactNode } from 'react'; -import { useLogin, useMessages } from 'components/hooks'; -import MenuLayout from 'components/layout/MenuLayout'; +import { useLogin, useMessages } from '@/components/hooks'; +import MenuLayout from '@/components/layout/MenuLayout'; export default function SettingsLayout({ children }: { children: ReactNode }) { const { user } = useLogin(); diff --git a/src/app/(main)/settings/teams/TeamAddForm.tsx b/src/app/(main)/settings/teams/TeamAddForm.tsx index c0fc7513..e940aa17 100644 --- a/src/app/(main)/settings/teams/TeamAddForm.tsx +++ b/src/app/(main)/settings/teams/TeamAddForm.tsx @@ -1,4 +1,4 @@ -import { useApi, useMessages } from 'components/hooks'; +import { useApi, useMessages } from '@/components/hooks'; import { Button, Form, diff --git a/src/app/(main)/settings/teams/TeamJoinForm.tsx b/src/app/(main)/settings/teams/TeamJoinForm.tsx index 939b5d4b..0a82260c 100644 --- a/src/app/(main)/settings/teams/TeamJoinForm.tsx +++ b/src/app/(main)/settings/teams/TeamJoinForm.tsx @@ -8,7 +8,7 @@ import { Button, SubmitButton, } from 'react-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; +import { useApi, useMessages, useModified } from '@/components/hooks'; export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { const { formatMessage, labels, getMessage } = useMessages(); diff --git a/src/app/(main)/settings/teams/TeamLeaveButton.tsx b/src/app/(main)/settings/teams/TeamLeaveButton.tsx index b8a24c7e..5f5b54f3 100644 --- a/src/app/(main)/settings/teams/TeamLeaveButton.tsx +++ b/src/app/(main)/settings/teams/TeamLeaveButton.tsx @@ -1,4 +1,4 @@ -import { useLocale, useLogin, useMessages, useModified } from 'components/hooks'; +import { useLocale, useLogin, useMessages, useModified } from '@/components/hooks'; import { useRouter } from 'next/navigation'; import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import TeamDeleteForm from './TeamLeaveForm'; diff --git a/src/app/(main)/settings/teams/TeamLeaveForm.tsx b/src/app/(main)/settings/teams/TeamLeaveForm.tsx index 8c9726be..daf46434 100644 --- a/src/app/(main)/settings/teams/TeamLeaveForm.tsx +++ b/src/app/(main)/settings/teams/TeamLeaveForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages, useModified } from 'components/hooks'; -import ConfirmationForm from 'components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; export function TeamLeaveForm({ teamId, @@ -14,7 +14,7 @@ export function TeamLeaveForm({ onSave: () => void; onClose: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const { del, useMutation } = useApi(); const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/teams/${teamId}/users/${userId}`), @@ -34,9 +34,7 @@ export function TeamLeaveForm({ return ( {teamName} }} /> - } + message={formatMessage(messages.confirmLeave, { target: {teamName} })} onConfirm={handleConfirm} onClose={onClose} isLoading={isPending} diff --git a/src/app/(main)/settings/teams/TeamsAddButton.tsx b/src/app/(main)/settings/teams/TeamsAddButton.tsx index 6ec4cf39..58c138a8 100644 --- a/src/app/(main)/settings/teams/TeamsAddButton.tsx +++ b/src/app/(main)/settings/teams/TeamsAddButton.tsx @@ -1,8 +1,8 @@ import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import Icons from 'components/icons'; -import { useMessages, useModified } from 'components/hooks'; +import Icons from '@/components/icons'; +import { useMessages, useModified } from '@/components/hooks'; import TeamAddForm from './TeamAddForm'; -import { messages } from 'components/messages'; +import { messages } from '@/components/messages'; export function TeamsAddButton({ onSave }: { onSave?: () => void }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/settings/teams/TeamsDataTable.tsx b/src/app/(main)/settings/teams/TeamsDataTable.tsx index e8563b2e..9b8c9b27 100644 --- a/src/app/(main)/settings/teams/TeamsDataTable.tsx +++ b/src/app/(main)/settings/teams/TeamsDataTable.tsx @@ -1,6 +1,6 @@ -import DataTable from 'components/common/DataTable'; -import TeamsTable from 'app/(main)/settings/teams/TeamsTable'; -import { useLogin, useTeams } from 'components/hooks'; +import DataTable from '@/components/common/DataTable'; +import TeamsTable from '@/app/(main)/settings/teams/TeamsTable'; +import { useLogin, useTeams } from '@/components/hooks'; import { ReactNode } from 'react'; export function TeamsDataTable({ diff --git a/src/app/(main)/settings/teams/TeamsHeader.tsx b/src/app/(main)/settings/teams/TeamsHeader.tsx index 8aa5e7a3..e1911a19 100644 --- a/src/app/(main)/settings/teams/TeamsHeader.tsx +++ b/src/app/(main)/settings/teams/TeamsHeader.tsx @@ -1,7 +1,7 @@ import { Flexbox } from 'react-basics'; -import PageHeader from 'components/layout/PageHeader'; -import { ROLES } from 'lib/constants'; -import { useLogin, useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { ROLES } from '@/lib/constants'; +import { useLogin, useMessages } from '@/components/hooks'; import TeamsJoinButton from './TeamsJoinButton'; import TeamsAddButton from './TeamsAddButton'; diff --git a/src/app/(main)/settings/teams/TeamsJoinButton.tsx b/src/app/(main)/settings/teams/TeamsJoinButton.tsx index 24925bb6..bbf2d685 100644 --- a/src/app/(main)/settings/teams/TeamsJoinButton.tsx +++ b/src/app/(main)/settings/teams/TeamsJoinButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import Icons from 'components/icons'; -import { useMessages, useModified } from 'components/hooks'; +import Icons from '@/components/icons'; +import { useMessages, useModified } from '@/components/hooks'; import TeamJoinForm from './TeamJoinForm'; export function TeamsJoinButton() { diff --git a/src/app/(main)/settings/teams/TeamsTable.tsx b/src/app/(main)/settings/teams/TeamsTable.tsx index a7a03958..8e7efa27 100644 --- a/src/app/(main)/settings/teams/TeamsTable.tsx +++ b/src/app/(main)/settings/teams/TeamsTable.tsx @@ -1,8 +1,8 @@ import { GridColumn, GridTable, Icon, Text } from 'react-basics'; -import { useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import { ROLES } from 'lib/constants'; -import LinkButton from 'components/common/LinkButton'; +import { useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { ROLES } from '@/lib/constants'; +import LinkButton from '@/components/common/LinkButton'; export function TeamsTable({ data = [], diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/settings/users/UserAddButton.tsx index 832cf75b..e1b04842 100644 --- a/src/app/(main)/settings/users/UserAddButton.tsx +++ b/src/app/(main)/settings/users/UserAddButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics'; import UserAddForm from './UserAddForm'; -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; export function UserAddButton({ onSave }: { onSave?: () => void }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/settings/users/UserAddForm.tsx b/src/app/(main)/settings/users/UserAddForm.tsx index 979f399f..13f2faf5 100644 --- a/src/app/(main)/settings/users/UserAddForm.tsx +++ b/src/app/(main)/settings/users/UserAddForm.tsx @@ -10,8 +10,8 @@ import { SubmitButton, Button, } from 'react-basics'; -import { useApi, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useApi, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; export function UserAddForm({ onSave, onClose }) { const { post, useMutation } = useApi(); diff --git a/src/app/(main)/settings/users/UserDeleteButton.tsx b/src/app/(main)/settings/users/UserDeleteButton.tsx index 9f1f8459..0909720e 100644 --- a/src/app/(main)/settings/users/UserDeleteButton.tsx +++ b/src/app/(main)/settings/users/UserDeleteButton.tsx @@ -1,5 +1,5 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { useMessages, useLogin } from 'components/hooks'; +import { useMessages, useLogin } from '@/components/hooks'; import UserDeleteForm from './UserDeleteForm'; export function UserDeleteButton({ diff --git a/src/app/(main)/settings/users/UserDeleteForm.tsx b/src/app/(main)/settings/users/UserDeleteForm.tsx index 9b49647f..3ac7c118 100644 --- a/src/app/(main)/settings/users/UserDeleteForm.tsx +++ b/src/app/(main)/settings/users/UserDeleteForm.tsx @@ -1,8 +1,8 @@ -import { useApi, useMessages, useModified } from 'components/hooks'; -import ConfirmationForm from 'components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; export function UserDeleteForm({ userId, username, onSave, onClose }) { - const { FormattedMessage, messages, labels, formatMessage } = useMessages(); + const { messages, labels, formatMessage } = useMessages(); const { del, useMutation } = useApi(); const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/users/${userId}`) }); const { touch } = useModified(); @@ -19,9 +19,7 @@ export function UserDeleteForm({ userId, username, onSave, onClose }) { return ( {username} }} /> - } + message={formatMessage(messages.confirmDelete, { target: {username} })} onConfirm={handleConfirm} onClose={onClose} buttonLabel={formatMessage(labels.delete)} diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/settings/users/UsersDataTable.tsx index 03addc74..867f4090 100644 --- a/src/app/(main)/settings/users/UsersDataTable.tsx +++ b/src/app/(main)/settings/users/UsersDataTable.tsx @@ -1,5 +1,5 @@ -import DataTable from 'components/common/DataTable'; -import { useUsers } from 'components/hooks'; +import DataTable from '@/components/common/DataTable'; +import { useUsers } from '@/components/hooks'; import UsersTable from './UsersTable'; import { ReactNode } from 'react'; diff --git a/src/app/(main)/settings/users/UsersHeader.tsx b/src/app/(main)/settings/users/UsersHeader.tsx index 6f4387c7..d07a159f 100644 --- a/src/app/(main)/settings/users/UsersHeader.tsx +++ b/src/app/(main)/settings/users/UsersHeader.tsx @@ -1,5 +1,5 @@ -import PageHeader from 'components/layout/PageHeader'; -import { useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; import UserAddButton from './UserAddButton'; export function UsersHeader({ onAdd }: { onAdd?: () => void }) { diff --git a/src/app/(main)/settings/users/UsersTable.tsx b/src/app/(main)/settings/users/UsersTable.tsx index e074be24..c698f38b 100644 --- a/src/app/(main)/settings/users/UsersTable.tsx +++ b/src/app/(main)/settings/users/UsersTable.tsx @@ -1,9 +1,9 @@ import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics'; import { formatDistance } from 'date-fns'; -import { ROLES } from 'lib/constants'; -import { useMessages, useLocale } from 'components/hooks'; +import { ROLES } from '@/lib/constants'; +import { useMessages, useLocale } from '@/components/hooks'; import UserDeleteButton from './UserDeleteButton'; -import LinkButton from 'components/common/LinkButton'; +import LinkButton from '@/components/common/LinkButton'; export function UsersTable({ data = [], diff --git a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx b/src/app/(main)/settings/users/[userId]/UserEditForm.tsx index 1acfc581..70f21f63 100644 --- a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx +++ b/src/app/(main)/settings/users/[userId]/UserEditForm.tsx @@ -9,8 +9,8 @@ import { SubmitButton, PasswordField, } from 'react-basics'; -import { useApi, useLogin, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useApi, useLogin, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import { useContext, useRef } from 'react'; import { UserContext } from './UserProvider'; diff --git a/src/app/(main)/settings/users/[userId]/UserProvider.tsx b/src/app/(main)/settings/users/[userId]/UserProvider.tsx index b289fca0..ed559c91 100644 --- a/src/app/(main)/settings/users/[userId]/UserProvider.tsx +++ b/src/app/(main)/settings/users/[userId]/UserProvider.tsx @@ -1,5 +1,5 @@ import { createContext, ReactNode, useEffect } from 'react'; -import { useModified, useUser } from 'components/hooks'; +import { useModified, useUser } from '@/components/hooks'; import { Loading } from 'react-basics'; export const UserContext = createContext(null); diff --git a/src/app/(main)/settings/users/[userId]/UserSettings.tsx b/src/app/(main)/settings/users/[userId]/UserSettings.tsx index f9e17a85..0d98205f 100644 --- a/src/app/(main)/settings/users/[userId]/UserSettings.tsx +++ b/src/app/(main)/settings/users/[userId]/UserSettings.tsx @@ -1,12 +1,12 @@ import { Key, useContext, useState } from 'react'; import { Item, Tabs, useToasts } from 'react-basics'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import UserEditForm from './UserEditForm'; -import PageHeader from 'components/layout/PageHeader'; -import { useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; import UserWebsites from './UserWebsites'; import { UserContext } from './UserProvider'; -import Breadcrumb from 'components/common/Breadcrumb'; +import Breadcrumb from '@/components/common/Breadcrumb'; export function UserSettings({ userId }: { userId: string }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx b/src/app/(main)/settings/users/[userId]/UserWebsites.tsx index bfc6f74b..15521b17 100644 --- a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx +++ b/src/app/(main)/settings/users/[userId]/UserWebsites.tsx @@ -1,6 +1,6 @@ -import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; -import DataTable from 'components/common/DataTable'; -import { useWebsites } from 'components/hooks'; +import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable'; +import DataTable from '@/components/common/DataTable'; +import { useWebsites } from '@/components/hooks'; export function UserWebsites({ userId }) { const queryResult = useWebsites({ userId }); diff --git a/src/app/(main)/settings/websites/WebsiteAddButton.tsx b/src/app/(main)/settings/websites/WebsiteAddButton.tsx index e534461c..6f32fc9f 100644 --- a/src/app/(main)/settings/websites/WebsiteAddButton.tsx +++ b/src/app/(main)/settings/websites/WebsiteAddButton.tsx @@ -1,4 +1,4 @@ -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import WebsiteAddForm from './WebsiteAddForm'; diff --git a/src/app/(main)/settings/websites/WebsiteAddForm.tsx b/src/app/(main)/settings/websites/WebsiteAddForm.tsx index 2bb4a0a8..90672412 100644 --- a/src/app/(main)/settings/websites/WebsiteAddForm.tsx +++ b/src/app/(main)/settings/websites/WebsiteAddForm.tsx @@ -7,9 +7,9 @@ import { Button, SubmitButton, } from 'react-basics'; -import { useApi } from 'components/hooks'; -import { DOMAIN_REGEX } from 'lib/constants'; -import { useMessages } from 'components/hooks'; +import { useApi } from '@/components/hooks'; +import { DOMAIN_REGEX } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; export function WebsiteAddForm({ teamId, diff --git a/src/app/(main)/settings/websites/WebsitesDataTable.tsx b/src/app/(main)/settings/websites/WebsitesDataTable.tsx index d91bbeef..023df857 100644 --- a/src/app/(main)/settings/websites/WebsitesDataTable.tsx +++ b/src/app/(main)/settings/websites/WebsitesDataTable.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; -import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; -import DataTable from 'components/common/DataTable'; -import { useWebsites } from 'components/hooks'; +import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable'; +import DataTable from '@/components/common/DataTable'; +import { useWebsites } from '@/components/hooks'; export function WebsitesDataTable({ teamId, diff --git a/src/app/(main)/settings/websites/WebsitesHeader.tsx b/src/app/(main)/settings/websites/WebsitesHeader.tsx index 6f322371..34e87a13 100644 --- a/src/app/(main)/settings/websites/WebsitesHeader.tsx +++ b/src/app/(main)/settings/websites/WebsitesHeader.tsx @@ -1,5 +1,5 @@ -import { useMessages } from 'components/hooks'; -import PageHeader from 'components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; import WebsiteAddButton from './WebsiteAddButton'; export interface WebsitesHeaderProps { diff --git a/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx index ff0938d1..61909a9e 100644 --- a/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx +++ b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useLogin } from 'components/hooks'; +import { useLogin } from '@/components/hooks'; import WebsitesDataTable from './WebsitesDataTable'; import WebsitesHeader from './WebsitesHeader'; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; export default function WebsitesSettingsPage({ teamId }: { teamId: string }) { const { user } = useLogin(); diff --git a/src/app/(main)/settings/websites/WebsitesTable.tsx b/src/app/(main)/settings/websites/WebsitesTable.tsx index 5e9aef2c..79749b97 100644 --- a/src/app/(main)/settings/websites/WebsitesTable.tsx +++ b/src/app/(main)/settings/websites/WebsitesTable.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics'; -import { useMessages, useTeamUrl } from 'components/hooks'; -import LinkButton from 'components/common/LinkButton'; +import { useMessages, useTeamUrl } from '@/components/hooks'; +import LinkButton from '@/components/common/LinkButton'; export interface WebsitesTableProps { data: any[]; diff --git a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx index e5673346..318e4e95 100644 --- a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx @@ -9,9 +9,9 @@ import { LoadingButton, } from 'react-basics'; import { useContext, useState } from 'react'; -import { getRandomChars } from 'next-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +import { getRandomChars } from '@/lib/crypto'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; const generateId = () => getRandomChars(16); @@ -35,7 +35,11 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => }; const handleCheck = (checked: boolean) => { - const data = { shareId: checked ? generateId() : null }; + const data = { + name: website.name, + domain: website.domain, + shareId: checked ? generateId() : null, + }; mutate(data, { onSuccess: async () => { touch(`website:${website.id}`); @@ -47,7 +51,7 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => const handleSave = () => { mutate( - { shareId: id }, + { name: website.name, domain: website.domain, shareId: id }, { onSuccess: async () => { touch(`website:${website.id}`); diff --git a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx b/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx index 95fb7068..cacdf689 100644 --- a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx @@ -1,5 +1,5 @@ import { TextArea } from 'react-basics'; -import { useMessages, useConfig } from 'components/hooks'; +import { useMessages, useConfig } from '@/components/hooks'; const SCRIPT_NAME = 'script.js'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx index bc6a3169..d11f24df 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx @@ -1,10 +1,10 @@ import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; import { useRouter } from 'next/navigation'; -import { useLogin, useMessages, useModified, useTeams, useTeamUrl } from 'components/hooks'; +import { useLogin, useMessages, useModified, useTeams, useTeamUrl } from '@/components/hooks'; import WebsiteDeleteForm from './WebsiteDeleteForm'; import WebsiteResetForm from './WebsiteResetForm'; import WebsiteTransferForm from './WebsiteTransferForm'; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx index 077a8f4a..5eef3544 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages } from 'components/hooks'; -import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; +import { useApi, useMessages } from '@/components/hooks'; +import TypeConfirmationForm from '@/components/common/TypeConfirmationForm'; const CONFIRM_VALUE = 'DELETE'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx index 15538661..aeef7f34 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx @@ -1,8 +1,8 @@ import { useContext, useRef } from 'react'; import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { DOMAIN_REGEX } from 'lib/constants'; -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { DOMAIN_REGEX } from '@/lib/constants'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { const website = useContext(WebsiteContext); diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx index c43f3efb..73886aa9 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages } from 'components/hooks'; -import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; +import { useApi, useMessages } from '@/components/hooks'; +import TypeConfirmationForm from '@/components/common/TypeConfirmationForm'; const CONFIRM_VALUE = 'RESET'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx index 11f662b1..5bea2704 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx @@ -1,8 +1,8 @@ -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; -import Breadcrumb from 'components/common/Breadcrumb'; -import { useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import PageHeader from 'components/layout/PageHeader'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import Breadcrumb from '@/components/common/Breadcrumb'; +import { useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import PageHeader from '@/components/layout/PageHeader'; import Link from 'next/link'; import { Key, useContext, useState } from 'react'; import { Button, Icon, Item, Tabs, Text, useToasts } from 'react-basics'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx index 00147629..8d7badb8 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx @@ -1,5 +1,5 @@ 'use client'; -import WebsiteProvider from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +import WebsiteProvider from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; import WebsiteSettings from './WebsiteSettings'; export default function WebsiteSettingsPage({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx index eb568a7f..8214fb16 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx @@ -10,9 +10,9 @@ import { Item, Flexbox, } from 'react-basics'; -import { useApi, useLogin, useMessages, useTeams } from 'components/hooks'; -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; -import { ROLES } from 'lib/constants'; +import { useApi, useLogin, useMessages, useTeams } from '@/components/hooks'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import { ROLES } from '@/lib/constants'; export function WebsiteTransferForm({ websiteId, @@ -71,7 +71,8 @@ export function WebsiteTransferForm({ {result.data .filter(({ teamUser }) => teamUser.find( - ({ role, userId }) => [ ROLES.teamOwner, ROLES.teamManager ].includes(role) && userId === user.id, + ({ role, userId }) => + [ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id, ), ) .map(({ id, name }) => { diff --git a/src/app/(main)/teams/[teamId]/TeamProvider.tsx b/src/app/(main)/teams/[teamId]/TeamProvider.tsx index 467a5d22..ed2d5467 100644 --- a/src/app/(main)/teams/[teamId]/TeamProvider.tsx +++ b/src/app/(main)/teams/[teamId]/TeamProvider.tsx @@ -1,6 +1,6 @@ 'use client'; import { createContext, ReactNode, useEffect } from 'react'; -import { useTeam, useModified } from 'components/hooks'; +import { useTeam, useModified } from '@/components/hooks'; import { Loading } from 'react-basics'; export const TeamContext = createContext(null); diff --git a/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx b/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx index f7df620a..8c638d29 100644 --- a/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx +++ b/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx @@ -1,7 +1,7 @@ 'use client'; import { ReactNode } from 'react'; -import { useMessages, useTeamUrl } from 'components/hooks'; -import MenuLayout from 'components/layout/MenuLayout'; +import { useMessages, useTeamUrl } from '@/components/hooks'; +import MenuLayout from '@/components/layout/MenuLayout'; export default function TeamSettingsLayout({ children }: { children: ReactNode }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx index 9cc99869..85292f60 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx @@ -1,4 +1,4 @@ -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import TeamMemberEditForm from './TeamMemberEditForm'; diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx index 40183989..4ce605df 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useApi, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import { Button, Dropdown, diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx index 3f024395..931390c7 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx @@ -1,8 +1,7 @@ -import ConfirmationForm from 'components/common/ConfirmationForm'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { messages } from 'components/messages'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { messages } from '@/components/messages'; import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { FormattedMessage } from 'react-intl'; export function TeamMemberRemoveButton({ teamId, @@ -44,12 +43,7 @@ export function TeamMemberRemoveButton({ {(close: () => void) => ( {userName} }} - /> - } + message={formatMessage(messages.confirmRemove, { target: {userName} })} isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx index 996283a7..9de26415 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx @@ -1,6 +1,6 @@ -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import TeamMembersTable from './TeamMembersTable'; -import { useTeamMembers } from 'components/hooks'; +import { useTeamMembers } from '@/components/hooks'; export function TeamMembersDataTable({ teamId, diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx index de0c4c0a..557a40ba 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx @@ -1,9 +1,9 @@ 'use client'; -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; import TeamMembersDataTable from './TeamMembersDataTable'; -import PageHeader from 'components/layout/PageHeader'; -import { useLogin, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import PageHeader from '@/components/layout/PageHeader'; +import { useLogin, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import { useContext } from 'react'; export function TeamMembersPage({ teamId }: { teamId: string }) { diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx index 67cb23c7..0054437a 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx @@ -1,6 +1,6 @@ import { GridColumn, GridTable } from 'react-basics'; -import { useMessages, useLogin } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useMessages, useLogin } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import TeamMemberRemoveButton from './TeamMemberRemoveButton'; import TeamMemberEditButton from './TeamMemberEditButton'; diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx index 3cbdf550..5e7f5cf8 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx @@ -1,5 +1,5 @@ -import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; -import { useApi, useMessages } from 'components/hooks'; +import TypeConfirmationForm from '@/components/common/TypeConfirmationForm'; +import { useApi, useMessages } from '@/components/hooks'; const CONFIRM_VALUE = 'DELETE'; diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx index 70858ee4..f3f258bd 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx @@ -1,11 +1,11 @@ -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; -import { useLogin, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import PageHeader from 'components/layout/PageHeader'; -import { ROLES } from 'lib/constants'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import { useLogin, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import PageHeader from '@/components/layout/PageHeader'; +import { ROLES } from '@/lib/constants'; import { useContext, useState } from 'react'; import { Flexbox, Item, Tabs } from 'react-basics'; -import TeamLeaveButton from 'app/(main)/settings/teams/TeamLeaveButton'; +import TeamLeaveButton from '@/app/(main)/settings/teams/TeamLeaveButton'; import TeamManage from './TeamManage'; import TeamEditForm from './TeamEditForm'; diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx index c2029ca6..ac158fa7 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx @@ -9,10 +9,10 @@ import { Flexbox, useToasts, } from 'react-basics'; -import { getRandomChars } from 'next-basics'; +import { getRandomChars } from '@/lib/crypto'; import { useContext, useRef, useState } from 'react'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; const generateId = () => getRandomChars(16); diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx index 40cbee04..24ca93d3 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx @@ -1,4 +1,4 @@ -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; import { useRouter } from 'next/navigation'; import { ActionForm, Button, Modal, ModalTrigger } from 'react-basics'; import TeamDeleteForm from './TeamDeleteForm'; diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx index 336e151a..fdd76cd2 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx @@ -1,4 +1,4 @@ -import { useApi, useMessages } from 'components/hooks'; +import { useApi, useMessages } from '@/components/hooks'; import { Icon, Icons, LoadingButton, Text } from 'react-basics'; export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx index 9e2985d4..63aa47f5 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx @@ -1,5 +1,5 @@ -import DataTable from 'components/common/DataTable'; -import { useTeamWebsites } from 'components/hooks'; +import DataTable from '@/components/common/DataTable'; +import { useTeamWebsites } from '@/components/hooks'; import TeamWebsitesTable from './TeamWebsitesTable'; export function TeamWebsitesDataTable({ diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx index 882ef8ec..d46d928a 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx @@ -1,10 +1,10 @@ 'use client'; -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; -import WebsiteAddButton from 'app/(main)/settings/websites/WebsiteAddButton'; -import { useLogin, useMessages } from 'components/hooks'; -import PageHeader from 'components/layout/PageHeader'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import WebsiteAddButton from '@/app/(main)/settings/websites/WebsiteAddButton'; +import { useLogin, useMessages } from '@/components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; import TeamWebsitesDataTable from './TeamWebsitesDataTable'; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; import { useContext } from 'react'; export function TeamWebsitesPage({ teamId }: { teamId: string }) { diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx index dc6760a6..76c343b1 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx @@ -1,7 +1,7 @@ import { GridColumn, GridTable, Icon, Text } from 'react-basics'; -import { useLogin, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import LinkButton from 'components/common/LinkButton'; +import { useLogin, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import LinkButton from '@/components/common/LinkButton'; export function TeamWebsitesTable({ teamId, diff --git a/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx b/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx index a6895296..a18f8a2e 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx @@ -1,4 +1,4 @@ -import Page from 'app/(main)/settings/websites/[websiteId]/page'; +import Page from '@/app/(main)/settings/websites/[websiteId]/page'; export default function ({ params }) { return ; diff --git a/src/app/(main)/websites/WebsitesPage.tsx b/src/app/(main)/websites/WebsitesPage.tsx index d6f8524b..b5e40b30 100644 --- a/src/app/(main)/websites/WebsitesPage.tsx +++ b/src/app/(main)/websites/WebsitesPage.tsx @@ -1,7 +1,7 @@ 'use client'; -import WebsitesHeader from 'app/(main)/settings/websites/WebsitesHeader'; -import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable'; -import { useTeamUrl } from 'components/hooks'; +import WebsitesHeader from '@/app/(main)/settings/websites/WebsitesHeader'; +import WebsitesDataTable from '@/app/(main)/settings/websites/WebsitesDataTable'; +import { useTeamUrl } from '@/components/hooks'; export default function WebsitesPage() { const { teamId } = useTeamUrl(); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index ddeba789..68192307 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import PageviewsChart from 'components/metrics/PageviewsChart'; -import useWebsitePageviews from 'components/hooks/queries/useWebsitePageviews'; -import { useDateRange } from 'components/hooks'; +import PageviewsChart from '@/components/metrics/PageviewsChart'; +import useWebsitePageviews from '@/components/hooks/queries/useWebsitePageviews'; +import { useDateRange } from '@/components/hooks'; export function WebsiteChart({ websiteId, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx index e33e948a..b27f9870 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx @@ -3,10 +3,10 @@ import { useMemo } from 'react'; import { firstBy } from 'thenby'; import Link from 'next/link'; import WebsiteChart from './WebsiteChart'; -import useDashboard from 'store/dashboard'; +import useDashboard from '@/store/dashboard'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; -import { useMessages, useLocale, useTeamUrl } from 'components/hooks'; +import { useMessages, useLocale, useTeamUrl } from '@/components/hooks'; export default function WebsiteChartList({ websites, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx index 3eeeb18f..460792ef 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx @@ -1,13 +1,13 @@ 'use client'; import { usePathname } from 'next/navigation'; -import FilterTags from 'components/metrics/FilterTags'; -import { useNavigation } from 'components/hooks'; +import FilterTags from '@/components/metrics/FilterTags'; +import { useNavigation } from '@/components/hooks'; import WebsiteChart from './WebsiteChart'; import WebsiteExpandedView from './WebsiteExpandedView'; import WebsiteHeader from './WebsiteHeader'; import WebsiteMetricsBar from './WebsiteMetricsBar'; import WebsiteTableView from './WebsiteTableView'; -import { FILTER_COLUMNS } from 'lib/constants'; +import { FILTER_COLUMNS } from '@/lib/constants'; export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) { const pathname = usePathname(); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx index 95e718b4..4858ec73 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx @@ -1,21 +1,22 @@ -import LinkButton from 'components/common/LinkButton'; -import { useLocale, useMessages, useNavigation } from 'components/hooks'; -import SideNav from 'components/layout/SideNav'; -import BrowsersTable from 'components/metrics/BrowsersTable'; -import CitiesTable from 'components/metrics/CitiesTable'; -import CountriesTable from 'components/metrics/CountriesTable'; -import DevicesTable from 'components/metrics/DevicesTable'; -import EventsTable from 'components/metrics/EventsTable'; -import HostsTable from 'components/metrics/HostsTable'; -import LanguagesTable from 'components/metrics/LanguagesTable'; -import OSTable from 'components/metrics/OSTable'; -import PagesTable from 'components/metrics/PagesTable'; -import QueryParametersTable from 'components/metrics/QueryParametersTable'; -import ReferrersTable from 'components/metrics/ReferrersTable'; -import RegionsTable from 'components/metrics/RegionsTable'; -import ScreenTable from 'components/metrics/ScreenTable'; -import TagsTable from 'components/metrics/TagsTable'; import { Dropdown, Icon, Icons, Item, Text } from 'react-basics'; +import LinkButton from '@/components/common/LinkButton'; +import { useLocale, useMessages, useNavigation } from '@/components/hooks'; +import SideNav from '@/components/layout/SideNav'; +import BrowsersTable from '@/components/metrics/BrowsersTable'; +import CitiesTable from '@/components/metrics/CitiesTable'; +import CountriesTable from '@/components/metrics/CountriesTable'; +import DevicesTable from '@/components/metrics/DevicesTable'; +import EventsTable from '@/components/metrics/EventsTable'; +import HostsTable from '@/components/metrics/HostsTable'; +import LanguagesTable from '@/components/metrics/LanguagesTable'; +import OSTable from '@/components/metrics/OSTable'; +import PagesTable from '@/components/metrics/PagesTable'; +import QueryParametersTable from '@/components/metrics/QueryParametersTable'; +import ReferrersTable from '@/components/metrics/ReferrersTable'; +import RegionsTable from '@/components/metrics/RegionsTable'; +import ScreenTable from '@/components/metrics/ScreenTable'; +import TagsTable from '@/components/metrics/TagsTable'; +import ChannelsTable from '@/components/metrics/ChannelsTable'; import styles from './WebsiteExpandedView.module.css'; const views = { @@ -24,6 +25,7 @@ const views = { exit: PagesTable, title: PagesTable, referrer: ReferrersTable, + grouped: ReferrersTable, host: HostsTable, browser: BrowsersTable, os: OSTable, @@ -36,6 +38,7 @@ const views = { event: EventsTable, query: QueryParametersTable, tag: TagsTable, + channel: ChannelsTable, }; export default function WebsiteExpandedView({ @@ -64,6 +67,11 @@ export default function WebsiteExpandedView({ label: formatMessage(labels.referrers), url: renderUrl({ view: 'referrer' }), }, + { + key: 'channel', + label: formatMessage(labels.channels), + url: renderUrl({ view: 'channel' }), + }, { key: 'browser', label: formatMessage(labels.browsers), diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index a6229e95..02b74418 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -1,8 +1,8 @@ import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics'; -import PopupForm from 'app/(main)/reports/[reportId]/PopupForm'; -import FilterSelectForm from 'app/(main)/reports/[reportId]/FilterSelectForm'; -import { useFields, useMessages, useNavigation, useDateRange } from 'components/hooks'; -import { OPERATOR_PREFIXES } from 'lib/constants'; +import PopupForm from '@/app/(main)/reports/[reportId]/PopupForm'; +import FilterSelectForm from '@/app/(main)/reports/[reportId]/FilterSelectForm'; +import { useFields, useMessages, useNavigation, useDateRange } from '@/components/hooks'; +import { OPERATOR_PREFIXES } from '@/lib/constants'; import styles from './WebsiteFilterButton.module.css'; export function WebsiteFilterButton({ diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index edd10b99..b568dd3d 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -1,13 +1,13 @@ import classNames from 'classnames'; -import Favicon from 'components/common/Favicon'; -import { useMessages, useTeamUrl, useWebsite } from 'components/hooks'; -import Icons from 'components/icons'; -import ActiveUsers from 'components/metrics/ActiveUsers'; +import Favicon from '@/components/common/Favicon'; +import { useMessages, useTeamUrl, useWebsite } from '@/components/hooks'; +import Icons from '@/components/icons'; +import ActiveUsers from '@/components/metrics/ActiveUsers'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { ReactNode } from 'react'; import { Button, Icon, Text } from 'react-basics'; -import Lightning from 'assets/lightning.svg'; +import Lightning from '@/assets/lightning.svg'; import styles from './WebsiteHeader.module.css'; export function WebsiteHeader({ diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index a6e7ad40..f206d3c9 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -1,14 +1,14 @@ -import classNames from 'classnames'; -import { useDateRange, useMessages, useSticky } from 'components/hooks'; -import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatShortTime, formatLongNumber } from 'lib/format'; -import WebsiteFilterButton from './WebsiteFilterButton'; -import useWebsiteStats from 'components/hooks/queries/useWebsiteStats'; -import styles from './WebsiteMetricsBar.module.css'; import { Dropdown, Item } from 'react-basics'; -import useStore, { setWebsiteDateCompare } from 'store/websites'; +import classNames from 'classnames'; +import { useDateRange, useMessages, useSticky } from '@/components/hooks'; +import WebsiteDateFilter from '@/components/input/WebsiteDateFilter'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatShortTime, formatLongNumber } from '@/lib/format'; +import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats'; +import useStore, { setWebsiteDateCompare } from '@/store/websites'; +import WebsiteFilterButton from './WebsiteFilterButton'; +import styles from './WebsiteMetricsBar.module.css'; export function WebsiteMetricsBar({ websiteId, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx b/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx index 3cdfdd5d..198ad030 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx @@ -1,6 +1,6 @@ 'use client'; import { createContext, ReactNode, useEffect } from 'react'; -import { useModified, useWebsite } from 'components/hooks'; +import { useModified, useWebsite } from '@/components/hooks'; import { Loading } from 'react-basics'; export const WebsiteContext = createContext(null); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx index 2782cac6..02422075 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx @@ -1,13 +1,13 @@ -import { Grid, GridRow } from 'components/layout/Grid'; -import PagesTable from 'components/metrics/PagesTable'; -import ReferrersTable from 'components/metrics/ReferrersTable'; -import BrowsersTable from 'components/metrics/BrowsersTable'; -import OSTable from 'components/metrics/OSTable'; -import DevicesTable from 'components/metrics/DevicesTable'; -import WorldMap from 'components/metrics/WorldMap'; -import CountriesTable from 'components/metrics/CountriesTable'; -import EventsTable from 'components/metrics/EventsTable'; -import EventsChart from 'components/metrics/EventsChart'; +import { Grid, GridRow } from '@/components/layout/Grid'; +import PagesTable from '@/components/metrics/PagesTable'; +import ReferrersTable from '@/components/metrics/ReferrersTable'; +import BrowsersTable from '@/components/metrics/BrowsersTable'; +import OSTable from '@/components/metrics/OSTable'; +import DevicesTable from '@/components/metrics/DevicesTable'; +import WorldMap from '@/components/metrics/WorldMap'; +import CountriesTable from '@/components/metrics/CountriesTable'; +import EventsTable from '@/components/metrics/EventsTable'; +import EventsChart from '@/components/metrics/EventsChart'; import { usePathname } from 'next/navigation'; export default function WebsiteTableView({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx index 21cd6597..10a2eed1 100644 --- a/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx @@ -1,9 +1,9 @@ 'use client'; import WebsiteHeader from '../WebsiteHeader'; import WebsiteMetricsBar from '../WebsiteMetricsBar'; -import FilterTags from 'components/metrics/FilterTags'; -import { useNavigation } from 'components/hooks'; -import { FILTER_COLUMNS } from 'lib/constants'; +import FilterTags from '@/components/metrics/FilterTags'; +import { useNavigation } from '@/components/hooks'; +import { FILTER_COLUMNS } from '@/lib/constants'; import WebsiteChart from '../WebsiteChart'; import WebsiteCompareTables from './WebsiteCompareTables'; diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx index af5a06d4..ce7f5b47 100644 --- a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx @@ -1,25 +1,25 @@ -import { useDateRange, useMessages, useNavigation } from 'components/hooks'; -import { Grid, GridRow } from 'components/layout/Grid'; -import SideNav from 'components/layout/SideNav'; -import BrowsersTable from 'components/metrics/BrowsersTable'; -import ChangeLabel from 'components/metrics/ChangeLabel'; -import CitiesTable from 'components/metrics/CitiesTable'; -import CountriesTable from 'components/metrics/CountriesTable'; -import DevicesTable from 'components/metrics/DevicesTable'; -import EventsTable from 'components/metrics/EventsTable'; -import LanguagesTable from 'components/metrics/LanguagesTable'; -import MetricsTable from 'components/metrics/MetricsTable'; -import OSTable from 'components/metrics/OSTable'; -import PagesTable from 'components/metrics/PagesTable'; -import QueryParametersTable from 'components/metrics/QueryParametersTable'; -import ReferrersTable from 'components/metrics/ReferrersTable'; -import RegionsTable from 'components/metrics/RegionsTable'; -import ScreenTable from 'components/metrics/ScreenTable'; -import TagsTable from 'components/metrics/TagsTable'; -import { getCompareDate } from 'lib/date'; -import { formatNumber } from 'lib/format'; +import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; +import { Grid, GridRow } from '@/components/layout/Grid'; +import SideNav from '@/components/layout/SideNav'; +import BrowsersTable from '@/components/metrics/BrowsersTable'; +import ChangeLabel from '@/components/metrics/ChangeLabel'; +import CitiesTable from '@/components/metrics/CitiesTable'; +import CountriesTable from '@/components/metrics/CountriesTable'; +import DevicesTable from '@/components/metrics/DevicesTable'; +import EventsTable from '@/components/metrics/EventsTable'; +import LanguagesTable from '@/components/metrics/LanguagesTable'; +import MetricsTable from '@/components/metrics/MetricsTable'; +import OSTable from '@/components/metrics/OSTable'; +import PagesTable from '@/components/metrics/PagesTable'; +import QueryParametersTable from '@/components/metrics/QueryParametersTable'; +import ReferrersTable from '@/components/metrics/ReferrersTable'; +import RegionsTable from '@/components/metrics/RegionsTable'; +import ScreenTable from '@/components/metrics/ScreenTable'; +import TagsTable from '@/components/metrics/TagsTable'; +import { getCompareDate } from '@/lib/date'; +import { formatNumber } from '@/lib/format'; import { useState } from 'react'; -import useStore from 'store/websites'; +import useStore from '@/store/websites'; import styles from './WebsiteCompareTables.module.css'; const views = { diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx index 760f34f9..453aa9a8 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx @@ -1,9 +1,9 @@ import { GridColumn, GridTable } from 'react-basics'; -import { useEventDataProperties, useEventDataValues, useMessages } from 'components/hooks'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import PieChart from 'components/charts/PieChart'; +import { useEventDataProperties, useEventDataValues, useMessages } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import PieChart from '@/components/charts/PieChart'; import { useState } from 'react'; -import { CHART_COLORS } from 'lib/constants'; +import { CHART_COLORS } from '@/lib/constants'; import styles from './EventProperties.module.css'; export function EventProperties({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx index 32eb985c..ce9048d3 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx @@ -1,6 +1,6 @@ -import { useWebsiteEvents } from 'components/hooks'; +import { useWebsiteEvents } from '@/components/hooks'; import EventsTable from './EventsTable'; -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import { ReactNode } from 'react'; export default function EventsDataTable({ diff --git a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx index d039b67f..e90a7790 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx @@ -1,9 +1,9 @@ -import { useMessages } from 'components/hooks'; -import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats'; -import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatLongNumber } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import useWebsiteSessionStats from '@/components/hooks/queries/useWebsiteSessionStats'; +import WebsiteDateFilter from '@/components/input/WebsiteDateFilter'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; import { Flexbox } from 'react-basics'; export function EventsMetricsBar({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 7dfc0394..cf4c19ef 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -2,10 +2,10 @@ import WebsiteHeader from '../WebsiteHeader'; import EventsDataTable from './EventsDataTable'; import EventsMetricsBar from './EventsMetricsBar'; -import EventsChart from 'components/metrics/EventsChart'; -import { GridRow } from 'components/layout/Grid'; -import MetricsTable from 'components/metrics/MetricsTable'; -import { useMessages } from 'components/hooks'; +import EventsChart from '@/components/metrics/EventsChart'; +import { GridRow } from '@/components/layout/Grid'; +import MetricsTable from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; import { Item, Tabs } from 'react-basics'; import { useState } from 'react'; import EventProperties from './EventProperties'; diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx index 42eb8f7a..8e6cdf76 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx @@ -1,9 +1,9 @@ import { GridTable, GridColumn, Icon } from 'react-basics'; -import { useMessages, useTeamUrl, useTimezone } from 'components/hooks'; -import Empty from 'components/common/Empty'; -import Avatar from 'components/common/Avatar'; +import { useMessages, useTeamUrl, useTimezone } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; +import Avatar from '@/components/common/Avatar'; import Link from 'next/link'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; export function EventsTable({ data = [] }) { const { formatTimezoneDate } = useTimezone(); diff --git a/src/app/(main)/websites/[websiteId]/layout.tsx b/src/app/(main)/websites/[websiteId]/layout.tsx index 1df69cd3..2542f65a 100644 --- a/src/app/(main)/websites/[websiteId]/layout.tsx +++ b/src/app/(main)/websites/[websiteId]/layout.tsx @@ -6,7 +6,7 @@ export default async function ({ params, }: { children: any; - params: { websiteId: string }; + params: Promise<{ websiteId: string }>; }) { const { websiteId } = await params; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx index 51663441..c3a3b8f7 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx @@ -1,9 +1,9 @@ import { useCallback } from 'react'; -import ListTable from 'components/metrics/ListTable'; -import { useLocale, useCountryNames, useMessages } from 'components/hooks'; +import ListTable from '@/components/metrics/ListTable'; +import { useLocale, useCountryNames, useMessages } from '@/components/hooks'; import classNames from 'classnames'; import styles from './RealtimeCountries.module.css'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function RealtimeCountries({ data }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx index c27143aa..6db56b76 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx @@ -1,6 +1,6 @@ -import MetricCard from 'components/metrics/MetricCard'; -import { useMessages } from 'components/hooks'; -import { RealtimeData } from 'lib/types'; +import MetricCard from '@/components/metrics/MetricCard'; +import { useMessages } from '@/components/hooks'; +import { RealtimeData } from '@/lib/types'; import styles from './RealtimeHeader.module.css'; export function RealtimeHeader({ data }: { data: RealtimeData }) { diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx index 0ed5fbde..104cf334 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx @@ -1,9 +1,9 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import { useApi, useMessages } from 'components/hooks'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import Page from '@/components/layout/Page'; +import PageHeader from '@/components/layout/PageHeader'; +import { useApi, useMessages } from '@/components/hooks'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; export function RealtimeHome() { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index f40be9db..21da2c54 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -1,12 +1,11 @@ -import useFormat from 'components//hooks/useFormat'; -import Empty from 'components/common/Empty'; -import FilterButtons from 'components/common/FilterButtons'; -import { useCountryNames, useLocale, useMessages, useTimezone } from 'components/hooks'; -import Icons from 'components/icons'; -import { BROWSERS, OS_NAMES } from 'lib/constants'; -import { stringToColor } from 'lib/format'; -import { RealtimeData } from 'lib/types'; -import { safeDecodeURI } from 'next-basics'; +import useFormat from '@/components//hooks/useFormat'; +import Empty from '@/components/common/Empty'; +import FilterButtons from '@/components/common/FilterButtons'; +import { useCountryNames, useLocale, useMessages, useTimezone } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { BROWSERS, OS_NAMES } from '@/lib/constants'; +import { stringToColor } from '@/lib/format'; +import { RealtimeData } from '@/lib/types'; import { useContext, useMemo, useState } from 'react'; import { Icon, SearchField, StatusLight, Text } from 'react-basics'; import { FixedSizeList } from 'react-window'; @@ -27,7 +26,7 @@ const icons = { export function RealtimeLog({ data }: { data: RealtimeData }) { const website = useContext(WebsiteContext); const [search, setSearch] = useState(''); - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const { formatValue } = useFormat(); const { locale } = useLocale(); const { formatTimezoneDate } = useTimezone(); @@ -71,24 +70,19 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { const { __type, eventName, urlPath: url, browser, os, country, device } = log; if (__type === TYPE_EVENT) { - return ( - {eventName || formatMessage(labels.unknown)}, - url: ( - - {url} - - ), - }} - /> - ); + return formatMessage(messages.eventLog, { + event: {eventName || formatMessage(labels.unknown)}, + url: ( + + {url} + + ), + }); } if (__type === TYPE_PAGEVIEW) { @@ -99,23 +93,18 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { target="_blank" rel="noreferrer noopener" > - {safeDecodeURI(url)} + {url} ); } if (__type === TYPE_SESSION) { - return ( - {countryNames[country] || formatMessage(labels.unknown)}, - browser: {BROWSERS[browser]}, - os: {OS_NAMES[os] || os}, - device: {formatMessage(labels[device] || labels.unknown)}, - }} - /> - ); + return formatMessage(messages.visitorLog, { + country: {countryNames[country] || formatMessage(labels.unknown)}, + browser: {BROWSERS[browser]}, + os: {OS_NAMES[os] || os}, + device: {formatMessage(labels[device] || labels.unknown)}, + }); } }; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx index 15b40f01..ce95bf41 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx @@ -1,11 +1,11 @@ import { Key, useContext, useState } from 'react'; import { ButtonGroup, Button, Flexbox } from 'react-basics'; import thenby from 'thenby'; -import { percentFilter } from 'lib/filters'; -import ListTable from 'components/metrics/ListTable'; -import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; -import { useMessages } from 'components/hooks'; -import { RealtimeData } from 'lib/types'; +import { percentFilter } from '@/lib/filters'; +import ListTable from '@/components/metrics/ListTable'; +import { FILTER_PAGES, FILTER_REFERRERS } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; +import { RealtimeData } from '@/lib/types'; import { WebsiteContext } from '../WebsiteProvider'; export function RealtimeUrls({ data }: { data: RealtimeData }) { diff --git a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx index 7030cc32..6edc28f9 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx @@ -1,16 +1,16 @@ 'use client'; import { firstBy } from 'thenby'; -import { Grid, GridRow } from 'components/layout/Grid'; -import Page from 'components/layout/Page'; -import RealtimeChart from 'components/metrics/RealtimeChart'; -import WorldMap from 'components/metrics/WorldMap'; -import { useRealtime } from 'components/hooks'; +import { Grid, GridRow } from '@/components/layout/Grid'; +import Page from '@/components/layout/Page'; +import RealtimeChart from '@/components/metrics/RealtimeChart'; +import WorldMap from '@/components/metrics/WorldMap'; +import { useRealtime } from '@/components/hooks'; import RealtimeLog from './RealtimeLog'; import RealtimeHeader from './RealtimeHeader'; import RealtimeUrls from './RealtimeUrls'; import RealtimeCountries from './RealtimeCountries'; import WebsiteHeader from '../WebsiteHeader'; -import { percentFilter } from 'lib/filters'; +import { percentFilter } from '@/lib/filters'; export function WebsiteRealtimePage({ websiteId }) { const { data, isLoading, error } = useRealtime(websiteId); diff --git a/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx b/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx index 051f6ed3..e61aacb1 100644 --- a/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx @@ -1,9 +1,9 @@ 'use client'; import Link from 'next/link'; import { Button, Flexbox, Icon, Icons, Text } from 'react-basics'; -import { useMessages, useTeamUrl } from 'components/hooks'; +import { useMessages, useTeamUrl } from '@/components/hooks'; import WebsiteHeader from '../WebsiteHeader'; -import ReportsDataTable from 'app/(main)/reports/ReportsDataTable'; +import ReportsDataTable from '@/app/(main)/reports/ReportsDataTable'; export function WebsiteReportsPage({ websiteId }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx index 49b63e74..a0b47bc9 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -1,9 +1,9 @@ import { GridColumn, GridTable } from 'react-basics'; -import { useSessionDataProperties, useSessionDataValues, useMessages } from 'components/hooks'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import PieChart from 'components/charts/PieChart'; +import { useSessionDataProperties, useSessionDataValues, useMessages } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import PieChart from '@/components/charts/PieChart'; import { useState } from 'react'; -import { CHART_COLORS } from 'lib/constants'; +import { CHART_COLORS } from '@/lib/constants'; import styles from './SessionProperties.module.css'; export function SessionProperties({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx index 788d0066..56e0df62 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx @@ -1,6 +1,6 @@ -import { useWebsiteSessions } from 'components/hooks'; +import { useWebsiteSessions } from '@/components/hooks'; import SessionsTable from './SessionsTable'; -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import { ReactNode } from 'react'; export default function SessionsDataTable({ diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx index 803e7a06..62d60de8 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx @@ -1,9 +1,9 @@ -import { useMessages } from 'components/hooks'; -import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats'; -import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatLongNumber } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import useWebsiteSessionStats from '@/components/hooks/queries/useWebsiteSessionStats'; +import WebsiteDateFilter from '@/components/input/WebsiteDateFilter'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; import { Flexbox } from 'react-basics'; export function SessionsMetricsBar({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx index 30fd193db..2ee044db 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx @@ -3,11 +3,11 @@ import WebsiteHeader from '../WebsiteHeader'; import SessionsDataTable from './SessionsDataTable'; import SessionsMetricsBar from './SessionsMetricsBar'; import SessionProperties from './SessionProperties'; -import WorldMap from 'components/metrics/WorldMap'; -import { GridRow } from 'components/layout/Grid'; +import WorldMap from '@/components/metrics/WorldMap'; +import { GridRow } from '@/components/layout/Grid'; import { Item, Tabs } from 'react-basics'; import { useState } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import SessionsWeekly from './SessionsWeekly'; export function SessionsPage({ websiteId }) { diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx index 3fea4836..ddb3ed65 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx @@ -1,9 +1,9 @@ import Link from 'next/link'; import { GridColumn, GridTable } from 'react-basics'; -import { useFormat, useMessages, useTimezone } from 'components/hooks'; -import Avatar from 'components/common/Avatar'; +import { useFormat, useMessages, useTimezone } from '@/components/hooks'; +import Avatar from '@/components/common/Avatar'; import styles from './SessionsTable.module.css'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) { const { formatTimezoneDate } = useTimezone(); diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx index 3e15ddfa..6082f0e2 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx @@ -1,7 +1,7 @@ import { format, startOfDay, addHours } from 'date-fns'; -import { useLocale, useMessages, useWebsiteSessionsWeekly } from 'components/hooks'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import { getDayOfWeekAsDate } from 'lib/date'; +import { useLocale, useMessages, useWebsiteSessionsWeekly } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { getDayOfWeekAsDate } from '@/lib/date'; import styles from './SessionsWeekly.module.css'; import classNames from 'classnames'; import { TooltipPopup } from 'react-basics'; @@ -54,10 +54,10 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
{format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
- {day?.map((hour: number) => { + {day?.map((hour: number, n) => { const pct = hour / max; return ( -
+
{hour > 0 && ( + {showHeader && (
{formatTimezoneDate(createdAt, 'EEEE, PPP')}
)} @@ -44,7 +45,7 @@ export function SessionActivity({ {eventName ? : }
{eventName || urlPath}
- + ); })}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx index 39b6afd1..56d4a0d9 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx @@ -1,9 +1,9 @@ import { TextOverflow } from 'react-basics'; -import { useMessages, useSessionData } from 'components/hooks'; -import Empty from 'components/common/Empty'; -import { DATA_TYPES } from 'lib/constants'; +import { useMessages, useSessionData } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; +import { DATA_TYPES } from '@/lib/constants'; import styles from './SessionData.module.css'; -import { LoadingPanel } from 'components/common/LoadingPanel'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx index d6a07edc..9ccf275f 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx @@ -1,7 +1,7 @@ 'use client'; -import Avatar from 'components/common/Avatar'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import { useWebsiteSession } from 'components/hooks'; +import Avatar from '@/components/common/Avatar'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useWebsiteSession } from '@/components/hooks'; import WebsiteHeader from '../../WebsiteHeader'; import { SessionActivity } from './SessionActivity'; import { SessionData } from './SessionData'; diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx index 6f9a8f3d..889eb972 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx @@ -1,7 +1,7 @@ -import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; import { Icon, CopyIcon } from 'react-basics'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import styles from './SessionInfo.module.css'; export default function SessionInfo({ data }) { diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx index ea606582..eb385e9b 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx @@ -1,7 +1,7 @@ -import { useMessages } from 'components/hooks'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatShortTime } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatShortTime } from '@/lib/format'; export function SessionStats({ data }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index bbc10a35..66884c2f 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -2,8 +2,8 @@ import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactBasicsProvider } from 'react-basics'; -import ErrorBoundary from 'components/common/ErrorBoundary'; -import { useLocale } from 'components/hooks'; +import ErrorBoundary from '@/components/common/ErrorBoundary'; +import { useLocale } from '@/components/hooks'; import 'chartjs-adapter-date-fns'; import { useEffect } from 'react'; diff --git a/src/app/actions/getConfig.ts b/src/app/actions/getConfig.ts new file mode 100644 index 00000000..bb892f01 --- /dev/null +++ b/src/app/actions/getConfig.ts @@ -0,0 +1,10 @@ +'use server'; + +export async function getConfig() { + return { + telemetryDisabled: !!process.env.DISABLE_TELEMETRY, + trackerScriptName: process.env.TRACKER_SCRIPT_NAME, + uiDisabled: !!process.env.DISABLE_UI, + updatesDisabled: !!process.env.DISABLE_UPDATES, + }; +} diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 00000000..2185e03e --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { pagingParams } from '@/lib/schema'; +import { canViewUsers } from '@/lib/auth'; +import { getUsers } from '@/queries/prisma/user'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewUsers(auth))) { + return unauthorized(); + } + + const users = await getUsers( + { + include: { + _count: { + select: { + websiteUser: { + where: { deletedAt: null }, + }, + }, + }, + }, + }, + query, + ); + + return json(users); +} diff --git a/src/app/api/admin/websites/route.ts b/src/app/api/admin/websites/route.ts new file mode 100644 index 00000000..3f35ea49 --- /dev/null +++ b/src/app/api/admin/websites/route.ts @@ -0,0 +1,90 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { pagingParams } from '@/lib/schema'; +import { canViewAllWebsites } from '@/lib/auth'; +import { getWebsites } from '@/queries/prisma/website'; +import { ROLES } from '@/lib/constants'; + +export async function GET(request: Request) { + const schema = z.object({ + userId: z.string().uuid(), + includeOwnedTeams: z.string().optional(), + includeAllTeams: z.string().optional(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewAllWebsites(auth))) { + return unauthorized(); + } + + const { userId, includeOwnedTeams, includeAllTeams } = query; + + const websites = await getWebsites( + { + where: { + OR: [ + ...(userId && [{ userId }]), + ...(userId && includeOwnedTeams + ? [ + { + team: { + deletedAt: null, + teamUser: { + some: { + role: ROLES.teamOwner, + userId, + }, + }, + }, + }, + ] + : []), + ...(userId && includeAllTeams + ? [ + { + team: { + deletedAt: null, + teamUser: { + some: { + userId, + }, + }, + }, + }, + ] + : []), + ], + }, + include: { + user: { + select: { + username: true, + id: true, + }, + }, + team: { + where: { + deletedAt: null, + }, + include: { + teamUser: { + where: { + role: ROLES.teamOwner, + }, + }, + }, + }, + }, + }, + query, + ); + + return json(websites); +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 00000000..0b48fe83 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import { checkPassword } from '@/lib/auth'; +import { createSecureToken } from '@/lib/jwt'; +import { redisEnabled } from '@umami/redis-client'; +import { getUserByUsername } from '@/queries'; +import { json, unauthorized } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { saveAuth } from '@/lib/auth'; +import { secret } from '@/lib/crypto'; +import { ROLES } from '@/lib/constants'; + +export async function POST(request: Request) { + const schema = z.object({ + username: z.string(), + password: z.string(), + }); + + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const { username, password } = body; + + const user = await getUserByUsername(username, { includePassword: true }); + + if (!user || !checkPassword(password, user.password)) { + return unauthorized(); + } + + if (redisEnabled) { + const token = await saveAuth({ userId: user.id }); + + return json({ token, user }); + } + + const token = createSecureToken({ userId: user.id }, secret()); + const { id, role, createdAt } = user; + + return json({ + token, + user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, + }); +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 00000000..22bb3091 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,14 @@ +import { getClient, redisEnabled } from '@umami/redis-client'; +import { ok } from '@/lib/response'; + +export async function POST(request: Request) { + if (redisEnabled) { + const redis = getClient(); + + const token = request.headers.get('authorization')?.split(' ')?.[1]; + + await redis.del(token); + } + + return ok(); +} diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts new file mode 100644 index 00000000..4a713424 --- /dev/null +++ b/src/app/api/auth/sso/route.ts @@ -0,0 +1,18 @@ +import { redisEnabled } from '@umami/redis-client'; +import { json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { saveAuth } from '@/lib/auth'; + +export async function POST(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + if (redisEnabled) { + const token = await saveAuth({ userId: auth.user.id }, 86400); + + return json({ user: auth.user, token }); + } +} diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts new file mode 100644 index 00000000..4d98b554 --- /dev/null +++ b/src/app/api/auth/verify/route.ts @@ -0,0 +1,12 @@ +import { parseRequest } from '@/lib/request'; +import { json } from '@/lib/response'; + +export async function GET(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + return json(auth.user); +} diff --git a/src/app/api/heartbeat/route.ts b/src/app/api/heartbeat/route.ts new file mode 100644 index 00000000..91463089 --- /dev/null +++ b/src/app/api/heartbeat/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return Response.json({ ok: true }); +} diff --git a/src/app/api/me/password/route.ts b/src/app/api/me/password/route.ts new file mode 100644 index 00000000..69bef49b --- /dev/null +++ b/src/app/api/me/password/route.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { checkPassword, hashPassword } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { json, badRequest } from '@/lib/response'; +import { getUser, updateUser } from '@/queries/prisma/user'; + +export async function POST(request: Request) { + const schema = z.object({ + currentPassword: z.string(), + newPassword: z.string().min(8), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const userId = auth.user.id; + const { currentPassword, newPassword } = body; + + const user = await getUser(userId, { includePassword: true }); + + if (!checkPassword(currentPassword, user.password)) { + return badRequest('Current password is incorrect'); + } + + const password = hashPassword(newPassword); + + const updated = await updateUser(userId, { password }); + + return json(updated); +} diff --git a/src/app/api/me/route.ts b/src/app/api/me/route.ts new file mode 100644 index 00000000..59a32552 --- /dev/null +++ b/src/app/api/me/route.ts @@ -0,0 +1,12 @@ +import { parseRequest } from '@/lib/request'; +import { json } from '@/lib/response'; + +export async function GET(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + return json(auth); +} diff --git a/src/app/api/me/teams/route.ts b/src/app/api/me/teams/route.ts new file mode 100644 index 00000000..2ea6575e --- /dev/null +++ b/src/app/api/me/teams/route.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { pagingParams } from '@/lib/schema'; +import { getUserTeams } from '@/queries'; +import { json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const teams = await getUserTeams(auth.user.id, query); + + return json(teams); +} diff --git a/src/app/api/me/websites/route.ts b/src/app/api/me/websites/route.ts new file mode 100644 index 00000000..a8df856a --- /dev/null +++ b/src/app/api/me/websites/route.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { pagingParams } from '@/lib/schema'; +import { getUserWebsites } from '@/queries'; +import { json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const websites = await getUserWebsites(auth.user.id, query); + + return json(websites); +} diff --git a/src/app/api/realtime/[websiteId]/route.ts b/src/app/api/realtime/[websiteId]/route.ts new file mode 100644 index 00000000..7f9c1a9a --- /dev/null +++ b/src/app/api/realtime/[websiteId]/route.ts @@ -0,0 +1,30 @@ +import { json, unauthorized } from '@/lib/response'; +import { getRealtimeData } from '@/queries'; +import { canViewWebsite } from '@/lib/auth'; +import { startOfMinute, subMinutes } from 'date-fns'; +import { REALTIME_RANGE } from '@/lib/constants'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, query, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { timezone } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + + const data = await getRealtimeData(websiteId, { startDate, timezone }); + + return json(data); +} diff --git a/src/app/api/reports/[reportId]/route.ts b/src/app/api/reports/[reportId]/route.ts new file mode 100644 index 00000000..ba90ee08 --- /dev/null +++ b/src/app/api/reports/[reportId]/route.ts @@ -0,0 +1,91 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { deleteReport, getReport, updateReport } from '@/queries'; +import { canDeleteReport, canUpdateReport, canViewReport } from '@/lib/auth'; +import { unauthorized, json, notFound, ok } from '@/lib/response'; +import { reportTypeParam } from '@/lib/schema'; + +export async function GET(request: Request, { params }: { params: Promise<{ reportId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { reportId } = await params; + + const report = await getReport(reportId); + + if (!(await canViewReport(auth, report))) { + return unauthorized(); + } + + report.parameters = JSON.parse(report.parameters); + + return json(report); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ reportId: string }> }, +) { + const schema = z.object({ + websiteId: z.string().uuid(), + type: reportTypeParam, + name: z.string().max(200), + description: z.string().max(500), + parameters: z.object({}).passthrough(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { reportId } = await params; + const { websiteId, type, name, description, parameters } = body; + + const report = await getReport(reportId); + + if (!report) { + return notFound(); + } + + if (!(await canUpdateReport(auth, report))) { + return unauthorized(); + } + + const result = await updateReport(reportId, { + websiteId, + userId: auth.user.id, + type, + name, + description, + parameters: JSON.stringify(parameters), + } as any); + + return json(result); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ reportId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { reportId } = await params; + const report = await getReport(reportId); + + if (!(await canDeleteReport(auth, report))) { + return unauthorized(); + } + + await deleteReport(reportId); + + return ok(); +} diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts new file mode 100644 index 00000000..471ae709 --- /dev/null +++ b/src/app/api/reports/funnel/route.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getFunnel } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + window: z.number().positive(), + steps: z + .array( + z.object({ + type: z.string(), + value: z.string(), + }), + ) + .min(2), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + steps, + window, + dateRange: { startDate, endDate }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getFunnel(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + steps, + windowMinutes: +window, + }); + + return json(data); +} diff --git a/src/app/api/reports/goals/route.ts b/src/app/api/reports/goals/route.ts new file mode 100644 index 00000000..5a2f6bd0 --- /dev/null +++ b/src/app/api/reports/goals/route.ts @@ -0,0 +1,57 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getGoals } from '@/queries/sql/reports/getGoals'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + goals: z + .array( + z + .object({ + type: z.string().regex(/url|event|event-data/), + value: z.string(), + goal: z.coerce.number(), + operator: z + .string() + .regex(/count|sum|average/) + .optional(), + property: z.string().optional(), + }) + .refine(data => { + if (data['type'] === 'event-data') { + return data['operator'] && data['property']; + } + return true; + }), + ) + .min(1), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + goals, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getGoals(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + goals, + }); + + return json(data); +} diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts new file mode 100644 index 00000000..b3569cba --- /dev/null +++ b/src/app/api/reports/insights/route.ts @@ -0,0 +1,62 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getInsights } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +function convertFilters(filters: any[]) { + return filters.reduce((obj, filter) => { + obj[filter.name] = filter; + + return obj; + }, {}); +} + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + fields: z + .array( + z.object({ + name: z.string(), + type: z.string(), + label: z.string(), + }), + ) + .min(1), + filters: z.array( + z.object({ + name: z.string(), + type: z.string(), + operator: z.string(), + value: z.string(), + }), + ), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + fields, + filters, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getInsights(websiteId, fields, { + ...convertFilters(filters), + startDate: new Date(startDate), + endDate: new Date(endDate), + }); + + return json(data); +} diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts new file mode 100644 index 00000000..b8a0a0a4 --- /dev/null +++ b/src/app/api/reports/journey/route.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getJourney } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + steps: z.number().min(3).max(7), + startStep: z.string(), + endStep: z.string(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + steps, + startStep, + endStep, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getJourney(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + steps, + startStep, + endStep, + }); + + return json(data); +} diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts new file mode 100644 index 00000000..83220bb4 --- /dev/null +++ b/src/app/api/reports/retention/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getRetention } from '@/queries'; +import { reportParms, timezoneParam } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + timezone: timezoneParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + timezone, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRetention(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + timezone, + }); + + return json(data); +} diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts new file mode 100644 index 00000000..f8f4041f --- /dev/null +++ b/src/app/api/reports/revenue/route.ts @@ -0,0 +1,62 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { reportParms, timezoneParam } from '@/lib/schema'; +import { getRevenue } from '@/queries/sql/reports/getRevenue'; +import { getRevenueValues } from '@/queries/sql/reports/getRevenueValues'; + +export async function GET(request: Request) { + const { auth, query, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, startDate, endDate } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRevenueValues(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + }); + + return json(data); +} + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + timezone: timezoneParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + currency, + timezone, + dateRange: { startDate, endDate, unit }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRevenue(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + unit, + timezone, + currency, + }); + + return json(data); +} diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts new file mode 100644 index 00000000..e50c57bc --- /dev/null +++ b/src/app/api/reports/route.ts @@ -0,0 +1,110 @@ +import { z } from 'zod'; +import { uuid } from '@/lib/crypto'; +import { pagingParams, reportTypeParam } from '@/lib/schema'; +import { parseRequest } from '@/lib/request'; +import { canViewTeam, canViewWebsite, canUpdateWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { getReports, createReport } from '@/queries'; + +export async function GET(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { page, search, pageSize, websiteId, teamId } = query; + const userId = auth.user.id; + const filters = { + page, + pageSize, + search, + }; + + if ( + (websiteId && !(await canViewWebsite(auth, websiteId))) || + (teamId && !(await canViewTeam(auth, teamId))) + ) { + return unauthorized(); + } + + const data = await getReports( + { + where: { + OR: [ + ...(websiteId ? [{ websiteId }] : []), + ...(teamId + ? [ + { + website: { + deletedAt: null, + teamId, + }, + }, + ] + : []), + ...(userId && !websiteId && !teamId + ? [ + { + website: { + deletedAt: null, + userId, + }, + }, + ] + : []), + ], + }, + include: { + website: { + select: { + domain: true, + }, + }, + }, + }, + filters, + ); + + return json(data); +} + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + name: z.string().max(200), + type: reportTypeParam, + description: z.string().max(500), + parameters: z.object({}).passthrough(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId, type, name, description, parameters } = body; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await createReport({ + id: uuid(), + userId: auth.user.id, + websiteId, + type, + name, + description, + parameters: JSON.stringify(parameters), + } as any); + + return json(result); +} diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts new file mode 100644 index 00000000..38e88a6d --- /dev/null +++ b/src/app/api/reports/utm/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getUTM } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate, timezone }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getUTM(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + timezone, + }); + + return json(data); +} diff --git a/src/app/api/scripts/telemetry/route.ts b/src/app/api/scripts/telemetry/route.ts index ecd83fcb..54cee565 100644 --- a/src/app/api/scripts/telemetry/route.ts +++ b/src/app/api/scripts/telemetry/route.ts @@ -1,4 +1,4 @@ -import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants'; +import { CURRENT_VERSION, TELEMETRY_PIXEL } from '@/lib/constants'; export async function GET() { if ( diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts new file mode 100644 index 00000000..415b5a8e --- /dev/null +++ b/src/app/api/send/route.ts @@ -0,0 +1,198 @@ +import { z } from 'zod'; +import { isbot } from 'isbot'; +import { createToken, parseToken } from '@/lib/jwt'; +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 { COLLECTION_TYPE } from '@/lib/constants'; +import { createSession, saveEvent, saveSessionData } from '@/queries'; + +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', 'identify']), + payload: z.object({ + website: z.string().uuid(), + data: z.object({}).passthrough().optional(), + hostname: z.string().max(100).optional(), + language: z.string().max(35).optional(), + referrer: z.string().optional(), + screen: z.string().max(11).optional(), + title: z.string().optional(), + url: z.string().optional(), + name: z.string().max(50).optional(), + tag: z.string().max(50).optional(), + ip: z.string().ip().optional(), + userAgent: z.string().optional(), + }), + }); + + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const { type, payload } = body; + + const { + website: websiteId, + hostname, + screen, + language, + url, + referrer, + name, + data, + title, + tag, + } = payload; + + // Cache check + let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null; + const cacheHeader = request.headers.get('x-umami-cache'); + + if (cacheHeader) { + const result = await parseToken(cacheHeader, secret()); + + if (result) { + cache = result; + } + } + + // Find website + if (!cache?.websiteId) { + const website = await fetchWebsite(websiteId); + + if (!website) { + return badRequest('Website not found.'); + } + } + + // Client info + const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } = + await getClientInfo(request, payload); + + // IP block + if (hasBlockedIp(ip)) { + return forbidden(); + } + + const sessionId = uuid(websiteId, hostname, ip, userAgent); + + // Find session + if (!cache?.sessionId) { + const session = await fetchSession(websiteId, sessionId); + + // Create a session if not found + if (!session && !clickhouse.enabled) { + try { + await createSession({ + id: sessionId, + websiteId, + hostname, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + }); + } catch (e: any) { + if (!e.message.toLowerCase().includes('unique constraint')) { + return serverError(e); + } + } + } + } + + // Visit info + const now = Math.floor(new Date().getTime() / 1000); + let visitId = cache?.visitId || uuid(sessionId, visitSalt()); + let iat = cache?.iat || now; + + // Expire visit after 30 minutes + if (now - iat > 1800) { + visitId = uuid(sessionId, visitSalt()); + iat = now; + } + + if (type === COLLECTION_TYPE.event) { + const base = hostname ? `http://${hostname}` : 'http://localhost'; + const currentUrl = new URL(url, base); + + let urlPath = currentUrl.pathname; + const urlQuery = currentUrl.search.substring(1); + const urlDomain = currentUrl.hostname.replace(/^www\./, ''); + + if (process.env.REMOVE_TRAILING_SLASH) { + urlPath = urlPath.replace(/(.+)\/$/, '$1'); + } + + let referrerPath: string; + let referrerQuery: string; + let referrerDomain: string; + + if (referrer) { + const referrerUrl = new URL(referrer, base); + + referrerPath = referrerUrl.pathname; + referrerQuery = referrerUrl.search.substring(1); + + if (referrerUrl.hostname !== 'localhost') { + referrerDomain = referrerUrl.hostname.replace(/^www\./, ''); + } + } + + await saveEvent({ + websiteId, + sessionId, + visitId, + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle: title, + eventName: name, + eventData: data, + hostname: hostname || urlDomain, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + tag, + }); + } + + if (type === COLLECTION_TYPE.identify) { + if (!data) { + return badRequest('Data required.'); + } + + await saveSessionData({ + websiteId, + sessionId, + sessionData: data, + }); + } + + const token = createToken({ websiteId, sessionId, visitId, iat }, secret()); + + return json({ cache: token, websiteId, sessionId, visitId, iat }); +} diff --git a/src/app/api/share/[shareId]/route.ts b/src/app/api/share/[shareId]/route.ts new file mode 100644 index 00000000..e387938d --- /dev/null +++ b/src/app/api/share/[shareId]/route.ts @@ -0,0 +1,19 @@ +import { json, notFound } from '@/lib/response'; +import { createToken } from '@/lib/jwt'; +import { secret } from '@/lib/crypto'; +import { getSharedWebsite } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) { + const { shareId } = await params; + + const website = await getSharedWebsite(shareId); + + if (!website) { + return notFound(); + } + + const data = { websiteId: website.id }; + const token = createToken(data, secret()); + + return json({ ...data, token }); +} diff --git a/src/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts new file mode 100644 index 00000000..f7f4b331 --- /dev/null +++ b/src/app/api/teams/[teamId]/route.ts @@ -0,0 +1,71 @@ +import { z } from 'zod'; +import { unauthorized, json, notFound, ok } from '@/lib/response'; +import { canDeleteTeam, canUpdateTeam, canViewTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { deleteTeam, getTeam, updateTeam } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canViewTeam(auth, teamId))) { + return unauthorized(); + } + + const team = await getTeam(teamId, { includeMembers: true }); + + if (!team) { + return notFound('Team not found.'); + } + + return json(team); +} + +export async function POST(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + name: z.string().max(50), + accessCode: z.string().max(50), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const team = await updateTeam(teamId, body); + + return json(team); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ teamId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canDeleteTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + await deleteTeam(teamId); + + return ok(); +} diff --git a/src/app/api/teams/[teamId]/users/[userId]/route.ts b/src/app/api/teams/[teamId]/users/[userId]/route.ts new file mode 100644 index 00000000..cadcd8b0 --- /dev/null +++ b/src/app/api/teams/[teamId]/users/[userId]/route.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest, ok } from '@/lib/response'; +import { canDeleteTeam, canUpdateTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { deleteTeam, getTeamUser, updateTeamUser } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId, userId } = await params; + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const teamUser = await getTeamUser(teamId, userId); + + return json(teamUser); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const schema = z.object({ + role: z.string().regex(/team-member|team-view-only|team-manager/), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId, userId } = await params; + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const teamUser = await getTeamUser(teamId, userId); + + if (!teamUser) { + return badRequest('The User does not exists on this team.'); + } + + const user = await updateTeamUser(teamUser.id, body); + + return json(user); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ teamId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canDeleteTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + await deleteTeam(teamId); + + return ok(); +} diff --git a/src/app/api/teams/[teamId]/users/route.ts b/src/app/api/teams/[teamId]/users/route.ts new file mode 100644 index 00000000..5ec9435f --- /dev/null +++ b/src/app/api/teams/[teamId]/users/route.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest } from '@/lib/response'; +import { canAddUserToTeam, canUpdateTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { pagingParams, roleParam } from '@/lib/schema'; +import { createTeamUser, getTeamUser, getTeamUsers } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const users = await getTeamUsers( + { + where: { + teamId, + user: { + deletedAt: null, + }, + }, + include: { + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, + query, + ); + + return json(users); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const schema = z.object({ + role: roleParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canAddUserToTeam(auth))) { + return unauthorized(); + } + + const { userId, role } = body; + + const teamUser = await getTeamUser(teamId, userId); + + if (teamUser) { + return badRequest('User is already a member of the Team.'); + } + + const users = await createTeamUser(userId, teamId, role); + + return json(users); +} diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts new file mode 100644 index 00000000..f69ab465 --- /dev/null +++ b/src/app/api/teams/[teamId]/websites/route.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; +import { unauthorized, json } from '@/lib/response'; +import { canViewTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { pagingParams } from '@/lib/schema'; +import { getTeamWebsites } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + const { teamId } = await params; + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewTeam(auth, teamId))) { + return unauthorized(); + } + + const websites = await getTeamWebsites(teamId, query); + + return json(websites); +} diff --git a/src/app/api/teams/join/route.ts b/src/app/api/teams/join/route.ts new file mode 100644 index 00000000..3464054c --- /dev/null +++ b/src/app/api/teams/join/route.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest, notFound } from '@/lib/response'; +import { canCreateTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { ROLES } from '@/lib/constants'; +import { createTeamUser, findTeam, getTeamUser } from '@/queries'; + +export async function POST(request: Request) { + const schema = z.object({ + accessCode: z.string().max(50), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canCreateTeam(auth))) { + return unauthorized(); + } + + const { accessCode } = body; + + const team = await findTeam({ + where: { + accessCode, + }, + }); + + if (!team) { + return notFound('Team not found.'); + } + + const teamUser = await getTeamUser(team.id, auth.user.id); + + if (teamUser) { + return badRequest('User is already a team member.'); + } + + const user = await createTeamUser(auth.user.id, team.id, ROLES.teamMember); + + return json(user); +} diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts new file mode 100644 index 00000000..d319d87b --- /dev/null +++ b/src/app/api/teams/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { getRandomChars } from '@/lib/crypto'; +import { unauthorized, json } from '@/lib/response'; +import { canCreateTeam } from '@/lib/auth'; +import { uuid } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { createTeam } from '@/queries'; + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(50), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canCreateTeam(auth))) { + return unauthorized(); + } + + const { name } = body; + + const team = await createTeam( + { + id: uuid(), + name, + accessCode: `team_${getRandomChars(16)}`, + }, + auth.user.id, + ); + + return json(team); +} diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts new file mode 100644 index 00000000..abb3331d --- /dev/null +++ b/src/app/api/users/[userId]/route.ts @@ -0,0 +1,101 @@ +import { z } from 'zod'; +import { canUpdateUser, canViewUser, canDeleteUser } from '@/lib/auth'; +import { getUser, getUserByUsername, updateUser, deleteUser } from '@/queries'; +import { json, unauthorized, badRequest, ok } from '@/lib/response'; +import { hashPassword } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!(await canViewUser(auth, userId))) { + return unauthorized(); + } + + const user = await getUser(userId); + + return json(user); +} + +export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + username: z.string().max(255), + password: z.string().max(255), + role: z.string().regex(/admin|user|view-only/i), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!(await canUpdateUser(auth, userId))) { + return unauthorized(); + } + + const { username, password, role } = body; + + const user = await getUser(userId); + + const data: any = {}; + + if (password) { + data.password = hashPassword(password); + } + + // Only admin can change these fields + if (role && auth.user.isAdmin) { + data.role = role; + } + + if (username && auth.user.isAdmin) { + data.username = username; + } + + // Check when username changes + if (data.username && user.username !== data.username) { + const user = await getUserByUsername(username); + + if (user) { + return badRequest('User already exists'); + } + } + + const updated = await updateUser(userId, data); + + return json(updated); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ userId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!(await canDeleteUser(auth))) { + return unauthorized(); + } + + if (userId === auth.user.id) { + return badRequest('You cannot delete yourself.'); + } + + await deleteUser(userId); + + return ok(); +} diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts new file mode 100644 index 00000000..ff659525 --- /dev/null +++ b/src/app/api/users/[userId]/teams/route.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; +import { pagingParams } from '@/lib/schema'; +import { getUserTeams } from '@/queries'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (auth.user.id !== userId && !auth.user.isAdmin) { + return unauthorized(); + } + + const teams = await getUserTeams(userId, query); + + return json(teams); +} diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts new file mode 100644 index 00000000..e6ff217d --- /dev/null +++ b/src/app/api/users/[userId]/usage/route.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { json, unauthorized } from '@/lib/response'; +import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website'; +import { getEventUsage } from '@/queries/sql/events/getEventUsage'; +import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!auth.user.isAdmin) { + return unauthorized(); + } + + const { userId } = await params; + const { startAt, endAt } = query; + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const websites = await getAllUserWebsitesIncludingTeamOwner(userId); + + const websiteIds = websites.map(a => a.id); + + const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); + const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); + + const websiteUsage = websites.map(a => ({ + websiteId: a.id, + websiteName: a.name, + websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, + eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, + deletedAt: a.deletedAt, + })); + + const usage = websiteUsage.reduce( + (acc, cv) => { + acc.websiteEventUsage += cv.websiteEventUsage; + acc.eventDataUsage += cv.eventDataUsage; + + return acc; + }, + { websiteEventUsage: 0, eventDataUsage: 0 }, + ); + + const filteredWebsiteUsage = websiteUsage.filter( + a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), + ); + + return json({ + ...usage, + websites: filteredWebsiteUsage, + }); +} diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts new file mode 100644 index 00000000..77d41084 --- /dev/null +++ b/src/app/api/users/[userId]/websites/route.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; +import { unauthorized, json } from '@/lib/response'; +import { getUserWebsites } from '@/queries/prisma/website'; +import { pagingParams } from '@/lib/schema'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!auth.user.isAdmin && auth.user.id !== userId) { + return unauthorized(); + } + + const websites = await getUserWebsites(userId, query); + + return json(websites); +} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 00000000..320f72bd --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { hashPassword, canCreateUser } from '@/lib/auth'; +import { ROLES } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json, badRequest } from '@/lib/response'; +import { createUser, getUserByUsername } from '@/queries'; + +export async function POST(request: Request) { + const schema = z.object({ + username: z.string().max(255), + password: z.string(), + id: z.string().uuid(), + role: z.string().regex(/admin|user|view-only/i), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canCreateUser(auth))) { + return unauthorized(); + } + + const { username, password, role, id } = body; + + const existingUser = await getUserByUsername(username, { showDeleted: true }); + + if (existingUser) { + return badRequest('User already exists'); + } + + const user = await createUser({ + id: id || uuid(), + username, + password: hashPassword(password), + role: role ?? ROLES.user, + }); + + return json(user); +} diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts new file mode 100644 index 00000000..275a4118 --- /dev/null +++ b/src/app/api/version/route.ts @@ -0,0 +1,6 @@ +import { json } from '@/lib/response'; +import { CURRENT_VERSION } from '@/lib/constants'; + +export async function GET() { + return json({ version: CURRENT_VERSION }); +} diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts new file mode 100644 index 00000000..88c0fd17 --- /dev/null +++ b/src/app/api/websites/[websiteId]/active/route.ts @@ -0,0 +1,25 @@ +import { canViewWebsite } from '@/lib/auth'; +import { json, unauthorized } from '@/lib/response'; +import { getActiveVisitors } from '@/queries'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await getActiveVisitors(websiteId); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts new file mode 100644 index 00000000..ea2d10d2 --- /dev/null +++ b/src/app/api/websites/[websiteId]/daterange/route.ts @@ -0,0 +1,25 @@ +import { canViewWebsite } from '@/lib/auth'; +import { getWebsiteDateRange } from '@/queries'; +import { json, unauthorized } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await getWebsiteDateRange(websiteId); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts new file mode 100644 index 00000000..aec7b471 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + event: z.string().optional(), + }); + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, event } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataEvents(websiteId, { + startDate, + endDate, + event, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts new file mode 100644 index 00000000..60101e45 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataFields } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataFields(websiteId, { + startDate, + endDate, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts new file mode 100644 index 00000000..fe085f74 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataProperties } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, propertyName } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts new file mode 100644 index 00000000..6928aa1e --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataStats(websiteId, { startDate, endDate }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts new file mode 100644 index 00000000..2a912439 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataValues } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + eventName: z.string().optional(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, eventName, propertyName } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataValues(websiteId, { + startDate, + endDate, + eventName, + propertyName, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts new file mode 100644 index 00000000..66eaba2c --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { pagingParams } from '@/lib/schema'; +import { getWebsiteEvents } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteEvents(websiteId, { startDate, endDate }, query); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts new file mode 100644 index 00000000..da4b0d4f --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; +import { getEventMetrics } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + unit: unitParam, + timezone: timezoneParam, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { timezone } = query; + const { startDate, endDate, unit } = await getRequestDateRange(query); + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + timezone, + unit, + }; + + const data = await getEventMetrics(websiteId, filters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts new file mode 100644 index 00000000..70ed9f90 --- /dev/null +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -0,0 +1,167 @@ +import { z } from 'zod'; +import thenby from 'thenby'; +import { canViewWebsite } from '@/lib/auth'; +import { + SESSION_COLUMNS, + EVENT_COLUMNS, + FILTER_COLUMNS, + OPERATORS, + SEARCH_DOMAINS, + SOCIAL_DOMAINS, + EMAIL_DOMAINS, + SHOPPING_DOMAINS, + VIDEO_DOMAINS, + PAID_AD_PARAMS, +} from '@/lib/constants'; +import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request'; +import { json, unauthorized, badRequest } from '@/lib/response'; +import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries'; +import { filterParams } from '@/lib/schema'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: z.string(), + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + search: z.string().optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { type, limit, offset, search } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + const column = FILTER_COLUMNS[type] || type; + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + }; + + if (search) { + filters[type] = { + name: type, + column, + operator: OPERATORS.contains, + value: search, + }; + } + + if (SESSION_COLUMNS.includes(type)) { + const data = await getSessionMetrics(websiteId, type, filters, limit, offset); + + if (type === 'language') { + const combined = {}; + + for (const { x, y } of data) { + const key = String(x).toLowerCase().split('-')[0]; + + if (combined[key] === undefined) { + combined[key] = { x: key, y }; + } else { + combined[key].y += y; + } + } + + return json(Object.values(combined)); + } + + return json(data); + } + + if (EVENT_COLUMNS.includes(type)) { + const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); + + return json(data); + } + + if (type === 'channel') { + const data = await getChannelMetrics(websiteId, filters); + + const channels = getChannels(data); + + return json( + Object.keys(channels) + .map(key => ({ x: key, y: channels[key] })) + .sort(thenby.firstBy('y', -1)), + ); + } + + return badRequest(); +} + +function getChannels(data: { domain: string; query: string; visitors: number }[]) { + const channels = { + direct: 0, + referral: 0, + affiliate: 0, + email: 0, + sms: 0, + organicSearch: 0, + organicSocial: 0, + organicShopping: 0, + organicVideo: 0, + paidAds: 0, + paidSearch: 0, + paidSocial: 0, + paidShopping: 0, + paidVideo: 0, + }; + + const match = (value: string) => { + return (str: string | RegExp) => { + return typeof str === 'string' ? value.includes(str) : (str as RegExp).test(value); + }; + }; + + for (const { domain, query, visitors } of data) { + if (!domain && !query) { + channels.direct += visitors; + } + + const prefix = /utm_medium=(.*cp.*|ppc|retargeting|paid.*)/.test(query) ? 'paid' : 'organic'; + + if (SEARCH_DOMAINS.some(match(domain)) || /utm_medium=organic/.test(query)) { + channels[`${prefix}Search`] += visitors; + } else if ( + SOCIAL_DOMAINS.some(match(domain)) || + /utm_medium=(social|social-network|social-media|sm|social network|social media)/.test(query) + ) { + channels[`${prefix}Social`] += visitors; + } else if (EMAIL_DOMAINS.some(match(domain)) || /utm_medium=(.*e[-_ ]?mail.*)/.test(query)) { + channels.email += visitors; + } else if ( + SHOPPING_DOMAINS.some(match(domain)) || + /utm_campaign=(.*(([^a-df-z]|^)shop|shopping).*)/.test(query) + ) { + channels[`${prefix}Shopping`] += visitors; + } else if (VIDEO_DOMAINS.some(match(domain)) || /utm_medium=(.*video.*)/.test(query)) { + channels[`${prefix}Video`] += visitors; + } else if (PAID_AD_PARAMS.some(match(query))) { + channels.paidAds += visitors; + } else if (/utm_medium=(referral|app|link)/.test(query)) { + channels.referral += visitors; + } else if (/utm_medium=affiliate/.test(query)) { + channels.affiliate += visitors; + } else if (/utm_(source|medium)=sms/.test(query)) { + channels.sms += visitors; + } + } + + return channels; +} diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts new file mode 100644 index 00000000..e603ae9c --- /dev/null +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -0,0 +1,85 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request'; +import { unitParam, timezoneParam, filterParams } from '@/lib/schema'; +import { getCompareDate } from '@/lib/date'; +import { unauthorized, json } from '@/lib/response'; +import { getPageviewStats, getSessionStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + unit: unitParam, + timezone: timezoneParam, + compare: z.string().optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { timezone, compare } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate, unit } = await getRequestDateRange(query); + + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + timezone, + unit, + }; + + const [pageviews, sessions] = await Promise.all([ + getPageviewStats(websiteId, filters), + getSessionStats(websiteId, filters), + ]); + + if (compare) { + const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( + compare, + startDate, + endDate, + ); + + const [comparePageviews, compareSessions] = await Promise.all([ + getPageviewStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }), + getSessionStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }), + ]); + + return json({ + pageviews, + sessions, + startDate, + endDate, + compare: { + pageviews: comparePageviews, + sessions: compareSessions, + startDate: compareStartDate, + endDate: compareEndDate, + }, + }); + } + + return json({ pageviews, sessions }); +} diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts new file mode 100644 index 00000000..c6941f53 --- /dev/null +++ b/src/app/api/websites/[websiteId]/reports/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { getWebsiteReports } from '@/queries'; +import { pagingParams } from '@/lib/schema'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { page, pageSize, search } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getWebsiteReports(websiteId, { + page: +page, + pageSize: +pageSize, + search, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts new file mode 100644 index 00000000..62edceea --- /dev/null +++ b/src/app/api/websites/[websiteId]/reset/route.ts @@ -0,0 +1,25 @@ +import { canUpdateWebsite } from '@/lib/auth'; +import { resetWebsite } from '@/queries'; +import { unauthorized, ok } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + await resetWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts new file mode 100644 index 00000000..f4ea327b --- /dev/null +++ b/src/app/api/websites/[websiteId]/route.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; +import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/lib/auth'; +import { SHARE_ID_REGEX } from '@/lib/constants'; +import { parseRequest } from '@/lib/request'; +import { ok, json, unauthorized, serverError } from '@/lib/response'; +import { deleteWebsite, getWebsite, updateWebsite } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const website = await getWebsite(websiteId); + + return json(website); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + name: z.string(), + domain: z.string(), + shareId: z.string().regex(SHARE_ID_REGEX).nullable(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { name, domain, shareId } = body; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + try { + const website = await updateWebsite(websiteId, { name, domain, shareId }); + + return Response.json(website); + } catch (e: any) { + if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { + return serverError(new Error('That share ID is already taken.')); + } + + return serverError(e); + } +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canDeleteWebsite(auth, websiteId))) { + return unauthorized(); + } + + await deleteWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts new file mode 100644 index 00000000..a6d9e2a4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getSessionDataProperties } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { startAt, endAt, propertyName } = query; + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts new file mode 100644 index 00000000..93e91775 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { startAt, endAt, event } = query; + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataEvents(websiteId, { + startDate, + endDate, + event, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts new file mode 100644 index 00000000..aac40c38 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getSessionActivity } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId, sessionId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts new file mode 100644 index 00000000..9c389c82 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts @@ -0,0 +1,25 @@ +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getSessionData } from '@/queries'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, sessionId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getSessionData(websiteId, sessionId); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts new file mode 100644 index 00000000..c4621ef4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts @@ -0,0 +1,25 @@ +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getWebsiteSession } from '@/queries'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, sessionId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getWebsiteSession(websiteId, sessionId); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts new file mode 100644 index 00000000..5a14f00f --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { pagingParams } from '@/lib/schema'; +import { getWebsiteSessions } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteSessions(websiteId, { startDate, endDate }, query); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts new file mode 100644 index 00000000..e8e8e6c8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; +import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { filterParams } from '@/lib/schema'; +import { getWebsiteSessionStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + + const filters = getRequestFilters(query); + + const metrics = await getWebsiteSessionStats(websiteId, { + ...filters, + startDate, + endDate, + }); + + const data = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = { + value: Number(metrics[0][key]) || 0, + }; + return obj; + }, {}); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts new file mode 100644 index 00000000..20be378d --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { pagingParams, timezoneParam } from '@/lib/schema'; +import { getWebsiteSessionsWeekly } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + timezone: timezoneParam, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, timezone } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts new file mode 100644 index 00000000..c146271f --- /dev/null +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getCompareDate } from '@/lib/date'; +import { filterParams } from '@/lib/schema'; +import { getWebsiteStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + compare: z.string().optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { compare } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( + compare, + startDate, + endDate, + ); + + const filters = getRequestFilters(query); + + const metrics = await getWebsiteStats(websiteId, { + ...filters, + startDate, + endDate, + }); + + const prevPeriod = await getWebsiteStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }); + + const stats = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = { + value: Number(metrics[0][key]) || 0, + prev: Number(prevPeriod[0][key]) || 0, + }; + return obj; + }, {}); + + return json(stats); +} diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts new file mode 100644 index 00000000..03c0ae7f --- /dev/null +++ b/src/app/api/websites/[websiteId]/transfer/route.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/lib/auth'; +import { updateWebsite } from '@/queries'; +import { parseRequest } from '@/lib/request'; +import { badRequest, unauthorized, json } from '@/lib/response'; + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + userId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { userId, teamId } = body; + + if (userId) { + if (!(await canTransferWebsiteToUser(auth, websiteId, userId))) { + return unauthorized(); + } + + const website = await updateWebsite(websiteId, { + userId, + teamId: null, + }); + + return json(website); + } else if (teamId) { + if (!(await canTransferWebsiteToTeam(auth, websiteId, teamId))) { + return unauthorized(); + } + + const website = await updateWebsite(websiteId, { + userId: null, + teamId, + }); + + return json(website); + } + + return badRequest(); +} diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts new file mode 100644 index 00000000..ed3cfae6 --- /dev/null +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { getValues } from '@/queries'; +import { parseRequest, getRequestDateRange } from '@/lib/request'; +import { badRequest, json, unauthorized } from '@/lib/response'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: z.string(), + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + search: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { type, search } = query; + const { startDate, endDate } = await getRequestDateRange(query); + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) { + return badRequest('Invalid type.'); + } + + const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); + + return json(values.filter(n => n).sort()); +} diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts new file mode 100644 index 00000000..b8fb2a0b --- /dev/null +++ b/src/app/api/websites/route.ts @@ -0,0 +1,59 @@ +import { z } from 'zod'; +import { canCreateTeamWebsite, canCreateWebsite } from '@/lib/auth'; +import { json, unauthorized } from '@/lib/response'; +import { uuid } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { createWebsite, getUserWebsites } from '@/queries'; +import { pagingParams } from '@/lib/schema'; + +export async function GET(request: Request) { + const schema = z.object({ ...pagingParams }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const websites = await getUserWebsites(auth.user.id, query); + + return json(websites); +} + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(100), + domain: z.string().max(500), + shareId: z.string().max(50).nullable().optional(), + teamId: z.string().nullable().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { name, domain, shareId, teamId } = body; + + if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { + return unauthorized(); + } + + const data: any = { + id: uuid(), + createdBy: auth.user.id, + name, + domain, + shareId, + teamId, + }; + + if (!teamId) { + data.userId = auth.user.id; + } + + const website = await createWebsite(data); + + return json(website); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3c0ed43c..f88d8169 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,8 +5,8 @@ import '@fontsource/inter/400.css'; import '@fontsource/inter/500.css'; import '@fontsource/inter/700.css'; import 'react-basics/dist/styles.css'; -import 'styles/index.css'; -import 'styles/variables.css'; +import '@/styles/index.css'; +import '@/styles/variables.css'; export default function ({ children }) { return ( diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx index 3101bf48..a808c622 100644 --- a/src/app/login/LoginForm.tsx +++ b/src/app/login/LoginForm.tsx @@ -9,10 +9,10 @@ import { Icon, } from 'react-basics'; import { useRouter } from 'next/navigation'; -import { useApi, useMessages } from 'components/hooks'; -import { setUser } from 'store/app'; -import { setClientAuthToken } from 'lib/client'; -import Logo from 'assets/logo.svg'; +import { useApi, useMessages } from '@/components/hooks'; +import { setUser } from '@/store/app'; +import { setClientAuthToken } from '@/lib/client'; +import Logo from '@/assets/logo.svg'; import styles from './LoginForm.module.css'; export function LoginForm() { diff --git a/src/app/logout/LogoutPage.tsx b/src/app/logout/LogoutPage.tsx index 11d96329..d3dc481a 100644 --- a/src/app/logout/LogoutPage.tsx +++ b/src/app/logout/LogoutPage.tsx @@ -1,9 +1,9 @@ 'use client'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { useApi } from 'components/hooks'; -import { setUser } from 'store/app'; -import { removeClientAuthToken } from 'lib/client'; +import { useApi } from '@/components/hooks'; +import { setUser } from '@/store/app'; +import { removeClientAuthToken } from '@/lib/client'; export function LogoutPage() { const disabled = !!(process.env.disableLogin || process.env.cloudMode); diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 7a2bbb53..c673e40f 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,6 +1,6 @@ 'use client'; import { Flexbox } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export default function () { const { formatMessage, labels } = useMessages(); diff --git a/src/app/share/[...shareId]/Footer.tsx b/src/app/share/[...shareId]/Footer.tsx index 3a07c12a..e1ba9833 100644 --- a/src/app/share/[...shareId]/Footer.tsx +++ b/src/app/share/[...shareId]/Footer.tsx @@ -1,4 +1,4 @@ -import { CURRENT_VERSION, HOMEPAGE_URL } from 'lib/constants'; +import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants'; import styles from './Footer.module.css'; export function Footer() { diff --git a/src/app/share/[...shareId]/Header.module.css b/src/app/share/[...shareId]/Header.module.css index d353d79a..04478199 100644 --- a/src/app/share/[...shareId]/Header.module.css +++ b/src/app/share/[...shareId]/Header.module.css @@ -7,10 +7,6 @@ height: 100px; } -.row { - align-items: center; -} - .title { display: flex; flex-direction: row; diff --git a/src/app/share/[...shareId]/Header.tsx b/src/app/share/[...shareId]/Header.tsx index ddfb52a5..a71a5b56 100644 --- a/src/app/share/[...shareId]/Header.tsx +++ b/src/app/share/[...shareId]/Header.tsx @@ -1,9 +1,9 @@ import { Icon, Text } from 'react-basics'; import Link from 'next/link'; -import LanguageButton from 'components/input/LanguageButton'; -import ThemeButton from 'components/input/ThemeButton'; -import SettingsButton from 'components/input/SettingsButton'; -import Icons from 'components/icons'; +import LanguageButton from '@/components/input/LanguageButton'; +import ThemeButton from '@/components/input/ThemeButton'; +import SettingsButton from '@/components/input/SettingsButton'; +import Icons from '@/components/icons'; import styles from './Header.module.css'; export function Header() { diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx index c4d9af62..00c7ec3f 100644 --- a/src/app/share/[...shareId]/SharePage.tsx +++ b/src/app/share/[...shareId]/SharePage.tsx @@ -1,11 +1,11 @@ 'use client'; import WebsiteDetailsPage from '../../(main)/websites/[websiteId]/WebsiteDetailsPage'; -import { useShareToken } from 'components/hooks'; -import Page from 'components/layout/Page'; +import { useShareToken } from '@/components/hooks'; +import Page from '@/components/layout/Page'; import Header from './Header'; import Footer from './Footer'; import styles from './SharePage.module.css'; -import { WebsiteProvider } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; export default function SharePage({ shareId }) { const { shareToken, isLoading } = useShareToken(shareId); diff --git a/src/app/sso/SSOPage.tsx b/src/app/sso/SSOPage.tsx index e577767a..eb7c0f0a 100644 --- a/src/app/sso/SSOPage.tsx +++ b/src/app/sso/SSOPage.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { Loading } from 'react-basics'; import { useRouter, useSearchParams } from 'next/navigation'; -import { setClientAuthToken } from 'lib/client'; +import { setClientAuthToken } from '@/lib/client'; export default function SSOPage() { const router = useRouter(); diff --git a/src/components/charts/BarChart.tsx b/src/components/charts/BarChart.tsx index 7c16730e..f6a6e5e0 100644 --- a/src/components/charts/BarChart.tsx +++ b/src/components/charts/BarChart.tsx @@ -1,7 +1,7 @@ -import BarChartTooltip from 'components/charts/BarChartTooltip'; -import Chart, { ChartProps } from 'components/charts/Chart'; -import { useTheme } from 'components/hooks'; -import { renderNumberLabels } from 'lib/charts'; +import BarChartTooltip from '@/components/charts/BarChartTooltip'; +import Chart, { ChartProps } from '@/components/charts/Chart'; +import { useTheme } from '@/components/hooks'; +import { renderNumberLabels } from '@/lib/charts'; import { useMemo, useState } from 'react'; export interface BarChartProps extends ChartProps { diff --git a/src/components/charts/BarChartTooltip.tsx b/src/components/charts/BarChartTooltip.tsx index 201c6e4c..af31c874 100644 --- a/src/components/charts/BarChartTooltip.tsx +++ b/src/components/charts/BarChartTooltip.tsx @@ -1,6 +1,6 @@ -import { useLocale } from 'components/hooks'; -import { formatDate } from 'lib/date'; -import { formatLongCurrency, formatLongNumber } from 'lib/format'; +import { useLocale } from '@/components/hooks'; +import { formatDate } from '@/lib/date'; +import { formatLongCurrency, formatLongNumber } from '@/lib/format'; import { Flexbox, StatusLight } from 'react-basics'; const formats = { diff --git a/src/components/charts/BubbleChart.tsx b/src/components/charts/BubbleChart.tsx index 956e260c..dfe67f3a 100644 --- a/src/components/charts/BubbleChart.tsx +++ b/src/components/charts/BubbleChart.tsx @@ -1,7 +1,7 @@ -import { Chart, ChartProps } from 'components/charts/Chart'; +import { Chart, ChartProps } from '@/components/charts/Chart'; import { useState } from 'react'; import { StatusLight } from 'react-basics'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; export interface BubbleChartProps extends ChartProps { type?: 'bubble'; diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index a4badbce..dde01eb4 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -2,9 +2,9 @@ import { useState, useRef, useEffect, useMemo, ReactNode } from 'react'; import { Loading } from 'react-basics'; import classNames from 'classnames'; import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto'; -import HoverTooltip from 'components/common/HoverTooltip'; -import Legend from 'components/metrics/Legend'; -import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; +import HoverTooltip from '@/components/common/HoverTooltip'; +import Legend from '@/components/metrics/Legend'; +import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants'; import styles from './Chart.module.css'; export interface ChartProps { diff --git a/src/components/charts/PieChart.tsx b/src/components/charts/PieChart.tsx index 57d676ca..a98b9730 100644 --- a/src/components/charts/PieChart.tsx +++ b/src/components/charts/PieChart.tsx @@ -1,7 +1,7 @@ -import { Chart, ChartProps } from 'components/charts/Chart'; +import { Chart, ChartProps } from '@/components/charts/Chart'; import { useState } from 'react'; import { StatusLight } from 'react-basics'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; export interface PieChartProps extends ChartProps { type?: 'doughnut' | 'pie'; diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 2e82b078..d0cae247 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { createAvatar } from '@dicebear/core'; import { lorelei } from '@dicebear/collection'; -import { getColor, getPastel } from 'lib/colors'; +import { getColor, getPastel } from '@/lib/colors'; const lib = lorelei; diff --git a/src/components/common/ConfirmationForm.tsx b/src/components/common/ConfirmationForm.tsx index 26b4ff24..8b617ab5 100644 --- a/src/components/common/ConfirmationForm.tsx +++ b/src/components/common/ConfirmationForm.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; import { Button, LoadingButton, Form, FormButtons } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export interface ConfirmationFormProps { message: ReactNode; diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx index d2094329..b19ddf91 100644 --- a/src/components/common/DataTable.tsx +++ b/src/components/common/DataTable.tsx @@ -1,12 +1,12 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; import { Loading, SearchField } from 'react-basics'; -import { useMessages, useNavigation } from 'components/hooks'; -import Empty from 'components/common/Empty'; -import Pager from 'components/common/Pager'; -import { PagedQueryResult } from 'lib/types'; +import { useMessages, useNavigation } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; +import Pager from '@/components/common/Pager'; +import { PagedQueryResult } from '@/lib/types'; import styles from './DataTable.module.css'; -import { LoadingPanel } from 'components/common/LoadingPanel'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; const DEFAULT_SEARCH_DELAY = 600; @@ -37,26 +37,26 @@ export function DataTable({ query: { error, isLoading, isFetched }, } = queryResult || {}; const { page, pageSize, count, data } = result || {}; - const { query } = params || {}; + const { search } = params || {}; const hasData = Boolean(!isLoading && data?.length); - const noResults = Boolean(query && !hasData); + const noResults = Boolean(search && !hasData); const { router, renderUrl } = useNavigation(); - const handleSearch = (query: string) => { - setParams({ ...params, query, page: params.page ? page : 1 }); + const handleSearch = (search: string) => { + setParams({ ...params, search, page: params.page ? page : 1 }); }; const handlePageChange = (page: number) => { - setParams({ ...params, query, page }); + setParams({ ...params, search, page }); router.push(renderUrl({ page })); }; return ( <> - {allowSearch && (hasData || query) && ( + {allowSearch && (hasData || search) && ( {hasData ? (typeof children === 'function' ? children(result) : children) : null} {isLoading && } - {!isLoading && !hasData && !query && (renderEmpty ? renderEmpty() : )} + {!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : )} {!isLoading && noResults && } {allowPaging && hasData && ( diff --git a/src/components/common/Empty.tsx b/src/components/common/Empty.tsx index 8e7d2d00..cf6d11cc 100644 --- a/src/components/common/Empty.tsx +++ b/src/components/common/Empty.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './Empty.module.css'; export interface EmptyProps { diff --git a/src/components/common/EmptyPlaceholder.tsx b/src/components/common/EmptyPlaceholder.tsx index 640e45d5..2fd606cd 100644 --- a/src/components/common/EmptyPlaceholder.tsx +++ b/src/components/common/EmptyPlaceholder.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; import { Icon, Text, Flexbox } from 'react-basics'; -import Logo from 'assets/logo.svg'; +import Logo from '@/assets/logo.svg'; export interface EmptyPlaceholderProps { message?: string; diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index 9669580f..b9521bb4 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -1,7 +1,7 @@ import { ErrorInfo, ReactNode } from 'react'; import { ErrorBoundary as Boundary } from 'react-error-boundary'; import { Button } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './ErrorBoundary.module.css'; const logError = (error: Error, info: ErrorInfo) => { diff --git a/src/components/common/ErrorMessage.tsx b/src/components/common/ErrorMessage.tsx index 7ed8662a..bf3eefb1 100644 --- a/src/components/common/ErrorMessage.tsx +++ b/src/components/common/ErrorMessage.tsx @@ -1,6 +1,6 @@ import { Icon, Icons, Text } from 'react-basics'; import styles from './ErrorMessage.module.css'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function ErrorMessage() { const { formatMessage, messages } = useMessages(); diff --git a/src/components/common/Favicon.tsx b/src/components/common/Favicon.tsx index 47c65aab..ea3f31aa 100644 --- a/src/components/common/Favicon.tsx +++ b/src/components/common/Favicon.tsx @@ -1,3 +1,5 @@ +import { GROUPED_DOMAINS } from '@/lib/constants'; + function getHostName(url: string) { const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?([^:/\n?=]+)/im); return match && match.length > 1 ? match[1] : null; @@ -9,16 +11,11 @@ export function Favicon({ domain, ...props }) { } const hostName = domain ? getHostName(domain) : null; + const src = hostName + ? `https://icons.duckduckgo.com/ip3/${GROUPED_DOMAINS[hostName]?.domain || hostName}.ico` + : null; - return hostName ? ( - - ) : null; + return hostName ? : null; } export default Favicon; diff --git a/src/components/common/FilterLink.tsx b/src/components/common/FilterLink.tsx index ef278ed2..9d726b58 100644 --- a/src/components/common/FilterLink.tsx +++ b/src/components/common/FilterLink.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames'; -import { useMessages, useNavigation } from 'components/hooks'; -import { safeDecodeURIComponent } from 'next-basics'; +import { useMessages, useNavigation } from '@/components/hooks'; import Link from 'next/link'; import { ReactNode } from 'react'; import { Icon, Icons } from 'react-basics'; @@ -39,7 +38,7 @@ export function FilterLink({ {!value && `(${label || formatMessage(labels.unknown)})`} {value && ( - {safeDecodeURIComponent(label || value)} + {label || value} )} {externalUrl && ( diff --git a/src/components/common/LinkButton.tsx b/src/components/common/LinkButton.tsx index 83d95151..3aa2a76a 100644 --- a/src/components/common/LinkButton.tsx +++ b/src/components/common/LinkButton.tsx @@ -1,8 +1,8 @@ +import { ReactNode } from 'react'; import classNames from 'classnames'; import Link from 'next/link'; -import { useLocale } from 'components/hooks'; +import { useLocale } from '@/components/hooks'; import styles from './LinkButton.module.css'; -import { ReactNode } from 'react'; export interface LinkButtonProps { href: string; diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx index 36de9365..4d27618a 100644 --- a/src/components/common/LoadingPanel.tsx +++ b/src/components/common/LoadingPanel.tsx @@ -1,8 +1,8 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; import { Loading } from 'react-basics'; -import ErrorMessage from 'components/common/ErrorMessage'; -import Empty from 'components/common/Empty'; +import ErrorMessage from '@/components/common/ErrorMessage'; +import Empty from '@/components/common/Empty'; import styles from './LoadingPanel.module.css'; export function LoadingPanel({ diff --git a/src/components/common/Pager.tsx b/src/components/common/Pager.tsx index 3e0a8033..b33d2236 100644 --- a/src/components/common/Pager.tsx +++ b/src/components/common/Pager.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { Button, Icon, Icons } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './Pager.module.css'; export interface PagerProps { diff --git a/src/components/common/TypeConfirmationForm.tsx b/src/components/common/TypeConfirmationForm.tsx index 2dfb2dff..baf5949f 100644 --- a/src/components/common/TypeConfirmationForm.tsx +++ b/src/components/common/TypeConfirmationForm.tsx @@ -7,7 +7,7 @@ import { TextField, SubmitButton, } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function TypeConfirmationForm({ confirmationValue, @@ -26,7 +26,7 @@ export function TypeConfirmationForm({ onConfirm?: () => void; onClose?: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); if (!confirmationValue) { return null; @@ -35,10 +35,7 @@ export function TypeConfirmationForm({ return (

- {confirmationValue} }} - /> + {formatMessage(messages.actionConfirmation, { confirmation: {confirmationValue} })}

value === confirmationValue }}> diff --git a/src/components/hooks/queries/useConfig.ts b/src/components/hooks/queries/useConfig.ts index f6293a44..223f4550 100644 --- a/src/components/hooks/queries/useConfig.ts +++ b/src/components/hooks/queries/useConfig.ts @@ -1,23 +1,16 @@ import { useEffect } from 'react'; -import useStore, { setConfig } from 'store/app'; -import { useApi } from '../useApi'; - -let loading = false; +import useStore, { setConfig } from '@/store/app'; +import { getConfig } from '@/app/actions/getConfig'; export function useConfig() { const { config } = useStore(); - const { get } = useApi(); - const configUrl = process.env.configUrl; async function loadConfig() { - const data = await get(configUrl); - loading = false; - setConfig(data); + setConfig(await getConfig()); } useEffect(() => { - if (!config && !loading && configUrl) { - loading = true; + if (!config) { loadConfig(); } }, []); diff --git a/src/components/hooks/queries/useLogin.ts b/src/components/hooks/queries/useLogin.ts index a54f38d1..f88efbf0 100644 --- a/src/components/hooks/queries/useLogin.ts +++ b/src/components/hooks/queries/useLogin.ts @@ -1,5 +1,5 @@ import { UseQueryResult } from '@tanstack/react-query'; -import useStore, { setUser } from 'store/app'; +import useStore, { setUser } from '@/store/app'; import { useApi } from '../useApi'; const selector = (state: { user: any }) => state.user; diff --git a/src/components/hooks/queries/useRealtime.ts b/src/components/hooks/queries/useRealtime.ts index b87f74c4..670b23be 100644 --- a/src/components/hooks/queries/useRealtime.ts +++ b/src/components/hooks/queries/useRealtime.ts @@ -1,6 +1,6 @@ -import { useTimezone } from 'components/hooks'; -import { REALTIME_INTERVAL } from 'lib/constants'; -import { RealtimeData } from 'lib/types'; +import { useTimezone } from '@/components/hooks/useTimezone'; +import { REALTIME_INTERVAL } from '@/lib/constants'; +import { RealtimeData } from '@/lib/types'; import { useApi } from '../useApi'; export function useRealtime(websiteId: string) { diff --git a/src/components/hooks/queries/useShareToken.ts b/src/components/hooks/queries/useShareToken.ts index f9db7dbf..cf17c756 100644 --- a/src/components/hooks/queries/useShareToken.ts +++ b/src/components/hooks/queries/useShareToken.ts @@ -1,4 +1,4 @@ -import useStore, { setShareToken } from 'store/app'; +import useStore, { setShareToken } from '@/store/app'; import { useApi } from '../useApi'; const selector = (state: { shareToken: string }) => state.shareToken; diff --git a/src/components/hooks/queries/useTeams.ts b/src/components/hooks/queries/useTeams.ts index e5197c97..d09e2f7d 100644 --- a/src/components/hooks/queries/useTeams.ts +++ b/src/components/hooks/queries/useTeams.ts @@ -11,6 +11,7 @@ export function useTeams(userId: string) { queryFn: (params: any) => { return get(`/users/${userId}/teams`, params); }, + enabled: !!userId, }); } diff --git a/src/components/hooks/queries/useWebsitePageviews.ts b/src/components/hooks/queries/useWebsitePageviews.ts index 42fb527e..43c51745 100644 --- a/src/components/hooks/queries/useWebsitePageviews.ts +++ b/src/components/hooks/queries/useWebsitePageviews.ts @@ -1,6 +1,6 @@ import { UseQueryOptions } from '@tanstack/react-query'; import { useApi } from '../useApi'; -import { useFilterParams } from '..//useFilterParams'; +import { useFilterParams } from '../useFilterParams'; export function useWebsitePageviews( websiteId: string, diff --git a/src/components/hooks/queries/useWebsiteSessions.ts b/src/components/hooks/queries/useWebsiteSessions.ts index ad7bb616..09e34a80 100644 --- a/src/components/hooks/queries/useWebsiteSessions.ts +++ b/src/components/hooks/queries/useWebsiteSessions.ts @@ -1,7 +1,7 @@ import { useApi } from '../useApi'; import { usePagedQuery } from '../usePagedQuery'; import useModified from '../useModified'; -import { useFilterParams } from 'components/hooks/useFilterParams'; +import { useFilterParams } from '@/components/hooks/useFilterParams'; export function useWebsiteSessions(websiteId: string, params?: { [key: string]: string | number }) { const { get } = useApi(); diff --git a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts index c4e83f98..f3aa3b00 100644 --- a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts +++ b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts @@ -1,6 +1,6 @@ import { useApi } from '../useApi'; import useModified from '../useModified'; -import { useFilterParams } from 'components/hooks/useFilterParams'; +import { useFilterParams } from '@/components/hooks/useFilterParams'; export function useWebsiteSessionsWeekly( websiteId: string, diff --git a/src/components/hooks/queries/useWebsiteValues.ts b/src/components/hooks/queries/useWebsiteValues.ts index 73a7c755..77f65fe5 100644 --- a/src/components/hooks/queries/useWebsiteValues.ts +++ b/src/components/hooks/queries/useWebsiteValues.ts @@ -1,5 +1,6 @@ import { useApi } from '../useApi'; -import { useCountryNames, useRegionNames } from 'components/hooks'; +import { useCountryNames } from '@/components/hooks/useCountryNames'; +import { useRegionNames } from '@/components/hooks/useRegionNames'; import useLocale from '../useLocale'; export function useWebsiteValues({ diff --git a/src/components/hooks/useApi.ts b/src/components/hooks/useApi.ts index e806d37e..be386a29 100644 --- a/src/components/hooks/useApi.ts +++ b/src/components/hooks/useApi.ts @@ -1,20 +1,78 @@ +import { useCallback } from 'react'; import * as reactQuery from '@tanstack/react-query'; -import { useApi as nextUseApi } from 'next-basics'; -import { getClientAuthToken } from 'lib/client'; -import { SHARE_TOKEN_HEADER } from 'lib/constants'; -import useStore from 'store/app'; +import { getClientAuthToken } from '@/lib/client'; +import { SHARE_TOKEN_HEADER } from '@/lib/constants'; +import { httpGet, httpPost, httpPut, httpDelete } from '@/lib/fetch'; +import useStore from '@/store/app'; const selector = (state: { shareToken: { token?: string } }) => state.shareToken; +async function handleResponse(data: any): Promise { + if (data.error) { + return Promise.reject(new Error(data.error)); + } + return Promise.resolve(data); +} + +function handleError(err: Error | string) { + return Promise.reject((err as Error)?.message || err || null); +} + export function useApi() { const shareToken = useStore(selector); - const { get, post, put, del } = nextUseApi( - { authorization: `Bearer ${getClientAuthToken()}`, [SHARE_TOKEN_HEADER]: shareToken?.token }, - process.env.basePath, - ); + const defaultHeaders = { + authorization: `Bearer ${getClientAuthToken()}`, + [SHARE_TOKEN_HEADER]: shareToken?.token, + }; + const basePath = process.env.basePath; - return { get, post, put, del, ...reactQuery }; + const getUrl = (url: string) => { + return url.startsWith('http') ? url : `${basePath || ''}/api${url}`; + }; + + const getHeaders = (headers: any = {}) => { + return { ...defaultHeaders, ...headers }; + }; + + return { + get: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpGet(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpGet], + ), + + post: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpPost(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpPost], + ), + + put: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpPut(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpPut], + ), + + del: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpDelete(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpDelete], + ), + ...reactQuery, + }; } export default useApi; diff --git a/src/components/hooks/useCountryNames.ts b/src/components/hooks/useCountryNames.ts index 2bdaa94e..691167e1 100644 --- a/src/components/hooks/useCountryNames.ts +++ b/src/components/hooks/useCountryNames.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { httpGet } from 'next-basics'; +import { httpGet } from '@/lib/fetch'; import enUS from '../../../public/intl/country/en-US.json'; const countryNames = { @@ -10,7 +10,7 @@ export function useCountryNames(locale: string) { const [list, setList] = useState(countryNames[locale] || enUS); async function loadData(locale: string) { - const { data } = await httpGet(`${process.env.basePath || ''}/intl/country/${locale}.json`); + const data = await httpGet(`${process.env.basePath || ''}/intl/country/${locale}.json`); if (data) { countryNames[locale] = data; diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 23cb6e70..61838980 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -1,9 +1,9 @@ -import { getMinimumUnit, parseDateRange } from 'lib/date'; -import { setItem } from 'next-basics'; -import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from 'lib/constants'; -import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from 'store/websites'; -import appStore, { setDateRange } from 'store/app'; -import { DateRange } from 'lib/types'; +import { getMinimumUnit, parseDateRange } from '@/lib/date'; +import { setItem } from '@/lib/storage'; +import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from '@/lib/constants'; +import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from '@/store/websites'; +import appStore, { setDateRange } from '@/store/app'; +import { DateRange } from '@/lib/types'; import { useLocale } from './useLocale'; import { useApi } from './useApi'; diff --git a/src/components/hooks/useFilters.ts b/src/components/hooks/useFilters.ts index 5f89eca4..2b99785a 100644 --- a/src/components/hooks/useFilters.ts +++ b/src/components/hooks/useFilters.ts @@ -1,5 +1,5 @@ import { useMessages } from './useMessages'; -import { OPERATORS } from 'lib/constants'; +import { OPERATORS } from '@/lib/constants'; export function useFilters() { const { formatMessage, labels } = useMessages(); diff --git a/src/components/hooks/useFormat.ts b/src/components/hooks/useFormat.ts index 10030721..927e21e8 100644 --- a/src/components/hooks/useFormat.ts +++ b/src/components/hooks/useFormat.ts @@ -1,5 +1,5 @@ import useMessages from './useMessages'; -import { BROWSERS, OS_NAMES } from 'lib/constants'; +import { BROWSERS, OS_NAMES } from '@/lib/constants'; import useLocale from './useLocale'; import useCountryNames from './useCountryNames'; import useLanguageNames from './useLanguageNames'; diff --git a/src/components/hooks/useLanguageNames.ts b/src/components/hooks/useLanguageNames.ts index 07b36a2c..d00b0968 100644 --- a/src/components/hooks/useLanguageNames.ts +++ b/src/components/hooks/useLanguageNames.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { httpGet } from 'next-basics'; +import { httpGet } from '@/lib/fetch'; import enUS from '../../../public/intl/language/en-US.json'; const languageNames = { @@ -10,7 +10,7 @@ export function useLanguageNames(locale) { const [list, setList] = useState(languageNames[locale] || enUS); async function loadData(locale) { - const { data } = await httpGet(`${process.env.basePath || ''}/intl/language/${locale}.json`); + const data = await httpGet(`${process.env.basePath || ''}/intl/language/${locale}.json`); if (data) { languageNames[locale] = data; diff --git a/src/components/hooks/useLocale.ts b/src/components/hooks/useLocale.ts index 69e7cc41..30f78037 100644 --- a/src/components/hooks/useLocale.ts +++ b/src/components/hooks/useLocale.ts @@ -1,8 +1,9 @@ import { useEffect } from 'react'; -import { httpGet, setItem } from 'next-basics'; -import { LOCALE_CONFIG } from 'lib/constants'; -import { getDateLocale, getTextDirection } from 'lib/lang'; -import useStore, { setLocale } from 'store/app'; +import { httpGet } from '@/lib/fetch'; +import { setItem } from '@/lib/storage'; +import { LOCALE_CONFIG } from '@/lib/constants'; +import { getDateLocale, getTextDirection } from '@/lib/lang'; +import useStore, { setLocale } from '@/store/app'; import { useForceUpdate } from './useForceUpdate'; import enUS from '../../../public/intl/country/en-US.json'; @@ -19,13 +20,7 @@ export function useLocale() { const dateLocale = getDateLocale(locale); async function loadMessages(locale: string) { - const { ok, data } = await httpGet( - `${process.env.basePath || ''}/intl/messages/${locale}.json`, - ); - - if (ok) { - messages[locale] = data; - } + messages[locale] = await httpGet(`${process.env.basePath || ''}/intl/messages/${locale}.json`); } async function saveLocale(value: string) { diff --git a/src/components/hooks/useMessages.ts b/src/components/hooks/useMessages.ts index ab37cc19..fc73494f 100644 --- a/src/components/hooks/useMessages.ts +++ b/src/components/hooks/useMessages.ts @@ -1,5 +1,5 @@ -import { useIntl, FormattedMessage } from 'react-intl'; -import { messages, labels } from 'components/messages'; +import { useIntl } from 'react-intl'; +import { messages, labels } from '@/components/messages'; export function useMessages(): any { const intl = useIntl(); @@ -21,7 +21,7 @@ export function useMessages(): any { return descriptor ? intl.formatMessage(descriptor, values, opts) : null; }; - return { formatMessage, FormattedMessage, messages, labels, getMessage }; + return { formatMessage, messages, labels, getMessage }; } export default useMessages; diff --git a/src/components/hooks/useModified.ts b/src/components/hooks/useModified.ts index 858be87e..fd8dc2e6 100644 --- a/src/components/hooks/useModified.ts +++ b/src/components/hooks/useModified.ts @@ -1,4 +1,4 @@ -import useStore from 'store/modified'; +import useStore from '@/store/modified'; export function useModified(key?: string) { const modified = useStore(state => state?.[key]); diff --git a/src/components/hooks/useNavigation.ts b/src/components/hooks/useNavigation.ts index a2c1167a..b727ee90 100644 --- a/src/components/hooks/useNavigation.ts +++ b/src/components/hooks/useNavigation.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { buildUrl, safeDecodeURIComponent } from 'next-basics'; +import { buildUrl } from '@/lib/url'; export function useNavigation(): { pathname: string; @@ -16,7 +16,7 @@ export function useNavigation(): { const obj = {}; for (const [key, value] of params.entries()) { - obj[key] = safeDecodeURIComponent(value); + obj[key] = value; } return obj; diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts index 19471432..bd59189a 100644 --- a/src/components/hooks/usePagedQuery.ts +++ b/src/components/hooks/usePagedQuery.ts @@ -1,6 +1,6 @@ import { UseQueryOptions } from '@tanstack/react-query'; import { useState } from 'react'; -import { PageResult, PageParams, PagedQueryResult } from 'lib/types'; +import { PageResult, PageParams, PagedQueryResult } from '@/lib/types'; import { useApi } from './useApi'; import { useNavigation } from './useNavigation'; @@ -11,8 +11,8 @@ export function usePagedQuery({ }: Omit & { queryFn: (params?: object) => any }): PagedQueryResult { const { query: queryParams } = useNavigation(); const [params, setParams] = useState({ - query: '', - page: +queryParams.page || 1, + search: '', + page: queryParams.page || '1', }); const { useQuery } = useApi(); diff --git a/src/components/hooks/useTheme.ts b/src/components/hooks/useTheme.ts index aa2b1d38..9bbe063c 100644 --- a/src/components/hooks/useTheme.ts +++ b/src/components/hooks/useTheme.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; -import useStore, { setTheme } from 'store/app'; -import { getItem, setItem } from 'next-basics'; -import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from 'lib/constants'; +import useStore, { setTheme } from '@/store/app'; +import { getItem, setItem } from '@/lib/storage'; +import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from '@/lib/constants'; import { colord } from 'colord'; const selector = (state: { theme: string }) => state.theme; diff --git a/src/components/hooks/useTimezone.ts b/src/components/hooks/useTimezone.ts index c74f513f..5f01c2ab 100644 --- a/src/components/hooks/useTimezone.ts +++ b/src/components/hooks/useTimezone.ts @@ -1,7 +1,7 @@ -import { setItem } from 'next-basics'; -import { TIMEZONE_CONFIG } from 'lib/constants'; +import { setItem } from '@/lib/storage'; +import { TIMEZONE_CONFIG } from '@/lib/constants'; import { formatInTimeZone, zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'; -import useStore, { setTimezone } from 'store/app'; +import useStore, { setTimezone } from '@/store/app'; const selector = (state: { timezone: string }) => state.timezone; diff --git a/src/components/icons.ts b/src/components/icons.ts index 1cf26543..e952e500 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -1,30 +1,30 @@ import { Icons } from 'react-basics'; -import AddUser from 'assets/add-user.svg'; -import Bars from 'assets/bars.svg'; -import BarChart from 'assets/bar-chart.svg'; -import Bolt from 'assets/bolt.svg'; -import Calendar from 'assets/calendar.svg'; -import Change from 'assets/change.svg'; -import Clock from 'assets/clock.svg'; -import Compare from 'assets/compare.svg'; -import Dashboard from 'assets/dashboard.svg'; -import Eye from 'assets/eye.svg'; -import Gear from 'assets/gear.svg'; -import Globe from 'assets/globe.svg'; -import Location from 'assets/location.svg'; -import Lock from 'assets/lock.svg'; -import Logo from 'assets/logo.svg'; -import Magnet from 'assets/magnet.svg'; -import Moon from 'assets/moon.svg'; -import Nodes from 'assets/nodes.svg'; -import Overview from 'assets/overview.svg'; -import Profile from 'assets/profile.svg'; -import PushPin from 'assets/pushpin.svg'; -import Reports from 'assets/reports.svg'; -import Sun from 'assets/sun.svg'; -import User from 'assets/user.svg'; -import Users from 'assets/users.svg'; -import Visitor from 'assets/visitor.svg'; +import AddUser from '@/assets/add-user.svg'; +import Bars from '@/assets/bars.svg'; +import BarChart from '@/assets/bar-chart.svg'; +import Bolt from '@/assets/bolt.svg'; +import Calendar from '@/assets/calendar.svg'; +import Change from '@/assets/change.svg'; +import Clock from '@/assets/clock.svg'; +import Compare from '@/assets/compare.svg'; +import Dashboard from '@/assets/dashboard.svg'; +import Eye from '@/assets/eye.svg'; +import Gear from '@/assets/gear.svg'; +import Globe from '@/assets/globe.svg'; +import Location from '@/assets/location.svg'; +import Lock from '@/assets/lock.svg'; +import Logo from '@/assets/logo.svg'; +import Magnet from '@/assets/magnet.svg'; +import Moon from '@/assets/moon.svg'; +import Nodes from '@/assets/nodes.svg'; +import Overview from '@/assets/overview.svg'; +import Profile from '@/assets/profile.svg'; +import PushPin from '@/assets/pushpin.svg'; +import Reports from '@/assets/reports.svg'; +import Sun from '@/assets/sun.svg'; +import User from '@/assets/user.svg'; +import Users from '@/assets/users.svg'; +import Visitor from '@/assets/visitor.svg'; const icons = { ...Icons, diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx index e486551d..443827a0 100644 --- a/src/components/input/DateFilter.tsx +++ b/src/components/input/DateFilter.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; -import DatePickerForm from 'components/metrics/DatePickerForm'; -import { useLocale, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import { formatDate, parseDateValue } from 'lib/date'; +import DatePickerForm from '@/components/metrics/DatePickerForm'; +import { useLocale, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { formatDate, parseDateValue } from '@/lib/date'; import styles from './DateFilter.module.css'; import classNames from 'classnames'; diff --git a/src/components/input/LanguageButton.tsx b/src/components/input/LanguageButton.tsx index 5da3bf78..54ce55eb 100644 --- a/src/components/input/LanguageButton.tsx +++ b/src/components/input/LanguageButton.tsx @@ -1,8 +1,8 @@ import { Icon, Button, PopupTrigger, Popup } from 'react-basics'; import classNames from 'classnames'; -import { languages } from 'lib/lang'; -import { useLocale } from 'components/hooks'; -import Icons from 'components/icons'; +import { languages } from '@/lib/lang'; +import { useLocale } from '@/components/hooks'; +import Icons from '@/components/icons'; import styles from './LanguageButton.module.css'; export function LanguageButton() { diff --git a/src/components/input/LogoutButton.tsx b/src/components/input/LogoutButton.tsx index ddc71142..a1a34a00 100644 --- a/src/components/input/LogoutButton.tsx +++ b/src/components/input/LogoutButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Icons, TooltipPopup } from 'react-basics'; import Link from 'next/link'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function LogoutButton({ tooltipPosition = 'top', diff --git a/src/components/input/MonthSelect.tsx b/src/components/input/MonthSelect.tsx index acb17dfe..144f5bd8 100644 --- a/src/components/input/MonthSelect.tsx +++ b/src/components/input/MonthSelect.tsx @@ -9,9 +9,9 @@ import { Popup, } from 'react-basics'; import { startOfMonth, endOfMonth } from 'date-fns'; -import Icons from 'components/icons'; -import { useLocale } from 'components/hooks'; -import { formatDate } from 'lib/date'; +import Icons from '@/components/icons'; +import { useLocale } from '@/components/hooks'; +import { formatDate } from '@/lib/date'; import styles from './MonthSelect.module.css'; export function MonthSelect({ date = new Date(), onChange }) { diff --git a/src/components/input/ProfileButton.tsx b/src/components/input/ProfileButton.tsx index b1875165..86a9d333 100644 --- a/src/components/input/ProfileButton.tsx +++ b/src/components/input/ProfileButton.tsx @@ -1,9 +1,9 @@ import { Key } from 'react'; import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics'; import { useRouter } from 'next/navigation'; -import Icons from 'components/icons'; -import { useMessages, useLogin, useLocale } from 'components/hooks'; -import { CURRENT_VERSION } from 'lib/constants'; +import Icons from '@/components/icons'; +import { useMessages, useLogin, useLocale } from '@/components/hooks'; +import { CURRENT_VERSION } from '@/lib/constants'; import styles from './ProfileButton.module.css'; export function ProfileButton() { diff --git a/src/components/input/RefreshButton.tsx b/src/components/input/RefreshButton.tsx index cd68c40a..35bfbf3c 100644 --- a/src/components/input/RefreshButton.tsx +++ b/src/components/input/RefreshButton.tsx @@ -1,8 +1,8 @@ import { LoadingButton, Icon, TooltipPopup } from 'react-basics'; -import { setWebsiteDateRange } from 'store/websites'; -import { useDateRange } from 'components/hooks'; -import Icons from 'components/icons'; -import { useMessages } from 'components/hooks'; +import { setWebsiteDateRange } from '@/store/websites'; +import { useDateRange } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { useMessages } from '@/components/hooks'; export function RefreshButton({ websiteId, diff --git a/src/components/input/SettingsButton.tsx b/src/components/input/SettingsButton.tsx index 535d03c3..d3dc471f 100644 --- a/src/components/input/SettingsButton.tsx +++ b/src/components/input/SettingsButton.tsx @@ -1,8 +1,8 @@ import { Button, Icon, PopupTrigger, Popup, Form, FormRow } from 'react-basics'; -import TimezoneSetting from 'app/(main)/profile/TimezoneSetting'; -import DateRangeSetting from 'app/(main)/profile/DateRangeSetting'; -import Icons from 'components/icons'; -import { useMessages } from 'components/hooks'; +import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting'; +import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting'; +import Icons from '@/components/icons'; +import { useMessages } from '@/components/hooks'; import styles from './SettingsButton.module.css'; export function SettingsButton() { diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx index 1f6270b4..f967a64c 100644 --- a/src/components/input/TeamsButton.tsx +++ b/src/components/input/TeamsButton.tsx @@ -1,8 +1,8 @@ import { Key } from 'react'; import { Text, Icon, Button, Popup, Menu, Item, PopupTrigger, Flexbox } from 'react-basics'; import classNames from 'classnames'; -import Icons from 'components/icons'; -import { useLogin, useMessages, useTeams, useTeamUrl } from 'components/hooks'; +import Icons from '@/components/icons'; +import { useLogin, useMessages, useTeams, useTeamUrl } from '@/components/hooks'; import styles from './TeamsButton.module.css'; export function TeamsButton({ @@ -16,7 +16,7 @@ export function TeamsButton({ }) { const { user } = useLogin(); const { formatMessage, labels } = useMessages(); - const { result } = useTeams(user?.id); + const { result } = useTeams(user.id); const { teamId } = useTeamUrl(); const team = result?.data?.find(({ id }) => id === teamId); diff --git a/src/components/input/ThemeButton.tsx b/src/components/input/ThemeButton.tsx index ece571ab..fd7d79a0 100644 --- a/src/components/input/ThemeButton.tsx +++ b/src/components/input/ThemeButton.tsx @@ -1,7 +1,7 @@ import { useTransition, animated } from '@react-spring/web'; import { Button, Icon } from 'react-basics'; -import { useTheme } from 'components/hooks'; -import Icons from 'components/icons'; +import { useTheme } from '@/components/hooks'; +import Icons from '@/components/icons'; import styles from './ThemeButton.module.css'; export function ThemeButton() { @@ -28,7 +28,7 @@ export function ThemeButton() { diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index 486f5de1..97beaf12 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -1,10 +1,10 @@ -import { useDateRange, useLocale } from 'components/hooks'; +import { useDateRange, useLocale } from '@/components/hooks'; import { isAfter } from 'date-fns'; -import { getOffsetDateRange } from 'lib/date'; +import { getOffsetDateRange } from '@/lib/date'; import { Button, Icon, Icons } from 'react-basics'; import DateFilter from './DateFilter'; import styles from './WebsiteDateFilter.module.css'; -import { DateRange } from 'lib/types'; +import { DateRange } from '@/lib/types'; export function WebsiteDateFilter({ websiteId, diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx index 0540ed38..ed78efc8 100644 --- a/src/components/input/WebsiteSelect.tsx +++ b/src/components/input/WebsiteSelect.tsx @@ -1,7 +1,7 @@ import { useState, Key } from 'react'; import { Dropdown, Item } from 'react-basics'; -import { useWebsite, useWebsites, useMessages } from 'components/hooks'; -import Empty from 'components/common/Empty'; +import { useWebsite, useWebsites, useMessages } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; import styles from './WebsiteSelect.module.css'; export function WebsiteSelect({ diff --git a/src/components/layout/MenuLayout.tsx b/src/components/layout/MenuLayout.tsx index 2edd1091..1465c062 100644 --- a/src/components/layout/MenuLayout.tsx +++ b/src/components/layout/MenuLayout.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; import { usePathname } from 'next/navigation'; -import SideNav from 'components/layout/SideNav'; +import SideNav from '@/components/layout/SideNav'; import styles from './MenuLayout.module.css'; export function MenuLayout({ items = [], children }: { items: any[]; children: ReactNode }) { diff --git a/src/components/layout/NavGroup.module.css b/src/components/layout/NavGroup.module.css index d827da86..4979210a 100644 --- a/src/components/layout/NavGroup.module.css +++ b/src/components/layout/NavGroup.module.css @@ -51,11 +51,6 @@ a.item { color: var(--base900); } -.item.disabled { - color: var(--base500) !important; - pointer-events: none; -} - .minimized .text, .minimized .header { display: none; diff --git a/src/components/layout/NavGroup.tsx b/src/components/layout/NavGroup.tsx index e95b61fa..723f9a7e 100644 --- a/src/components/layout/NavGroup.tsx +++ b/src/components/layout/NavGroup.tsx @@ -3,7 +3,7 @@ import { Icon, Text, TooltipPopup } from 'react-basics'; import classNames from 'classnames'; import { usePathname } from 'next/navigation'; import Link from 'next/link'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import styles from './NavGroup.module.css'; export interface NavGroupProps { diff --git a/src/components/layout/Page.tsx b/src/components/layout/Page.tsx index 83312d12..c06054b4 100644 --- a/src/components/layout/Page.tsx +++ b/src/components/layout/Page.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; import { Banner, Loading } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './Page.module.css'; export function Page({ diff --git a/src/components/messages.ts b/src/components/messages.ts index 688dd11d..5279e1b4 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -280,6 +280,23 @@ export const labels = defineMessages({ lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' }, firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' }, properties: { id: 'label.properties', defaultMessage: 'Properties' }, + channels: { id: 'label.channels', defaultMessage: 'Channels' }, + direct: { id: 'label.direct', defaultMessage: 'Direct' }, + referral: { id: 'label.referral', defaultMessage: 'Referral' }, + affiliate: { id: 'label.affiliate', defaultMessage: 'Affiliate' }, + email: { id: 'label.email', defaultMessage: 'Email' }, + sms: { id: 'label.sms', defaultMessage: 'SMS' }, + organicSearch: { id: 'label.organic-search', defaultMessage: 'Organic search' }, + organicSocial: { id: 'label.organic-social', defaultMessage: 'Organic social' }, + organicShopping: { id: 'label.organic-shopping', defaultMessage: 'Organic shopping' }, + organicVideo: { id: 'label.organic-video', defaultMessage: 'Organic video' }, + paidAds: { id: 'label.paid-ads', defaultMessage: 'Paid ads' }, + paidSearch: { id: 'label.paid-search', defaultMessage: 'Paid search' }, + paidSocial: { id: 'label.paid-social', defaultMessage: 'Paid social' }, + paidShopping: { id: 'label.paid-shopping', defaultMessage: 'Paid shopping' }, + paidVideo: { id: 'label.paid-video', defaultMessage: 'Paid video' }, + grouped: { id: 'label.grouped', defaultMessage: 'Grouped' }, + other: { id: 'label.other', defaultMessage: 'Other' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/ActiveUsers.module.css b/src/components/metrics/ActiveUsers.module.css index 5d0a4c7d..4a984725 100644 --- a/src/components/metrics/ActiveUsers.module.css +++ b/src/components/metrics/ActiveUsers.module.css @@ -10,8 +10,3 @@ font-size: var(--font-size-md); font-weight: 400; } - -.value { - font-weight: 600; - margin-inline-end: 4px; -} diff --git a/src/components/metrics/ActiveUsers.tsx b/src/components/metrics/ActiveUsers.tsx index 05d0fc1d..50c676ab 100644 --- a/src/components/metrics/ActiveUsers.tsx +++ b/src/components/metrics/ActiveUsers.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { StatusLight } from 'react-basics'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './ActiveUsers.module.css'; export function ActiveUsers({ @@ -24,7 +24,7 @@ export function ActiveUsers({ const count = useMemo(() => { if (websiteId) { - return data?.x || 0; + return data?.visitors || 0; } return value !== undefined ? value : 0; diff --git a/src/components/metrics/BrowsersTable.tsx b/src/components/metrics/BrowsersTable.tsx index d0cec124..500686b1 100644 --- a/src/components/metrics/BrowsersTable.tsx +++ b/src/components/metrics/BrowsersTable.tsx @@ -1,8 +1,8 @@ -import FilterLink from 'components/common/FilterLink'; -import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import FilterLink from '@/components/common/FilterLink'; +import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; export function BrowsersTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/ChannelsTable.tsx b/src/components/metrics/ChannelsTable.tsx new file mode 100644 index 00000000..c7b3906d --- /dev/null +++ b/src/components/metrics/ChannelsTable.tsx @@ -0,0 +1,22 @@ +import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; + +export function BrowsersTable(props: MetricsTableProps) { + const { formatMessage, labels } = useMessages(); + + const renderLabel = ({ x }) => { + return formatMessage(labels[x]); + }; + + return ( + + ); +} + +export default BrowsersTable; diff --git a/src/components/metrics/CitiesTable.tsx b/src/components/metrics/CitiesTable.tsx index fd628e7f..1e5fc735 100644 --- a/src/components/metrics/CitiesTable.tsx +++ b/src/components/metrics/CitiesTable.tsx @@ -1,8 +1,8 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { emptyFilter } from 'lib/filters'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; +import { emptyFilter } from '@/lib/filters'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; export function CitiesTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/CountriesTable.tsx b/src/components/metrics/CountriesTable.tsx index f4560ae3..cdd05115 100644 --- a/src/components/metrics/CountriesTable.tsx +++ b/src/components/metrics/CountriesTable.tsx @@ -1,8 +1,8 @@ -import FilterLink from 'components/common/FilterLink'; -import { useCountryNames } from 'components/hooks'; -import { useLocale, useMessages, useFormat } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { useCountryNames } from '@/components/hooks'; +import { useLocale, useMessages, useFormat } from '@/components/hooks'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function CountriesTable({ ...props }: MetricsTableProps) { const { locale } = useLocale(); diff --git a/src/components/metrics/DatePickerForm.tsx b/src/components/metrics/DatePickerForm.tsx index 892cd127..d1a5c7db 100644 --- a/src/components/metrics/DatePickerForm.tsx +++ b/src/components/metrics/DatePickerForm.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { Button, ButtonGroup, Calendar } from 'react-basics'; import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns'; -import { useLocale } from 'components/hooks'; -import { FILTER_DAY, FILTER_RANGE } from 'lib/constants'; -import { useMessages } from 'components/hooks'; +import { useLocale } from '@/components/hooks'; +import { FILTER_DAY, FILTER_RANGE } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; import styles from './DatePickerForm.module.css'; export function DatePickerForm({ diff --git a/src/components/metrics/DevicesTable.tsx b/src/components/metrics/DevicesTable.tsx index c25afe4f..ed327c33 100644 --- a/src/components/metrics/DevicesTable.tsx +++ b/src/components/metrics/DevicesTable.tsx @@ -1,8 +1,8 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; export function DevicesTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx index 2ba2caee..9655c4a4 100644 --- a/src/components/metrics/EventsChart.tsx +++ b/src/components/metrics/EventsChart.tsx @@ -1,8 +1,8 @@ import { colord } from 'colord'; -import BarChart from 'components/charts/BarChart'; -import { useDateRange, useLocale, useWebsiteEventsSeries } from 'components/hooks'; -import { renderDateLabels } from 'lib/charts'; -import { CHART_COLORS } from 'lib/constants'; +import BarChart from '@/components/charts/BarChart'; +import { useDateRange, useLocale, useWebsiteEventsSeries } from '@/components/hooks'; +import { renderDateLabels } from '@/lib/charts'; +import { CHART_COLORS } from '@/lib/constants'; import { useMemo } from 'react'; export interface EventsChartProps { diff --git a/src/components/metrics/EventsTable.tsx b/src/components/metrics/EventsTable.tsx index c90ae988..bc753b3b 100644 --- a/src/components/metrics/EventsTable.tsx +++ b/src/components/metrics/EventsTable.tsx @@ -1,5 +1,5 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function EventsTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/FilterTags.tsx b/src/components/metrics/FilterTags.tsx index 60cf90c1..fcba3c9e 100644 --- a/src/components/metrics/FilterTags.tsx +++ b/src/components/metrics/FilterTags.tsx @@ -7,13 +7,13 @@ import { useMessages, useFormat, useFilters, -} from 'components/hooks'; -import PopupForm from 'app/(main)/reports/[reportId]/PopupForm'; -import FieldFilterEditForm from 'app/(main)/reports/[reportId]/FieldFilterEditForm'; -import { OPERATOR_PREFIXES } from 'lib/constants'; -import { isSearchOperator, parseParameterValue } from 'lib/params'; +} from '@/components/hooks'; +import PopupForm from '@/app/(main)/reports/[reportId]/PopupForm'; +import FieldFilterEditForm from '@/app/(main)/reports/[reportId]/FieldFilterEditForm'; +import { OPERATOR_PREFIXES } from '@/lib/constants'; +import { isSearchOperator, parseParameterValue } from '@/lib/params'; import styles from './FilterTags.module.css'; -import WebsiteFilterButton from 'app/(main)/websites/[websiteId]/WebsiteFilterButton'; +import WebsiteFilterButton from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; export function FilterTags({ websiteId, diff --git a/src/components/metrics/HostsTable.tsx b/src/components/metrics/HostsTable.tsx index 45147eac..e034b970 100644 --- a/src/components/metrics/HostsTable.tsx +++ b/src/components/metrics/HostsTable.tsx @@ -1,6 +1,6 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; import { Flexbox } from 'react-basics'; export function HostsTable(props: MetricsTableProps) { diff --git a/src/components/metrics/LanguagesTable.tsx b/src/components/metrics/LanguagesTable.tsx index 24b62046..3ced249e 100644 --- a/src/components/metrics/LanguagesTable.tsx +++ b/src/components/metrics/LanguagesTable.tsx @@ -1,8 +1,8 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { percentFilter } from 'lib/filters'; -import { useLocale } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; +import { percentFilter } from '@/lib/filters'; +import { useLocale } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; export function LanguagesTable({ onDataLoad, diff --git a/src/components/metrics/Legend.tsx b/src/components/metrics/Legend.tsx index 4ebcf4b4..77442957 100644 --- a/src/components/metrics/Legend.tsx +++ b/src/components/metrics/Legend.tsx @@ -1,5 +1,4 @@ import { StatusLight } from 'react-basics'; -import { safeDecodeURIComponent } from 'next-basics'; import { colord } from 'colord'; import classNames from 'classnames'; import { LegendItem } from 'chart.js/auto'; @@ -28,9 +27,7 @@ export function Legend({ className={classNames(styles.label, { [styles.hidden]: hidden })} onClick={() => onClick(item)} > - - {safeDecodeURIComponent(text)} - + {text} ); })} diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx index 59ded491..6fbf390a 100644 --- a/src/components/metrics/ListTable.tsx +++ b/src/components/metrics/ListTable.tsx @@ -1,9 +1,9 @@ import { FixedSizeList } from 'react-window'; import { useSpring, animated, config } from '@react-spring/web'; import classNames from 'classnames'; -import Empty from 'components/common/Empty'; -import { formatLongNumber } from 'lib/format'; -import { useMessages } from 'components/hooks'; +import Empty from '@/components/common/Empty'; +import { formatLongNumber } from '@/lib/format'; +import { useMessages } from '@/components/hooks'; import styles from './ListTable.module.css'; import { ReactNode } from 'react'; diff --git a/src/components/metrics/MetricCard.tsx b/src/components/metrics/MetricCard.tsx index 64f2a1b6..41766167 100644 --- a/src/components/metrics/MetricCard.tsx +++ b/src/components/metrics/MetricCard.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import { useSpring, animated } from '@react-spring/web'; -import { formatNumber } from 'lib/format'; -import ChangeLabel from 'components/metrics/ChangeLabel'; +import { formatNumber } from '@/lib/format'; +import ChangeLabel from '@/components/metrics/ChangeLabel'; import styles from './MetricCard.module.css'; export interface MetricCardProps { diff --git a/src/components/metrics/MetricsBar.tsx b/src/components/metrics/MetricsBar.tsx index 60a21706..6e9f22de 100644 --- a/src/components/metrics/MetricsBar.tsx +++ b/src/components/metrics/MetricsBar.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; import { Loading, cloneChildren } from 'react-basics'; -import ErrorMessage from 'components/common/ErrorMessage'; -import { formatLongNumber } from 'lib/format'; +import ErrorMessage from '@/components/common/ErrorMessage'; +import { formatLongNumber } from '@/lib/format'; import styles from './MetricsBar.module.css'; export interface MetricsBarProps { diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 4db599b9..33b457b5 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -1,18 +1,18 @@ import { ReactNode, useMemo, useState } from 'react'; import { Loading, Icon, Text, SearchField } from 'react-basics'; import classNames from 'classnames'; -import ErrorMessage from 'components/common/ErrorMessage'; -import LinkButton from 'components/common/LinkButton'; -import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; -import { percentFilter } from 'lib/filters'; +import ErrorMessage from '@/components/common/ErrorMessage'; +import LinkButton from '@/components/common/LinkButton'; +import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants'; +import { percentFilter } from '@/lib/filters'; import { useNavigation, useWebsiteMetrics, useMessages, useLocale, useFormat, -} from 'components/hooks'; -import Icons from 'components/icons'; +} from '@/components/hooks'; +import Icons from '@/components/icons'; import ListTable, { ListTableProps } from './ListTable'; import styles from './MetricsTable.module.css'; diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx index 6989504c..37b79549 100644 --- a/src/components/metrics/OSTable.tsx +++ b/src/components/metrics/OSTable.tsx @@ -1,7 +1,7 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages, useFormat } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages, useFormat } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; export function OSTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx index b2d8ca9c..8163b3d9 100644 --- a/src/components/metrics/PagesTable.tsx +++ b/src/components/metrics/PagesTable.tsx @@ -1,8 +1,8 @@ -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; -import FilterButtons from 'components/common/FilterButtons'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages, useNavigation } from 'components/hooks'; -import { emptyFilter } from 'lib/filters'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import FilterButtons from '@/components/common/FilterButtons'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages, useNavigation } from '@/components/hooks'; +import { emptyFilter } from '@/lib/filters'; import { useContext } from 'react'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; diff --git a/src/components/metrics/PageviewsChart.tsx b/src/components/metrics/PageviewsChart.tsx index 6274defc..6fa3285f 100644 --- a/src/components/metrics/PageviewsChart.tsx +++ b/src/components/metrics/PageviewsChart.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import BarChart, { BarChartProps } from 'components/charts/BarChart'; -import { useLocale, useTheme, useMessages } from 'components/hooks'; -import { renderDateLabels } from 'lib/charts'; +import BarChart, { BarChartProps } from '@/components/charts/BarChart'; +import { useLocale, useTheme, useMessages } from '@/components/hooks'; +import { renderDateLabels } from '@/lib/charts'; export interface PagepageviewsChartProps extends BarChartProps { data: { diff --git a/src/components/metrics/QueryParametersTable.tsx b/src/components/metrics/QueryParametersTable.tsx index f0d08ecf..26f01faf 100644 --- a/src/components/metrics/QueryParametersTable.tsx +++ b/src/components/metrics/QueryParametersTable.tsx @@ -1,10 +1,9 @@ import { useState } from 'react'; -import { safeDecodeURI } from 'next-basics'; -import FilterButtons from 'components/common/FilterButtons'; -import { emptyFilter, paramFilter } from 'lib/filters'; -import { FILTER_RAW, FILTER_COMBINED } from 'lib/constants'; +import FilterButtons from '@/components/common/FilterButtons'; +import { emptyFilter, paramFilter } from '@/lib/filters'; +import { FILTER_RAW, FILTER_COMBINED } from '@/lib/constants'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './QueryParametersTable.module.css'; const filters = { @@ -39,8 +38,8 @@ export function QueryParametersTable({ x ) : (
-
{safeDecodeURI(p)}
-
{safeDecodeURI(v)}
+
{p}
+
{v}
) } diff --git a/src/components/metrics/RealtimeChart.tsx b/src/components/metrics/RealtimeChart.tsx index b2819f9c..f5697caa 100644 --- a/src/components/metrics/RealtimeChart.tsx +++ b/src/components/metrics/RealtimeChart.tsx @@ -1,8 +1,8 @@ import { useMemo, useRef } from 'react'; import { startOfMinute, subMinutes, isBefore } from 'date-fns'; import PageviewsChart from './PageviewsChart'; -import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants'; -import { RealtimeData } from 'lib/types'; +import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from '@/lib/constants'; +import { RealtimeData } from '@/lib/types'; export interface RealtimeChartProps { data: RealtimeData; diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx index d83c4d12..142f361b 100644 --- a/src/components/metrics/ReferrersTable.tsx +++ b/src/components/metrics/ReferrersTable.tsx @@ -1,12 +1,53 @@ -import FilterLink from 'components/common/FilterLink'; -import Favicon from 'components/common/Favicon'; -import { useMessages } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import Favicon from '@/components/common/Favicon'; +import { useMessages, useNavigation } from '@/components/hooks'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; +import FilterButtons from '@/components/common/FilterButtons'; +import thenby from 'thenby'; +import { GROUPED_DOMAINS } from '@/lib/constants'; +import { Flexbox } from 'react-basics'; -export function ReferrersTable(props: MetricsTableProps) { +export interface ReferrersTableProps extends MetricsTableProps { + allowFilter?: boolean; +} + +export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) { + const { + router, + renderUrl, + query: { view = 'referrer' }, + } = useNavigation(); const { formatMessage, labels } = useMessages(); + const handleSelect = (key: any) => { + router.push(renderUrl({ view: key }), { scroll: false }); + }; + + const buttons = [ + { + label: formatMessage(labels.domain), + key: 'referrer', + }, + { + label: formatMessage(labels.grouped), + key: 'grouped', + }, + ]; + const renderLink = ({ x: referrer }) => { + if (view === 'grouped') { + if (referrer === '_other') { + return `(${formatMessage(labels.other)})`; + } else { + return ( + + + {GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name} + + ); + } + } + return ( { + const groups = { _other: 0 }; + + for (const { x, y } of data) { + for (const { domain, match } of GROUPED_DOMAINS) { + if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) { + if (!groups[domain]) { + groups[domain] = 0; + } + groups[domain] += y; + } else { + groups._other += y; + } + } + } + + return Object.keys(groups) + .map((key: any) => ({ x: key, y: groups[key] })) + .sort(thenby.firstBy('y', -1)); + }; + return ( <> + > + {allowFilter && ( + + )} + ); } diff --git a/src/components/metrics/RegionsTable.tsx b/src/components/metrics/RegionsTable.tsx index 0c3a931f..0b7e3bdf 100644 --- a/src/components/metrics/RegionsTable.tsx +++ b/src/components/metrics/RegionsTable.tsx @@ -1,8 +1,8 @@ -import FilterLink from 'components/common/FilterLink'; -import { emptyFilter } from 'lib/filters'; -import { useMessages, useLocale, useRegionNames } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { emptyFilter } from '@/lib/filters'; +import { useMessages, useLocale, useRegionNames } from '@/components/hooks'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function RegionsTable(props: MetricsTableProps) { const { locale } = useLocale(); diff --git a/src/components/metrics/ScreenTable.tsx b/src/components/metrics/ScreenTable.tsx index 51015fcb..c2a19caa 100644 --- a/src/components/metrics/ScreenTable.tsx +++ b/src/components/metrics/ScreenTable.tsx @@ -1,5 +1,5 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function ScreenTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/TagsTable.tsx b/src/components/metrics/TagsTable.tsx index a1130bb4..e915f873 100644 --- a/src/components/metrics/TagsTable.tsx +++ b/src/components/metrics/TagsTable.tsx @@ -1,6 +1,6 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; import { Flexbox } from 'react-basics'; export function TagsTable(props: MetricsTableProps) { diff --git a/src/components/metrics/WorldMap.tsx b/src/components/metrics/WorldMap.tsx index 5dfc5f74..a377bfc9 100644 --- a/src/components/metrics/WorldMap.tsx +++ b/src/components/metrics/WorldMap.tsx @@ -2,14 +2,14 @@ import { useState, useMemo, HTMLAttributes } from 'react'; import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; import classNames from 'classnames'; import { colord } from 'colord'; -import HoverTooltip from 'components/common/HoverTooltip'; -import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants'; -import { useDateRange, useTheme, useWebsiteMetrics } from 'components/hooks'; -import { useCountryNames } from 'components/hooks'; -import { useLocale } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import { formatLongNumber } from 'lib/format'; -import { percentFilter } from 'lib/filters'; +import HoverTooltip from '@/components/common/HoverTooltip'; +import { ISO_COUNTRIES, MAP_FILE } from '@/lib/constants'; +import { useDateRange, useTheme, useWebsiteMetrics } from '@/components/hooks'; +import { useCountryNames } from '@/components/hooks'; +import { useLocale } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; +import { formatLongNumber } from '@/lib/format'; +import { percentFilter } from '@/lib/filters'; import styles from './WorldMap.module.css'; export function WorldMap({ diff --git a/src/declaration.d.ts b/src/declaration.d.ts index 986adf27..7dff68b8 100644 --- a/src/declaration.d.ts +++ b/src/declaration.d.ts @@ -1,5 +1,6 @@ +declare module 'bcryptjs'; +declare module 'chartjs-adapter-date-fns'; declare module 'cors'; declare module 'debug'; -declare module 'chartjs-adapter-date-fns'; +declare module 'jsonwebtoken'; declare module 'md5'; -declare module 'request-ip'; diff --git a/src/index.ts b/src/index.ts index 553a44b5..e7b0e6c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,64 +1,64 @@ -export * from 'components/hooks'; +export * from '@/components/hooks'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMembersTable'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMembersTable'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamDeleteForm'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamDetails'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamEditForm'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamManage'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamDetails'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamEditForm'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamManage'; -export * from 'app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton'; -export * from 'app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable'; -export * from 'app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable'; +export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton'; +export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable'; +export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable'; -export * from 'app/(main)/settings/teams/TeamAddForm'; -export * from 'app/(main)/settings/teams/TeamJoinForm'; -export * from 'app/(main)/settings/teams/TeamLeaveButton'; -export * from 'app/(main)/settings/teams/TeamLeaveForm'; -export * from 'app/(main)/settings/teams/TeamsAddButton'; -export * from 'app/(main)/settings/teams/TeamsDataTable'; -export * from 'app/(main)/settings/teams/TeamsHeader'; -export * from 'app/(main)/settings/teams/TeamsJoinButton'; -export * from 'app/(main)/settings/teams/TeamsTable'; -export * from 'app/(main)/settings/teams/WebsiteTags'; +export * from '@/app/(main)/settings/teams/TeamAddForm'; +export * from '@/app/(main)/settings/teams/TeamJoinForm'; +export * from '@/app/(main)/settings/teams/TeamLeaveButton'; +export * from '@/app/(main)/settings/teams/TeamLeaveForm'; +export * from '@/app/(main)/settings/teams/TeamsAddButton'; +export * from '@/app/(main)/settings/teams/TeamsDataTable'; +export * from '@/app/(main)/settings/teams/TeamsHeader'; +export * from '@/app/(main)/settings/teams/TeamsJoinButton'; +export * from '@/app/(main)/settings/teams/TeamsTable'; +export * from '@/app/(main)/settings/teams/WebsiteTags'; -export * from 'app/(main)/settings/websites/[websiteId]/ShareUrl'; -export * from 'app/(main)/settings/websites/[websiteId]/TrackingCode'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteData'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteEditForm'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteResetForm'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteSettings'; +export * from '@/app/(main)/settings/websites/[websiteId]/ShareUrl'; +export * from '@/app/(main)/settings/websites/[websiteId]/TrackingCode'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteData'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteEditForm'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteResetForm'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettings'; -export * from 'app/(main)/settings/websites/WebsiteAddButton'; -export * from 'app/(main)/settings/websites/WebsiteAddForm'; -export * from 'app/(main)/settings/websites/WebsitesDataTable'; -export * from 'app/(main)/settings/websites/WebsitesHeader'; -export * from 'app/(main)/settings/websites/WebsitesTable'; +export * from '@/app/(main)/settings/websites/WebsiteAddButton'; +export * from '@/app/(main)/settings/websites/WebsiteAddForm'; +export * from '@/app/(main)/settings/websites/WebsitesDataTable'; +export * from '@/app/(main)/settings/websites/WebsitesHeader'; +export * from '@/app/(main)/settings/websites/WebsitesTable'; -export * from 'app/(main)/teams/[teamId]/TeamProvider'; -export * from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +export * from '@/app/(main)/teams/[teamId]/TeamProvider'; +export * from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; -export * from 'components/common/ConfirmationForm'; -export * from 'components/common/DataTable'; -export * from 'components/common/Empty'; -export * from 'components/common/ErrorBoundary'; -export * from 'components/common/ErrorMessage'; -export * from 'components/common/Favicon'; -export * from 'components/common/FilterButtons'; -export * from 'components/common/FilterLink'; -export * from 'components/common/HamburgerButton'; -export * from 'components/common/HoverTooltip'; -export * from 'components/common/LinkButton'; -export * from 'components/common/MobileMenu'; -export * from 'components/common/Pager'; -export * from 'components/common/TypeConfirmationForm'; +export * from '@/components/common/ConfirmationForm'; +export * from '@/components/common/DataTable'; +export * from '@/components/common/Empty'; +export * from '@/components/common/ErrorBoundary'; +export * from '@/components/common/ErrorMessage'; +export * from '@/components/common/Favicon'; +export * from '@/components/common/FilterButtons'; +export * from '@/components/common/FilterLink'; +export * from '@/components/common/HamburgerButton'; +export * from '@/components/common/HoverTooltip'; +export * from '@/components/common/LinkButton'; +export * from '@/components/common/MobileMenu'; +export * from '@/components/common/Pager'; +export * from '@/components/common/TypeConfirmationForm'; -export * from 'components/input/TeamsButton'; -export * from 'components/input/ThemeButton'; +export * from '@/components/input/TeamsButton'; +export * from '@/components/input/ThemeButton'; -export { ROLES } from 'lib/constants'; +export { ROLES } from '@/lib/constants'; diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts index 14c67dde..1cb558ad 100644 --- a/src/lib/__tests__/detect.test.ts +++ b/src/lib/__tests__/detect.test.ts @@ -6,17 +6,17 @@ const IP = '127.0.0.1'; test('getIpAddress: Custom header', () => { process.env.CLIENT_IP_HEADER = 'x-custom-ip-header'; - expect(detect.getIpAddress({ headers: { 'x-custom-ip-header': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP); }); test('getIpAddress: CloudFlare header', () => { - expect(detect.getIpAddress({ headers: { 'cf-connecting-ip': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP); }); test('getIpAddress: Standard header', () => { - expect(detect.getIpAddress({ headers: { 'x-forwarded-for': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP); }); test('getIpAddress: No header', () => { - expect(detect.getIpAddress({ headers: {} } as any)).toEqual(null); + expect(detect.getIpAddress(new Headers())).toEqual(null); }); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 7b8ac823..4ce90706 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,15 +1,67 @@ +import bcrypt from 'bcryptjs'; import { Report } from '@prisma/client'; -import { getClient } from '@umami/redis-client'; +import { getClient, redisEnabled } from '@umami/redis-client'; import debug from 'debug'; -import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; -import { secret } from 'lib/crypto'; -import { NextApiRequest } from 'next'; -import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics'; -import { getTeamUser, getWebsite } from 'queries'; +import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants'; +import { secret, getRandomChars } from '@/lib/crypto'; +import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt'; +import { ensureArray } from '@/lib/utils'; +import { getTeamUser, getUser, getWebsite } from '@/queries'; import { Auth } from './types'; const log = debug('umami:auth'); const cloudMode = process.env.CLOUD_MODE; +const SALT_ROUNDS = 10; + +export function hashPassword(password: string, rounds = SALT_ROUNDS) { + return bcrypt.hashSync(password, rounds); +} + +export function checkPassword(password: string, passwordHash: string) { + return bcrypt.compareSync(password, passwordHash); +} + +export async function checkAuth(request: Request) { + const token = request.headers.get('authorization')?.split(' ')?.[1]; + const payload = parseSecureToken(token, secret()); + const shareToken = await parseShareToken(request.headers); + + let user = null; + const { userId, authKey, grant } = payload || {}; + + if (userId) { + user = await getUser(userId); + } else if (redisEnabled && authKey) { + const redis = getClient(); + + const key = await redis.get(authKey); + + if (key?.userId) { + user = await getUser(key.userId); + } + } + + if (process.env.NODE_ENV === 'development') { + log('checkAuth:', { token, shareToken, payload, user, grant }); + } + + if (!user?.id && !shareToken) { + log('checkAuth: User not authorized'); + return null; + } + + if (user) { + user.isAdmin = user.role === ROLES.admin; + } + + return { + user, + grant, + token, + shareToken, + authKey, + }; +} export async function saveAuth(data: any, expire = 0) { const authKey = `auth:${getRandomChars(32)}`; @@ -25,17 +77,9 @@ export async function saveAuth(data: any, expire = 0) { return createSecureToken({ authKey }, secret()); } -export function getAuthToken(req: NextApiRequest) { +export function parseShareToken(headers: Headers) { try { - return req.headers.authorization.split(' ')[1]; - } catch { - return null; - } -} - -export function parseShareToken(req: Request) { - try { - return parseToken(req.headers[SHARE_TOKEN_HEADER], secret()); + return parseToken(headers.get(SHARE_TOKEN_HEADER), secret()); } catch (e) { log(e); return null; diff --git a/src/lib/charts.ts b/src/lib/charts.ts index 8939b3c1..d805eefe 100644 --- a/src/lib/charts.ts +++ b/src/lib/charts.ts @@ -1,5 +1,5 @@ -import { formatDate } from 'lib/date'; -import { formatLongNumber } from 'lib/format'; +import { formatDate } from '@/lib/date'; +import { formatLongNumber } from '@/lib/format'; export function renderNumberLabels(label: string) { return +label > 1000 ? formatLongNumber(+label) : label; diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 5f0248b4..13abde9d 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -1,8 +1,8 @@ import { ClickHouseClient, createClient } from '@clickhouse/client'; import { formatInTimeZone } from 'date-fns-tz'; import debug from 'debug'; -import { CLICKHOUSE } from 'lib/db'; -import { getWebsite } from 'queries/index'; +import { CLICKHOUSE } from '@/lib/db'; +import { getWebsite } from '@/queries/index'; import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants'; import { maxDate } from './date'; import { filtersToArray } from './params'; diff --git a/src/lib/client.ts b/src/lib/client.ts index 7810c44a..795e7780 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -1,4 +1,4 @@ -import { getItem, setItem, removeItem } from 'next-basics'; +import { getItem, setItem, removeItem } from '@/lib/storage'; import { AUTH_TOKEN } from './constants'; export function getClientAuthToken() { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a9e13c14..545f86c8 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -324,6 +324,104 @@ 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 SOCIAL_DOMAINS = [ + 'facebook.com', + 'fb.com', + 'instagram.com', + 'ig.com', + 'twitter.com', + 't.co', + 'x.com', + 'linkedin.', + 'tiktok.', + 'reddit.', + 'threads.net', + 'bsky.app', + 'news.ycombinator.com', + 'snapchat.', + 'pinterest.', +]; + +export const SEARCH_DOMAINS = [ + 'google.', + 'bing.com', + 'msn.com', + 'duckduckgo.com', + 'search.brave.com', + 'yandex.', + 'baidu.com', + 'ecosia.org', + 'chatgpt.com', + 'perplexity.ai', +]; + +export const SHOPPING_DOMAINS = [ + 'amazon.', + 'ebay.com', + 'walmart.com', + 'alibab.com', + 'aliexpress.com', + 'etsy.com', + 'bestbuy.com', + 'target.com', + 'newegg.com', +]; + +export const EMAIL_DOMAINS = [ + 'gmail.', + 'mail.yahoo.', + 'outlook.', + 'hotmail.', + 'protonmail.', + 'proton.me', +]; + +export const VIDEO_DOMAINS = ['youtube.', 'twitch.']; + +export const PAID_AD_PARAMS = [ + 'utm_source=google', + 'gclid=', + 'fbclid=', + 'msclkid=', + 'dclid=', + 'twclid=', + 'li_fat_id=', + 'epik=', + 'ttclid=', + 'scid=', +]; + +export const GROUPED_DOMAINS = [ + { name: 'Google', domain: 'google.com', match: 'google.' }, + { name: 'Facebook', domain: 'facebook.com', match: 'facebook.' }, + { name: 'Reddit', domain: 'reddit.com', match: 'reddit.' }, + { name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' }, + { name: 'GitHub', domain: 'github.com', match: 'github.' }, + { name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' }, + { name: 'Bing', domain: 'bing.com', match: 'bing.' }, + { name: 'Brave', domain: 'brave.com', match: 'brave.' }, + { name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' }, + { name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] }, + { name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] }, + { name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' }, + { name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' }, + { name: 'ChatGPT', domain: 'chatgpt.com', match: 'chatgpt.' }, +]; + export const MAP_FILE = '/datamaps.world.json'; export const ISO_COUNTRIES = { diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 689efe62..a4ff3a52 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,7 +1,78 @@ +import crypto from 'crypto'; import { startOfHour, startOfMonth } from 'date-fns'; -import { hash } from 'next-basics'; +import prand from 'pure-rand'; import { v4, v5 } from 'uuid'; +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 16; +const SALT_LENGTH = 64; +const TAG_LENGTH = 16; +const TAG_POSITION = SALT_LENGTH + IV_LENGTH; +const ENC_POSITION = TAG_POSITION + TAG_LENGTH; + +const HASH_ALGO = 'sha512'; +const HASH_ENCODING = 'hex'; + +const seed = Date.now() ^ (Math.random() * 0x100000000); +const rng = prand.xoroshiro128plus(seed); + +export function random(min: number, max: number) { + return prand.unsafeUniformIntDistribution(min, max, rng); +} + +export function getRandomChars( + n: number, + chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', +) { + const arr = chars.split(''); + let s = ''; + for (let i = 0; i < n; i++) { + s += arr[random(0, arr.length - 1)]; + } + return s; +} + +const getKey = (password: string, salt: Buffer) => + crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha512'); + +export function encrypt(value: any, secret: any) { + const iv = crypto.randomBytes(IV_LENGTH); + const salt = crypto.randomBytes(SALT_LENGTH); + const key = getKey(secret, salt); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + const encrypted = Buffer.concat([cipher.update(String(value), 'utf8'), cipher.final()]); + + const tag = cipher.getAuthTag(); + + return Buffer.concat([salt, iv, tag, encrypted]).toString('base64'); +} + +export function decrypt(value: any, secret: any) { + const str = Buffer.from(String(value), 'base64'); + const salt = str.subarray(0, SALT_LENGTH); + const iv = str.subarray(SALT_LENGTH, TAG_POSITION); + const tag = str.subarray(TAG_POSITION, ENC_POSITION); + const encrypted = str.subarray(ENC_POSITION); + + const key = getKey(secret, salt); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + + decipher.setAuthTag(tag); + + return decipher.update(encrypted) + decipher.final('utf8'); +} + +export function hash(...args: string[]) { + return crypto.createHash(HASH_ALGO).update(args.join('')).digest(HASH_ENCODING); +} + +export function md5(...args: string[]) { + return crypto.createHash('md5').update(args.join('')).digest('hex'); +} + export function secret() { return hash(process.env.APP_SECRET || process.env.DATABASE_URL); } diff --git a/src/lib/date.ts b/src/lib/date.ts index b7755ffc..96135845 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -35,8 +35,8 @@ import { endOfMinute, isSameDay, } from 'date-fns'; -import { getDateLocale } from 'lib/lang'; -import { DateRange } from 'lib/types'; +import { getDateLocale } from '@/lib/lang'; +import { DateRange } from '@/lib/types'; export const TIME_UNIT = { minute: 'minute', diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 0bea4403..cd91069e 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; +const MAXMIND = 'maxmind'; -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,46 +86,48 @@ 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 - if (!lookup) { + if (!global[MAXMIND]) { const dir = path.join(process.cwd(), 'geo'); - lookup = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb')); + global[MAXMIND] = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb')); } - const result = lookup.get(ip); + const result = global[MAXMIND].get(ip); if (result) { const country = result.country?.iso_code ?? result?.registered_country?.iso_code; @@ -131,22 +144,22 @@ export async function getLocation(ip: string, req: NextApiRequestCollect) { } } -export async function getClientInfo(req: NextApiRequestCollect) { - const userAgent = req.body?.payload?.userAgent || req.headers['user-agent']; - const ip = req.body?.payload?.ip || getIpAddress(req); - const location = await getLocation(ip, req); - const country = location?.country; +export async function getClientInfo(request: Request, payload: Record) { + const userAgent = payload?.userAgent || request.headers.get('user-agent'); + const ip = payload?.ip || getIpAddress(request.headers); + const location = await getLocation(ip, request.headers); + const country = payload?.userAgent || location?.country; const subdivision1 = location?.subdivision1; const subdivision2 = location?.subdivision2; const city = location?.city; const browser = browserName(userAgent); const os = detectOS(userAgent) as string; - const device = getDevice(req.body?.payload?.screen, os); + const device = getDevice(payload?.screen, os); return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device }; } -export function hasBlockedIp(req: NextApiRequestCollect) { +export function hasBlockedIp(clientIp: string) { const ignoreIps = process.env.IGNORE_IP; if (ignoreIps) { @@ -156,17 +169,19 @@ export function hasBlockedIp(req: NextApiRequestCollect) { ips.push(...ignoreIps.split(',').map(n => n.trim())); } - const clientIp = getIpAddress(req); - return ips.find(ip => { - if (ip === clientIp) return true; + if (ip === clientIp) { + return true; + } // CIDR notation if (ip.indexOf('/') > 0) { const addr = ipaddr.parse(clientIp); const range = ipaddr.parseCIDR(ip); - if (addr.kind() === range[0].kind() && addr.match(range)) return true; + if (addr.kind() === range[0].kind() && addr.match(range)) { + return true; + } } }); } diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts new file mode 100644 index 00000000..754da56f --- /dev/null +++ b/src/lib/fetch.ts @@ -0,0 +1,30 @@ +import { buildUrl } from '@/lib/url'; + +export async function request(method: string, url: string, body?: string, headers: object = {}) { + return fetch(url, { + method, + cache: 'no-cache', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...headers, + }, + body, + }).then(res => res.json()); +} + +export async function httpGet(url: string, params: object = {}, headers: object = {}) { + return request('GET', buildUrl(url, params), undefined, headers); +} + +export async function httpDelete(url: string, params: object = {}, headers: object = {}) { + return request('DELETE', buildUrl(url, params), undefined, headers); +} + +export async function httpPost(url: string, params: object = {}, headers: object = {}) { + return request('POST', url, JSON.stringify(params), headers); +} + +export async function httpPut(url: string, params: object = {}, headers: object = {}) { + return request('PUT', url, JSON.stringify(params), headers); +} diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts new file mode 100644 index 00000000..470c48ff --- /dev/null +++ b/src/lib/jwt.ts @@ -0,0 +1,36 @@ +import jwt from 'jsonwebtoken'; +import { decrypt, encrypt } from '@/lib/crypto'; + +export function createToken(payload: any, secret: any, options?: any) { + return jwt.sign(payload, secret, options); +} + +export function parseToken(token: string, secret: any) { + try { + return jwt.verify(token, secret); + } catch { + return null; + } +} + +export function createSecureToken(payload: any, secret: any, options?: any) { + return encrypt(createToken(payload, secret, options), secret); +} + +export function parseSecureToken(token: string, secret: any) { + try { + return jwt.verify(decrypt(token, secret), secret); + } catch { + return null; + } +} + +export async function parseAuthToken(req: Request, secret: string) { + try { + const token = req.headers.get('authorization')?.split(' ')?.[1]; + + return parseSecureToken(token as string, secret); + } catch { + return null; + } +} diff --git a/src/lib/kafka.ts b/src/lib/kafka.ts index 2e875c59..e7f06910 100644 --- a/src/lib/kafka.ts +++ b/src/lib/kafka.ts @@ -1,7 +1,7 @@ import { serializeError } from 'serialize-error'; import debug from 'debug'; import { Kafka, Producer, RecordMetadata, SASLOptions, logLevel } from 'kafkajs'; -import { KAFKA, KAFKA_PRODUCER } from 'lib/db'; +import { KAFKA, KAFKA_PRODUCER } from '@/lib/db'; import * as tls from 'tls'; const log = debug('umami:kafka'); diff --git a/src/lib/load.ts b/src/lib/load.ts index 3650f233..2b7f7de4 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/params.ts b/src/lib/params.ts index ef4568ba..8e631ed8 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -1,5 +1,5 @@ -import { FILTER_COLUMNS, OPERATOR_PREFIXES, OPERATORS } from 'lib/constants'; -import { QueryFilters, QueryOptions } from 'lib/types'; +import { FILTER_COLUMNS, OPERATOR_PREFIXES, OPERATORS } from '@/lib/constants'; +import { QueryFilters, QueryOptions } from '@/lib/types'; export function parseParameterValue(param: any) { if (typeof param === 'string') { diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index cc1b8734..6c0237d0 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,7 +1,7 @@ import debug from 'debug'; import prisma from '@umami/prisma-client'; import { formatInTimeZone } from 'date-fns-tz'; -import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; +import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db'; import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; import { fetchWebsite } from './load'; import { maxDate } from './date'; @@ -243,7 +243,7 @@ async function pagedQuery(model: string, criteria: T, pageParams: PageParams) const data = await prisma.client[model].findMany({ ...criteria, ...{ - ...(size > 0 && { take: +size, skip: +size * (page - 1) }), + ...(size > 0 && { take: +size, skip: +size * (+page - 1) }), ...(orderBy && { orderBy: [ { @@ -266,7 +266,7 @@ async function pagedRawQuery( ) { const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams; const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (page - 1); + const offset = +size * (+page - 1); const direction = sortDescending ? 'desc' : 'asc'; const statements = [ diff --git a/src/lib/request.ts b/src/lib/request.ts index 5e2be2fe..9d32f89b 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,10 +1,55 @@ -import { NextApiRequest } from 'next'; -import { getAllowedUnits, getMinimumUnit } from './date'; -import { getWebsiteDateRange } from '../queries'; -import { FILTER_COLUMNS } from 'lib/constants'; +import { ZodObject } from 'zod'; +import { FILTER_COLUMNS } from '@/lib/constants'; +import { badRequest, unauthorized } from '@/lib/response'; +import { getAllowedUnits, getMinimumUnit } from '@/lib/date'; +import { checkAuth } from '@/lib/auth'; +import { getWebsiteDateRange } from '@/queries'; -export async function getRequestDateRange(req: NextApiRequest) { - const { websiteId, startAt, endAt, unit } = req.query; +export async function getJsonBody(request: Request) { + try { + return await request.clone().json(); + } catch { + return undefined; + } +} + +export async function parseRequest( + request: Request, + schema?: ZodObject, + options?: { skipAuth: boolean }, +): Promise { + const url = new URL(request.url); + let query = Object.fromEntries(url.searchParams); + let body = await getJsonBody(request); + let error: () => void | undefined; + let auth = null; + + if (schema) { + const isGet = request.method === 'GET'; + const result = schema.safeParse(isGet ? query : body); + + if (!result.success) { + error = () => badRequest(result.error); + } else if (isGet) { + query = result.data; + } else { + body = result.data; + } + } + + if (!options?.skipAuth && !error) { + auth = await checkAuth(request); + + if (!auth) { + error = () => unauthorized(); + } + } + + return { url, query, body, auth, error }; +} + +export async function getRequestDateRange(query: Record) { + const { websiteId, startAt, endAt, unit } = query; // All-time if (+startAt === 0 && +endAt === 1) { @@ -31,9 +76,9 @@ export async function getRequestDateRange(req: NextApiRequest) { }; } -export function getRequestFilters(req: NextApiRequest) { +export function getRequestFilters(query: Record) { return Object.keys(FILTER_COLUMNS).reduce((obj, key) => { - const value = req.query[key]; + const value = query[key]; if (value !== undefined) { obj[key] = value; diff --git a/src/lib/response.ts b/src/lib/response.ts new file mode 100644 index 00000000..d50b453c --- /dev/null +++ b/src/lib/response.ts @@ -0,0 +1,29 @@ +import { serializeError } from 'serialize-error'; + +export function ok() { + return Response.json({ ok: true }); +} + +export function json(data: any) { + return Response.json(data); +} + +export function badRequest(error: any = 'Bad request') { + return Response.json({ error: serializeError(error) }, { status: 400 }); +} + +export function unauthorized(error: any = 'Unauthorized') { + return Response.json({ error: serializeError(error) }, { status: 401 }); +} + +export function forbidden(error: any = 'Forbidden') { + return Response.json({ error: serializeError(error) }, { status: 403 }); +} + +export function notFound(error: any = 'Not found') { + return Response.json({ error: serializeError(error) }, { status: 404 }); +} + +export function serverError(error: any = 'Server error') { + return Response.json({ error: serializeError(error) }, { status: 500 }); +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 5218af10..84662f04 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -1,13 +1,59 @@ -import * as yup from 'yup'; +import { z } from 'zod'; +import { isValidTimezone } from '@/lib/date'; +import { UNIT_TYPES } from './constants'; -export const dateRange = { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), +export const filterParams = { + url: z.string().optional(), + referrer: z.string().optional(), + title: z.string().optional(), + query: z.string().optional(), + os: z.string().optional(), + browser: z.string().optional(), + device: z.string().optional(), + country: z.string().optional(), + region: z.string().optional(), + city: z.string().optional(), + tag: z.string().optional(), + host: z.string().optional(), + language: z.string().optional(), + event: z.string().optional(), }; -export const pageInfo = { - query: yup.string(), - page: yup.number().integer().positive(), - pageSize: yup.number().integer().positive().min(1).max(200), - orderBy: yup.string(), +export const pagingParams = { + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().optional(), + orderBy: z.string().optional(), + query: z.string().optional(), +}; + +export const timezoneParam = z.string().refine(value => isValidTimezone(value), { + message: 'Invalid timezone', +}); + +export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), { + message: 'Invalid unit', +}); + +export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']); + +export const reportTypeParam = z.enum([ + 'funnel', + 'insights', + 'retention', + 'utm', + 'goals', + 'journey', + 'revenue', +]); + +export const reportParms = { + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), + num: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + unit: z.string().optional(), + value: z.string().optional(), + }), }; diff --git a/src/lib/session.ts b/src/lib/session.ts deleted file mode 100644 index 0bfe0302..00000000 --- a/src/lib/session.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { secret, uuid, visitSalt } from 'lib/crypto'; -import { getClientInfo } from 'lib/detect'; -import { parseToken } from 'next-basics'; -import { NextApiRequestCollect } from 'pages/api/send'; -import { createSession } from 'queries'; -import clickhouse from './clickhouse'; -import { fetchSession, fetchWebsite } from './load'; -import { SessionData } from 'lib/types'; - -export async function getSession(req: NextApiRequestCollect): Promise { - const { payload } = req.body; - - if (!payload) { - throw new Error('Invalid payload.'); - } - - // Check if cache token is passed - const cacheToken = req.headers['x-umami-cache']; - - if (cacheToken) { - const result = await parseToken(cacheToken, secret()); - - // Token is valid - if (result) { - return result; - } - } - - // Verify payload - const { website: websiteId, hostname, screen, language } = payload; - - // Find website - const website = await fetchWebsite(websiteId); - - if (!website) { - throw new Error(`Website not found: ${websiteId}.`); - } - - const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } = - await getClientInfo(req); - - const sessionId = uuid(websiteId, hostname, ip, userAgent); - const visitId = uuid(sessionId, visitSalt()); - - // Clickhouse does not require session lookup - if (clickhouse.enabled) { - return { - id: sessionId, - websiteId, - visitId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - ip, - userAgent, - }; - } - - // Find session - let session = await fetchSession(websiteId, sessionId); - - // Create a session if not found - if (!session) { - try { - session = await createSession({ - id: sessionId, - websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - }); - } catch (e: any) { - if (!e.message.toLowerCase().includes('unique constraint')) { - throw e; - } - } - } - - return { ...session, visitId }; -} diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 00000000..f08a7f7a --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,21 @@ +export function setItem(key: string, data: any, session?: boolean): void { + if (typeof window !== 'undefined' && data) { + return (session ? sessionStorage : localStorage).setItem(key, JSON.stringify(data)); + } +} + +export function getItem(key: string, session?: boolean): any { + if (typeof window !== 'undefined') { + const value = (session ? sessionStorage : localStorage).getItem(key); + + if (value !== 'undefined' && value !== null) { + return JSON.parse(value); + } + } +} + +export function removeItem(key: string, session?: boolean): void { + if (typeof window !== 'undefined') { + return (session ? sessionStorage : localStorage).removeItem(key); + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts index dc8ea887..c385fa53 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,3 @@ -import { NextApiRequest } from 'next'; import { COLLECTION_TYPE, DATA_TYPE, @@ -8,7 +7,6 @@ import { REPORT_TYPES, ROLES, } from './constants'; -import * as yup from 'yup'; import { TIME_UNIT } from './date'; import { Dispatch, SetStateAction } from 'react'; @@ -25,9 +23,9 @@ export type KafkaTopic = ObjectValues; export type ReportType = ObjectValues; export interface PageParams { - query?: string; - page?: number; - pageSize?: number; + search?: string; + page?: string; + pageSize?: string; orderBy?: string; sortDescending?: boolean; } @@ -65,26 +63,6 @@ export interface Auth { }; } -export interface YupRequest { - GET?: yup.ObjectSchema; - POST?: yup.ObjectSchema; - PUT?: yup.ObjectSchema; - DELETE?: yup.ObjectSchema; -} - -export interface NextApiRequestQueryBody extends NextApiRequest { - auth?: Auth; - query: TQuery & { [key: string]: string | string[] }; - body: TBody; - headers: any; - yup: YupRequest; -} - -export interface NextApiRequestAuth extends NextApiRequest { - auth?: Auth; - headers: any; -} - export interface User { id: string; username: string; diff --git a/src/lib/url.ts b/src/lib/url.ts new file mode 100644 index 00000000..a039d7d8 --- /dev/null +++ b/src/lib/url.ts @@ -0,0 +1,40 @@ +export function getQueryString(params: object = {}): string { + const searchParams = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + searchParams.append(key, value); + } + }); + + return searchParams.toString(); +} + +export function buildUrl(url: string, params: object = {}): string { + const queryString = getQueryString(params); + return `${url}${queryString && '?' + queryString}`; +} + +export function safeDecodeURI(s: string | undefined | null): string | undefined | null { + if (s === undefined || s === null) { + return s; + } + + try { + return decodeURI(s); + } catch (e) { + return s; + } +} + +export function safeDecodeURIComponent(s: string | undefined | null): string | undefined | null { + if (s === undefined || s === null) { + return s; + } + + try { + return decodeURIComponent(s); + } catch (e) { + return s; + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 00000000..2b0d9ff7 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,46 @@ +export function hook( + _this: { [x: string]: any }, + method: string | number, + callback: (arg0: any) => void, +) { + const orig = _this[method]; + + return (...args: any) => { + callback.apply(_this, args); + + return orig.apply(_this, args); + }; +} + +export function sleep(ms: number | undefined) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export function shuffleArray(a) { + const arr = a.slice(); + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + return arr; +} + +export function chunkArray(arr: any[], size: number) { + const chunks: any[] = []; + + let index = 0; + while (index < arr.length) { + chunks.push(arr.slice(index, size + index)); + index += size; + } + + return chunks; +} + +export function ensureArray(arr?: any) { + if (arr === undefined || arr === null) return []; + if (Array.isArray(arr)) return arr; + return [arr]; +} diff --git a/src/lib/yup.ts b/src/lib/yup.ts deleted file mode 100644 index d2652eda..00000000 --- a/src/lib/yup.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as yup from 'yup'; -import { isValidTimezone } from 'lib/date'; -import { UNIT_TYPES } from './constants'; - -export const TimezoneTest = yup - .string() - .default('UTC') - .test( - 'timezone', - () => `Invalid timezone`, - value => isValidTimezone(value), - ); - -export const UnitTypeTest = yup.string().test( - 'unit', - () => `Invalid unit`, - value => UNIT_TYPES.includes(value), -); diff --git a/src/pages/api/admin/users.ts b/src/pages/api/admin/users.ts deleted file mode 100644 index 4f03ec9f..00000000 --- a/src/pages/api/admin/users.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { canViewUsers } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUsers } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid(), - username: yup.string().max(255).required(), - password: yup.string().required(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewUsers(req.auth))) { - return unauthorized(res); - } - - const users = await getUsers( - { - include: { - _count: { - select: { - websiteUser: { - where: { deletedAt: null }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/admin/websites.ts b/src/pages/api/admin/websites.ts deleted file mode 100644 index d7dd6b74..00000000 --- a/src/pages/api/admin/websites.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { canViewAllWebsites } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsites } from 'queries'; -import * as yup from 'yup'; - -export interface WebsitesRequestQuery extends PageParams { - userId?: string; - includeOwnedTeams?: boolean; - includeAllTeams?: boolean; -} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewAllWebsites(req.auth))) { - return unauthorized(res); - } - - const { userId, includeOwnedTeams, includeAllTeams } = req.query; - - const websites = await getWebsites( - { - where: { - OR: [ - ...(userId && [{ userId }]), - ...(userId && includeOwnedTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - role: ROLES.teamOwner, - userId, - }, - }, - }, - }, - ] - : []), - ...(userId && includeAllTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - userId, - }, - }, - }, - }, - ] - : []), - ], - }, - include: { - user: { - select: { - username: true, - id: true, - }, - }, - team: { - where: { - deletedAt: null, - }, - include: { - teamUser: { - where: { - role: ROLES.teamOwner, - }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts deleted file mode 100644 index fc671785..00000000 --- a/src/pages/api/auth/login.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; -import { secret } from 'lib/crypto'; -import { useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - checkPassword, - createSecureToken, - forbidden, - methodNotAllowed, - ok, - unauthorized, -} from 'next-basics'; -import { getUserByUsername } from 'queries'; -import * as yup from 'yup'; -import { ROLES } from 'lib/constants'; - -export interface LoginRequestBody { - username: string; - password: string; -} - -export interface LoginResponse { - token: string; - user: User; -} - -const schema = { - POST: yup.object().shape({ - username: yup.string().required(), - password: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.disableLogin) { - return forbidden(res); - } - - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { username, password } = req.body; - - const user = await getUserByUsername(username, { includePassword: true }); - - if (user && checkPassword(password, user.password)) { - if (redisEnabled) { - const token = await saveAuth({ userId: user.id }); - - return ok(res, { token, user }); - } - - const token = createSecureToken({ userId: user.id }, secret()); - const { id, username, role, createdAt } = user; - - return ok(res, { - token, - user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, - }); - } - - return unauthorized(res, 'message.incorrect-username-password'); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/logout.ts deleted file mode 100644 index f1604989..00000000 --- a/src/pages/api/auth/logout.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { methodNotAllowed, ok } from 'next-basics'; -import { getClient, redisEnabled } from '@umami/redis-client'; -import { useAuth } from 'lib/middleware'; -import { getAuthToken } from 'lib/auth'; -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async (req: NextApiRequest, res: NextApiResponse) => { - await useAuth(req, res); - - if (req.method === 'POST') { - if (redisEnabled) { - const redis = getClient(); - - await redis.del(getAuthToken(req)); - } - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/sso.ts b/src/pages/api/auth/sso.ts deleted file mode 100644 index c5560cb1..00000000 --- a/src/pages/api/auth/sso.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, ok } from 'next-basics'; -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - if (redisEnabled && req.auth.user) { - const token = await saveAuth({ userId: req.auth.user.id }, 86400); - - return ok(res, { user: req.auth.user, token }); - } - - return badRequest(res); -}; diff --git a/src/pages/api/auth/verify.ts b/src/pages/api/auth/verify.ts deleted file mode 100644 index 3cc78ed3..00000000 --- a/src/pages/api/auth/verify.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { ok } from 'next-basics'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - const { user } = req.auth; - - return ok(res, user); -}; diff --git a/src/pages/api/config.ts b/src/pages/api/config.ts deleted file mode 100644 index adba894a..00000000 --- a/src/pages/api/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; - -export interface ConfigResponse { - telemetryDisabled: boolean; - trackerScriptName: string; - uiDisabled: boolean; - updatesDisabled: boolean; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - telemetryDisabled: !!process.env.DISABLE_TELEMETRY, - trackerScriptName: process.env.TRACKER_SCRIPT_NAME, - uiDisabled: !!process.env.DISABLE_UI, - updatesDisabled: !!process.env.DISABLE_UPDATES, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/heartbeat.ts b/src/pages/api/heartbeat.ts deleted file mode 100644 index 1b515d39..00000000 --- a/src/pages/api/heartbeat.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok } from 'next-basics'; - -export default async (req: NextApiRequest, res: NextApiResponse) => { - return ok(res); -}; diff --git a/src/pages/api/me/index.ts b/src/pages/api/me/index.ts deleted file mode 100644 index 93e97067..00000000 --- a/src/pages/api/me/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextApiResponse } from 'next'; -import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { ok } from 'next-basics'; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - - return ok(res, req.auth.user); -}; diff --git a/src/pages/api/me/password.ts b/src/pages/api/me/password.ts deleted file mode 100644 index 2ba91d86..00000000 --- a/src/pages/api/me/password.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - badRequest, - checkPassword, - forbidden, - hashPassword, - methodNotAllowed, - ok, -} from 'next-basics'; -import { getUser, updateUser } from 'queries'; -import * as yup from 'yup'; - -export interface UserPasswordRequestBody { - currentPassword: string; - newPassword: string; -} - -const schema = { - POST: yup.object().shape({ - currentPassword: yup.string().required(), - newPassword: yup.string().min(8).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.CLOUD_MODE) { - return forbidden(res); - } - - await useAuth(req, res); - await useValidate(schema, req, res); - - const { currentPassword, newPassword } = req.body; - const { id: userId } = req.auth.user; - - if (req.method === 'POST') { - const user = await getUser(userId, { includePassword: true }); - - if (!checkPassword(currentPassword, user.password)) { - return badRequest(res, 'Current password is incorrect'); - } - - const password = hashPassword(newPassword); - - const updated = await updateUser(userId, { password }); - - return ok(res, updated); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts deleted file mode 100644 index 3b88689d..00000000 --- a/src/pages/api/me/teams.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userTeamsRoute from 'pages/api/users/[userId]/teams'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userTeamsRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts deleted file mode 100644 index 48800f90..00000000 --- a/src/pages/api/me/websites.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userWebsitesRoute from 'pages/api/users/[userId]/websites'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userWebsitesRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/realtime/[websiteId].ts b/src/pages/api/realtime/[websiteId].ts deleted file mode 100644 index 08e9bc47..00000000 --- a/src/pages/api/realtime/[websiteId].ts +++ /dev/null @@ -1,43 +0,0 @@ -import { startOfMinute, subMinutes } from 'date-fns'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRealtimeData } from 'queries'; -import * as yup from 'yup'; -import { REALTIME_RANGE } from 'lib/constants'; -import { TimezoneTest } from 'lib/yup'; - -export interface RealtimeRequestQuery { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, timezone } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - - const data = await getRealtimeData(websiteId, { startDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/[reportId].ts b/src/pages/api/reports/[reportId].ts deleted file mode 100644 index 91b5fb51..00000000 --- a/src/pages/api/reports/[reportId].ts +++ /dev/null @@ -1,102 +0,0 @@ -import { canDeleteReport, canUpdateReport, canViewReport } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, ReportType, YupRequest } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteReport, getReport, updateReport } from 'queries'; -import * as yup from 'yup'; - -export interface ReportRequestQuery { - reportId: string; -} - -export interface ReportRequestBody { - websiteId: string; - type: ReportType; - name: string; - description: string; - parameters: string; -} - -const schema: YupRequest = { - GET: yup.object().shape({ - reportId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - reportId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/i) - .required(), - name: yup.string().max(200).required(), - description: yup.string().max(500), - parameters: yup - .object() - .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), - }), - DELETE: yup.object().shape({ - reportId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { reportId } = req.query; - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const report = await getReport(reportId); - - if (!(await canViewReport(req.auth, report))) { - return unauthorized(res); - } - - report.parameters = JSON.parse(report.parameters); - - return ok(res, report); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - const report = await getReport(reportId); - - if (!(await canUpdateReport(req.auth, report))) { - return unauthorized(res); - } - - const result = await updateReport(reportId, { - websiteId, - userId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - if (req.method === 'DELETE') { - const report = await getReport(reportId); - - if (!(await canDeleteReport(req.auth, report))) { - return unauthorized(res); - } - - await deleteReport(reportId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/funnel.ts b/src/pages/api/reports/funnel.ts deleted file mode 100644 index 35759a30..00000000 --- a/src/pages/api/reports/funnel.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getFunnel } from 'queries'; -import * as yup from 'yup'; - -export interface FunnelRequestBody { - websiteId: string; - steps: { type: string; value: string }[]; - window: number; - dateRange: { - startDate: string; - endDate: string; - }; -} - -export interface FunnelResponse { - steps: { type: string; value: string }[]; - window: number; - startAt: number; - endAt: number; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - steps: yup - .array() - .of( - yup.object().shape({ - type: yup.string().required(), - value: yup.string().required(), - }), - ) - .min(2) - .required(), - window: yup.number().positive().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - steps, - window, - dateRange: { startDate, endDate }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getFunnel(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - windowMinutes: +window, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/goals.ts b/src/pages/api/reports/goals.ts deleted file mode 100644 index f775dc3c..00000000 --- a/src/pages/api/reports/goals.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getGoals } from 'queries/analytics/reports/getGoals'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; - goals: { type: string; value: string; goal: number }[]; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - goals: yup - .array() - .of( - yup.object().shape({ - type: yup - .string() - .matches(/url|event|event-data/i) - .required(), - value: yup.string().required(), - goal: yup.number().required(), - operator: yup - .string() - .matches(/count|sum|average/i) - .when('type', { - is: 'eventData', - then: yup.string().required(), - }), - property: yup.string().when('type', { - is: 'eventData', - then: yup.string().required(), - }), - }), - ) - .min(1) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - goals, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getGoals(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - goals, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/index.ts b/src/pages/api/reports/index.ts deleted file mode 100644 index 38996b7a..00000000 --- a/src/pages/api/reports/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getReports } from 'queries'; -import * as yup from 'yup'; -import { canUpdateWebsite, canViewTeam, canViewWebsite } from 'lib/auth'; - -export interface ReportRequestBody { - websiteId: string; - name: string; - type: string; - description: string; - parameters: { - [key: string]: any; - }; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string().max(200).required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/i) - .required(), - description: yup.string().max(500), - parameters: yup - .object() - .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const { page, query, pageSize, websiteId, teamId } = req.query; - const filters = { - page, - pageSize, - query, - }; - - if ( - (websiteId && !(await canViewWebsite(req.auth, websiteId))) || - (teamId && !(await canViewTeam(req.auth, teamId))) - ) { - return unauthorized(res); - } - - const data = await getReports( - { - where: { - OR: [ - ...(websiteId ? [{ websiteId }] : []), - ...(teamId - ? [ - { - website: { - deletedAt: null, - teamId, - }, - }, - ] - : []), - ...(userId && !websiteId && !teamId - ? [ - { - website: { - deletedAt: null, - userId, - }, - }, - ] - : []), - ], - }, - include: { - website: { - select: { - domain: true, - }, - }, - }, - }, - filters, - ); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await createReport({ - id: uuid(), - userId, - websiteId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts deleted file mode 100644 index ba4f643e..00000000 --- a/src/pages/api/reports/insights.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getInsights } from 'queries'; -import * as yup from 'yup'; - -export interface InsightsRequestBody { - websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; - fields: { name: string; type: string; label: string }[]; - filters: { name: string; type: string; operator: string; value: string }[]; - groups: { name: string; type: string }[]; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - fields: yup - .array() - .of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - label: yup.string().required(), - }), - ) - .min(1) - .required(), - filters: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - operator: yup.string().required(), - value: yup.string().required(), - }), - ), - groups: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - }), - ), - }), -}; - -function convertFilters(filters: any[]) { - return filters.reduce((obj, filter) => { - obj[filter.name] = filter; - - return obj; - }, {}); -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - fields, - filters, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getInsights(websiteId, fields, { - ...convertFilters(filters), - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/journey.ts b/src/pages/api/reports/journey.ts deleted file mode 100644 index dd3bd57b..00000000 --- a/src/pages/api/reports/journey.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getJourney } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - steps: number; - startStep?: string; - endStep?: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - steps: yup.number().min(3).max(7).required(), - startStep: yup.string(), - endStep: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - steps, - startStep, - endStep, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getJourney(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - startStep, - endStep, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/retention.ts b/src/pages/api/reports/retention.ts deleted file mode 100644 index f4d9b7df..00000000 --- a/src/pages/api/reports/retention.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRetention } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - timezone: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - timezone: TimezoneTest, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - timezone, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRetention(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/revenue.ts b/src/pages/api/reports/revenue.ts deleted file mode 100644 index d23ce55a..00000000 --- a/src/pages/api/reports/revenue.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRevenue } from 'queries/analytics/reports/getRevenue'; -import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues'; -import * as yup from 'yup'; - -export interface RevenueRequestBody { - websiteId: string; - currency?: string; - timezone?: string; - dateRange: { startDate: string; endDate: string; unit?: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - unit: UnitTypeTest, - }) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startDate, endDate } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenueValues(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { - websiteId, - currency, - timezone, - dateRange: { startDate, endDate, unit }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenue(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - unit, - timezone, - currency, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/utm.ts b/src/pages/api/reports/utm.ts deleted file mode 100644 index 59399ee4..00000000 --- a/src/pages/api/reports/utm.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUTM } from 'queries'; -import * as yup from 'yup'; - -export interface UTMRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate, timezone }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getUTM(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts deleted file mode 100644 index a47b91d2..00000000 --- a/src/pages/api/send.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* eslint-disable no-console */ -import { isbot } from 'isbot'; -import { NextApiRequest, NextApiResponse } from 'next'; -import { - badRequest, - createToken, - forbidden, - methodNotAllowed, - ok, - safeDecodeURI, -} from 'next-basics'; -import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants'; -import { secret, visitSalt, uuid } from 'lib/crypto'; -import { hasBlockedIp } from 'lib/detect'; -import { useCors, useSession, useValidate } from 'lib/middleware'; -import { CollectionType, YupRequest } from 'lib/types'; -import { saveEvent, saveSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface CollectRequestBody { - payload: { - website: string; - data?: { [key: string]: any }; - hostname?: string; - language?: string; - name?: string; - referrer?: string; - screen?: string; - tag?: string; - title?: string; - url: string; - ip?: string; - userAgent?: string; - }; - type: CollectionType; -} - -export interface NextApiRequestCollect extends NextApiRequest { - body: CollectRequestBody; - session: { - id: string; - websiteId: string; - visitId: string; - hostname: string; - browser: string; - os: string; - device: string; - screen: string; - language: string; - country: string; - subdivision1: string; - subdivision2: string; - city: string; - iat: number; - }; - headers: { [key: string]: any }; - yup: YupRequest; -} - -const schema = { - POST: yup.object().shape({ - payload: yup - .object() - .shape({ - data: yup.object(), - hostname: yup.string().matches(HOSTNAME_REGEX).max(100), - language: yup.string().max(35), - referrer: yup.string(), - screen: yup.string().max(11), - title: yup.string(), - url: yup.string(), - website: yup.string().uuid().required(), - name: yup.string().max(50), - tag: yup.string().max(50).nullable(), - ip: yup.string().matches(IP_REGEX), - userAgent: yup.string(), - }) - .required(), - type: yup - .string() - .matches(/event|identify/i) - .required(), - }), -}; - -export default async (req: NextApiRequestCollect, res: NextApiResponse) => { - await useCors(req, res); - - if (req.method === 'POST') { - if (!process.env.DISABLE_BOT_CHECK && isbot(req.headers['user-agent'])) { - return ok(res, { beep: 'boop' }); - } - - await useValidate(schema, req, res); - - if (hasBlockedIp(req)) { - return forbidden(res); - } - - const { type, payload } = req.body; - const { url, referrer, name, data, title, tag } = payload; - - await useSession(req, res); - - const session = req.session; - - if (!session?.id || !session?.websiteId) { - return ok(res, {}); - } - - const iat = Math.floor(new Date().getTime() / 1000); - - // expire visitId after 30 minutes - if (session.iat && iat - session.iat > 1800) { - session.visitId = uuid(session.id, visitSalt()); - } - - session.iat = iat; - - if (type === COLLECTION_TYPE.event) { - // eslint-disable-next-line prefer-const - let [urlPath, urlQuery] = safeDecodeURI(url)?.split('?') || []; - let [referrerPath, referrerQuery] = safeDecodeURI(referrer)?.split('?') || []; - let referrerDomain = ''; - - if (!urlPath) { - urlPath = '/'; - } - - if (/^[\w-]+:\/\/\w+/.test(referrerPath)) { - const refUrl = new URL(referrer); - referrerPath = refUrl.pathname; - referrerQuery = refUrl.search.substring(1); - referrerDomain = refUrl.hostname.replace(/www\./, ''); - } - - if (process.env.REMOVE_TRAILING_SLASH) { - urlPath = urlPath.replace(/(.+)\/$/, '$1'); - } - - await saveEvent({ - urlPath, - urlQuery, - referrerPath, - referrerQuery, - referrerDomain, - pageTitle: title, - eventName: name, - eventData: data, - ...session, - sessionId: session.id, - tag, - }); - } else if (type === COLLECTION_TYPE.identify) { - if (!data) { - return badRequest(res, 'Data required.'); - } - - await saveSessionData({ - websiteId: session.websiteId, - sessionId: session.id, - sessionData: data, - }); - } - - const cache = createToken(session, secret()); - - return ok(res, { cache }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/share/[shareId].ts b/src/pages/api/share/[shareId].ts deleted file mode 100644 index 26ac4cdc..00000000 --- a/src/pages/api/share/[shareId].ts +++ /dev/null @@ -1,46 +0,0 @@ -import { secret } from 'lib/crypto'; -import { useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { createToken, methodNotAllowed, notFound, ok } from 'next-basics'; -import { getSharedWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface ShareRequestQuery { - shareId: string; -} - -export interface ShareResponse { - shareId: string; - token: string; -} - -const schema = { - GET: yup.object().shape({ - shareId: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useValidate(schema, req, res); - - const { shareId } = req.query; - - if (req.method === 'GET') { - const website = await getSharedWebsite(shareId); - - if (website) { - const data = { websiteId: website.id }; - const token = createToken(data, secret()); - - return ok(res, { ...data, token }); - } - - return notFound(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/index.ts b/src/pages/api/teams/[teamId]/index.ts deleted file mode 100644 index b731ee0c..00000000 --- a/src/pages/api/teams/[teamId]/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Team } from '@prisma/client'; -import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, notFound, ok, unauthorized } from 'next-basics'; -import { deleteTeam, getTeam, updateTeam } from 'queries'; -import * as yup from 'yup'; - -export interface TeamRequestQuery { - teamId: string; -} - -export interface TeamRequestBody { - name: string; - accessCode: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - id: yup.string().uuid().required(), - name: yup.string().max(50), - accessCode: yup.string().max(50), - }), - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const team = await getTeam(teamId, { includeMembers: true }); - - if (!team) { - return notFound(res); - } - - return ok(res, team); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const { name, accessCode } = req.body; - const data = { name, accessCode }; - - const updated = await updateTeam(teamId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - await deleteTeam(teamId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/[userId].ts b/src/pages/api/teams/[teamId]/users/[userId].ts deleted file mode 100644 index c1e80b1a..00000000 --- a/src/pages/api/teams/[teamId]/users/[userId].ts +++ /dev/null @@ -1,85 +0,0 @@ -import { canDeleteTeamUser, canUpdateTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteTeamUser, getTeamUser, updateTeamUser } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery { - teamId: string; - userId: string; -} - -export interface TeamUserRequestBody { - role: string; -} - -const schema = { - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId, userId } = req.query; - - if (req.method === 'GET') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - return ok(res, teamUser); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - const { role } = req.body; - - await updateTeamUser(teamUser.id, { role }); - - return ok(res); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeamUser(req.auth, teamId, userId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - await deleteTeamUser(teamId, userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/index.ts b/src/pages/api/teams/[teamId]/users/index.ts deleted file mode 100644 index f25b99da..00000000 --- a/src/pages/api/teams/[teamId]/users/index.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { canAddUserToTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getTeamUser, getTeamUsers } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery extends PageParams { - teamId: string; -} - -export interface TeamUserRequestBody { - userId: string; - role: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const users = await getTeamUsers( - { - where: { - teamId, - user: { - deletedAt: null, - }, - }, - include: { - user: { - select: { - id: true, - username: true, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - // admin function only - if (req.method === 'POST') { - if (!(await canAddUserToTeam(req.auth))) { - return unauthorized(res); - } - - const { userId, role } = req.body; - - const teamUser = await getTeamUser(teamId, userId); - - if (teamUser) { - return badRequest(res, 'User is already a member of the Team.'); - } - - const users = await createTeamUser(userId, teamId, role); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/websites/index.ts b/src/pages/api/teams/[teamId]/websites/index.ts deleted file mode 100644 index 75020fa4..00000000 --- a/src/pages/api/teams/[teamId]/websites/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as yup from 'yup'; -import { canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { ok, unauthorized } from 'next-basics'; -import { getTeamWebsites } from 'queries'; - -export interface TeamWebsiteRequestQuery extends PageParams { - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const websites = await getTeamWebsites(teamId, req.query); - - return ok(res, websites); - } -}; diff --git a/src/pages/api/teams/index.ts b/src/pages/api/teams/index.ts deleted file mode 100644 index 1e683469..00000000 --- a/src/pages/api/teams/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as yup from 'yup'; -import { Team } from '@prisma/client'; -import { canCreateTeam } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeam } from 'queries'; - -export interface TeamsRequestQuery extends PageParams {} -export interface TeamsRequestBody { - name: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(50).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'POST') { - if (!(await canCreateTeam(req.auth))) { - return unauthorized(res); - } - - const { name } = req.body; - - const team = await createTeam( - { - id: uuid(), - name, - accessCode: getRandomChars(16), - }, - userId, - ); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/join.ts b/src/pages/api/teams/join.ts deleted file mode 100644 index a9943f64..00000000 --- a/src/pages/api/teams/join.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Team } from '@prisma/client'; -import { ROLES } from 'lib/constants'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, notFound, ok } from 'next-basics'; -import { createTeamUser, findTeam, getTeamUser } from 'queries'; -import * as yup from 'yup'; - -export interface TeamsJoinRequestBody { - accessCode: string; -} - -const schema = { - POST: yup.object().shape({ - accessCode: yup.string().max(50).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { accessCode } = req.body; - - const team = await findTeam({ - where: { - accessCode, - }, - }); - - if (!team) { - return notFound(res, 'message.team-not-found'); - } - - const teamUser = await getTeamUser(team.id, req.auth.user.id); - - if (teamUser) { - return methodNotAllowed(res, 'message.team-already-member'); - } - - await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/index.ts b/src/pages/api/users/[userId]/index.ts deleted file mode 100644 index d69cad3c..00000000 --- a/src/pages/api/users/[userId]/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as yup from 'yup'; -import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteUser, getUser, getUserByUsername, updateUser } from 'queries'; - -export interface UserRequestQuery { - userId: string; -} - -export interface UserRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - username: yup.string().max(255), - password: yup.string(), - role: yup.string().matches(/admin|user|view-only/i), - }), - DELETE: yup.object().shape({ - userId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { isAdmin }, - } = req.auth; - const userId: string = req.query.userId; - - if (req.method === 'GET') { - if (!(await canViewUser(req.auth, userId))) { - return unauthorized(res); - } - - const user = await getUser(userId); - - return ok(res, user); - } - - if (req.method === 'POST') { - if (!(await canUpdateUser(req.auth, userId))) { - return unauthorized(res); - } - - const { username, password, role } = req.body; - - const user = await getUser(userId); - - const data: any = {}; - - if (password) { - data.password = hashPassword(password); - } - - // Only admin can change these fields - if (role && isAdmin) { - data.role = role; - } - - if (username && isAdmin) { - data.username = username; - } - - // Check when username changes - if (data.username && user.username !== data.username) { - const user = await getUserByUsername(username); - - if (user) { - return badRequest(res, 'User already exists'); - } - } - - const updated = await updateUser(userId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteUser(req.auth))) { - return unauthorized(res); - } - - if (userId === req.auth.user.id) { - return badRequest(res, 'You cannot delete yourself.'); - } - - await deleteUser(userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/teams.ts b/src/pages/api/users/[userId]/teams.ts deleted file mode 100644 index 3f2af9e2..00000000 --- a/src/pages/api/users/[userId]/teams.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserTeams } from 'queries'; - -export interface UserTeamsRequestQuery extends PageParams { - userId: string; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && (!userId || user.id !== userId)) { - return unauthorized(res); - } - - const teams = await getUserTeams(userId as string, req.query); - - return ok(res, teams); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/usage.ts b/src/pages/api/users/[userId]/usage.ts deleted file mode 100644 index b5000395..00000000 --- a/src/pages/api/users/[userId]/usage.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getAllUserWebsitesIncludingTeamOwner, getEventDataUsage, getEventUsage } from 'queries'; -import * as yup from 'yup'; - -export interface UserUsageRequestQuery { - userId: string; - startAt: string; - endAt: string; -} - -export interface UserUsageRequestResponse { - websiteEventUsage: number; - eventDataUsage: number; - websites: { - websiteEventUsage: number; - eventDataUsage: number; - websiteId: string; - websiteName: string; - }[]; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - - if (req.method === 'GET') { - if (!user.isAdmin) { - return unauthorized(res); - } - - const { userId, startAt, endAt } = req.query; - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const websites = await getAllUserWebsitesIncludingTeamOwner(userId); - - const websiteIds = websites.map(a => a.id); - - const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); - const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); - - const websiteUsage = websites.map(a => ({ - websiteId: a.id, - websiteName: a.name, - websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, - eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, - deletedAt: a.deletedAt, - })); - - const usage = websiteUsage.reduce( - (acc, cv) => { - acc.websiteEventUsage += cv.websiteEventUsage; - acc.eventDataUsage += cv.eventDataUsage; - - return acc; - }, - { websiteEventUsage: 0, eventDataUsage: 0 }, - ); - - const filteredWebsiteUsage = websiteUsage.filter( - a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), - ); - - return ok(res, { - ...usage, - websites: filteredWebsiteUsage, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/websites.ts b/src/pages/api/users/[userId]/websites.ts deleted file mode 100644 index 88a2bad1..00000000 --- a/src/pages/api/users/[userId]/websites.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserWebsites } from 'queries'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId, page = 1, pageSize, query = '', ...rest } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && user.id !== userId) { - return unauthorized(res); - } - - const websites = await getUserWebsites(userId, { - page, - pageSize, - query, - ...rest, - }); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts deleted file mode 100644 index 333670a9..00000000 --- a/src/pages/api/users/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canCreateUser } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createUser, getUserByUsername } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - username: string; - password: string; - id: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - username: yup.string().max(255).required(), - password: yup.string().required(), - id: yup.string().uuid(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - if (!(await canCreateUser(req.auth))) { - return unauthorized(res); - } - - const { username, password, role, id } = req.body; - - const existingUser = await getUserByUsername(username, { showDeleted: true }); - - if (existingUser) { - return badRequest(res, 'User already exists'); - } - - const created = await createUser({ - id: id || uuid(), - username, - password: hashPassword(password), - role: role ?? ROLES.user, - }); - - return ok(res, created); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/version.ts b/src/pages/api/version.ts deleted file mode 100644 index 4453b56f..00000000 --- a/src/pages/api/version.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; -import { CURRENT_VERSION } from 'lib/constants'; - -export interface VersionResponse { - version: string; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - version: CURRENT_VERSION, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/active.ts b/src/pages/api/websites/[websiteId]/active.ts deleted file mode 100644 index d87a7818..00000000 --- a/src/pages/api/websites/[websiteId]/active.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getActiveVisitors } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteActiveRequestQuery { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId as string))) { - return unauthorized(res); - } - - const result = await getActiveVisitors(websiteId as string); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/daterange.ts b/src/pages/api/websites/[websiteId]/daterange.ts deleted file mode 100644 index 1aeb76cb..00000000 --- a/src/pages/api/websites/[websiteId]/daterange.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteDateRange } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteDateRangeRequestQuery { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await getWebsiteDateRange(websiteId); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/events.ts b/src/pages/api/websites/[websiteId]/event-data/events.ts deleted file mode 100644 index bf0f409a..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/events.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataEvents } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - event?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - event: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, event } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataEvents(websiteId, { - startDate, - endDate, - event, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/fields.ts b/src/pages/api/websites/[websiteId]/event-data/fields.ts deleted file mode 100644 index c5075c5e..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/fields.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataFields } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataFields(websiteId, { - startDate, - endDate, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/properties.ts b/src/pages/api/websites/[websiteId]/event-data/properties.ts deleted file mode 100644 index 19e9bbb8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/stats.ts b/src/pages/api/websites/[websiteId]/event-data/stats.ts deleted file mode 100644 index 7e440b88..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/stats.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataStats } from 'queries/index'; -import * as yup from 'yup'; - -export interface EventDataStatsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataStats(websiteId, { startDate, endDate }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/values.ts b/src/pages/api/websites/[websiteId]/event-data/values.ts deleted file mode 100644 index e5bb4ab8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/values.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - eventName?: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - eventName: yup.string(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, eventName, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataValues(websiteId, { - startDate, - endDate, - eventName, - propertyName, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/index.ts b/src/pages/api/websites/[websiteId]/events/index.ts deleted file mode 100644 index 13b31fc0..00000000 --- a/src/pages/api/websites/[websiteId]/events/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteEvents } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteEvents(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/series.ts b/src/pages/api/websites/[websiteId]/events/series.ts deleted file mode 100644 index 6d67a264..00000000 --- a/src/pages/api/websites/[websiteId]/events/series.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventMetrics } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteEventsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - unit?: string; - timezone?: string; - url: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone } = req.query; - const { startDate, endDate, unit } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const events = await getEventMetrics(websiteId, filters); - - return ok(res, events); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/index.ts b/src/pages/api/websites/[websiteId]/index.ts deleted file mode 100644 index c60a8399..00000000 --- a/src/pages/api/websites/[websiteId]/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; -import { Website, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; -import { SHARE_ID_REGEX } from 'lib/constants'; - -export interface WebsiteRequestQuery { - websiteId: string; -} - -export interface WebsiteRequestBody { - name: string; - domain: string; - shareId: string; -} - -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string(), - domain: yup.string(), - shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }).nullable(), - }), - DELETE: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const website = await getWebsite(websiteId); - - return ok(res, website); - } - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { name, domain, shareId } = req.body; - - try { - const website = await updateWebsite(websiteId, { name, domain, shareId }); - - return ok(res, website); - } catch (e: any) { - if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { - return serverError(res, 'That share ID is already taken.'); - } - - return serverError(res, e); - } - } - - if (req.method === 'DELETE') { - if (!(await canDeleteWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await deleteWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/metrics.ts b/src/pages/api/websites/[websiteId]/metrics.ts deleted file mode 100644 index 1996a61a..00000000 --- a/src/pages/api/websites/[websiteId]/metrics.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants'; -import { getPageviewMetrics, getSessionMetrics } from 'queries'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface WebsiteMetricsRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - language?: string; - event?: string; - limit?: number; - offset?: number; - search?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - language: yup.string(), - event: yup.string(), - limit: yup.number(), - offset: yup.number(), - search: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, limit, offset, search } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const column = FILTER_COLUMNS[type] || type; - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - }; - - if (search) { - filters[type] = { - name: type, - column, - operator: OPERATORS.contains, - value: search, - }; - } - - if (SESSION_COLUMNS.includes(type)) { - const data = await getSessionMetrics(websiteId, type, filters, limit, offset); - - if (type === 'language') { - const combined = {}; - - for (const { x, y } of data) { - const key = String(x).toLowerCase().split('-')[0]; - - if (combined[key] === undefined) { - combined[key] = { x: key, y }; - } else { - combined[key].y += y; - } - } - - return ok(res, Object.values(combined)); - } - - return ok(res, data); - } - - if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); - - return ok(res, data); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/pageviews.ts b/src/pages/api/websites/[websiteId]/pageviews.ts deleted file mode 100644 index c3b6b797..00000000 --- a/src/pages/api/websites/[websiteId]/pageviews.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getPageviewStats, getSessionStats } from 'queries'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { getCompareDate } from 'lib/date'; - -export interface WebsitePageviewRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - unit?: string; - timezone?: string; - url?: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate, unit } = await getRequestDateRange(req); - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, filters), - getSessionStats(websiteId, filters), - ]); - - if (compare) { - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const [comparePageviews, compareSessions] = await Promise.all([ - getPageviewStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - getSessionStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - ]); - - return ok(res, { - pageviews, - sessions, - startDate, - endDate, - compare: { - pageviews: comparePageviews, - sessions: compareSessions, - startDate: compareStartDate, - endDate: compareEndDate, - }, - }); - } - - return ok(res, { pageviews, sessions }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/reports.ts b/src/pages/api/websites/[websiteId]/reports.ts deleted file mode 100644 index 72e5b0f2..00000000 --- a/src/pages/api/websites/[websiteId]/reports.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteReports } from 'queries'; -import { pageInfo } from 'lib/schema'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { page, query, pageSize } = req.query; - - const data = await getWebsiteReports(websiteId, { - page, - pageSize, - query, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/reset.ts b/src/pages/api/websites/[websiteId]/reset.ts deleted file mode 100644 index 82e769dc..00000000 --- a/src/pages/api/websites/[websiteId]/reset.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canUpdateWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { resetWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteResetRequestQuery { - websiteId: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await resetWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/properties.ts b/src/pages/api/websites/[websiteId]/session-data/properties.ts deleted file mode 100644 index 92e182d2..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/values.ts b/src/pages/api/websites/[websiteId]/session-data/values.ts deleted file mode 100644 index 98463f15..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/values.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataValues(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts deleted file mode 100644 index 2b0fc084..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionActivity } from 'queries'; - -export interface SessionActivityRequestQuery extends PageParams { - websiteId: string; - sessionId: string; - startAt: number; - endAt: number; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - startAt: yup.number().integer(), - endAt: yup.number().integer(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts deleted file mode 100644 index f627a208..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSession } from 'queries'; - -export interface WesiteSessionRequestQuery extends PageParams { - websiteId: string; - sessionId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getWebsiteSession(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts deleted file mode 100644 index c0c20064..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataRequestQuery { - sessionId: string; - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - sessionId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, sessionId } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getSessionData(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/index.ts b/src/pages/api/websites/[websiteId]/sessions/index.ts deleted file mode 100644 index 1809929c..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessions } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessions(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/stats.ts b/src/pages/api/websites/[websiteId]/sessions/stats.ts deleted file mode 100644 index fe92ce6f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/stats.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSessionStats } from 'queries/analytics/sessions/getWebsiteSessionStats'; -import * as yup from 'yup'; - -export interface WebsiteSessionStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteSessionStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/weekly.ts b/src/pages/api/websites/[websiteId]/sessions/weekly.ts deleted file mode 100644 index b1c28c3f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/weekly.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessionsWeekly } from 'queries'; -import { TimezoneTest } from 'lib/yup'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - timezone: TimezoneTest, - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt, timezone } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/stats.ts b/src/pages/api/websites/[websiteId]/stats.ts deleted file mode 100644 index dfc9198d..00000000 --- a/src/pages/api/websites/[websiteId]/stats.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as yup from 'yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { getWebsiteStats } from 'queries'; -import { getCompareDate } from 'lib/date'; - -export interface WebsiteStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const prevPeriod = await getWebsiteStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - prev: Number(prevPeriod[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/transfer.ts b/src/pages/api/websites/[websiteId]/transfer.ts deleted file mode 100644 index 56cf6bac..00000000 --- a/src/pages/api/websites/[websiteId]/transfer.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { updateWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteTransferRequestQuery { - websiteId: string; -} - -export interface WebsiteTransferRequestBody { - userId?: string; - teamId?: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - userId: yup.string().uuid(), - teamId: yup.string().uuid(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - const { userId, teamId } = req.body; - - if (req.method === 'POST') { - if (userId) { - if (!(await canTransferWebsiteToUser(req.auth, websiteId, userId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId, - teamId: null, - }); - - return ok(res, website); - } else if (teamId) { - if (!(await canTransferWebsiteToTeam(req.auth, websiteId, teamId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId: null, - teamId, - }); - - return ok(res, website); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/values.ts b/src/pages/api/websites/[websiteId]/values.ts deleted file mode 100644 index 53d717a5..00000000 --- a/src/pages/api/websites/[websiteId]/values.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { - badRequest, - methodNotAllowed, - ok, - safeDecodeURIComponent, - unauthorized, -} from 'next-basics'; -import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { getValues } from 'queries'; -import { getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface ValuesRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - search?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - search: yup.string(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, search } = req.query; - const { startDate, endDate } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!SESSION_COLUMNS.includes(type as string) && !EVENT_COLUMNS.includes(type as string)) { - return badRequest(res); - } - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); - - return ok( - res, - values - .map(({ value }) => safeDecodeURIComponent(value)) - .filter(n => n) - .sort(), - ); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts deleted file mode 100644 index c5eb7200..00000000 --- a/src/pages/api/websites/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createWebsite } from 'queries'; -import userWebsitesRoute from 'pages/api/users/[userId]/websites'; -import * as yup from 'yup'; -import { pageInfo } from 'lib/schema'; - -export interface WebsitesRequestQuery extends PageParams {} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - teamId: yup.string().nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - if (!req.query.userId) { - req.query.userId = userId; - } - - return userWebsitesRoute(req, res); - } - - if (req.method === 'POST') { - const { name, domain, shareId, teamId } = req.body; - - if ( - (teamId && !(await canCreateTeamWebsite(req.auth, teamId))) || - !(await canCreateWebsite(req.auth)) - ) { - return unauthorized(res); - } - - const data: any = { - id: uuid(), - createdBy: userId, - name, - domain, - shareId, - teamId, - }; - - if (!teamId) { - data.userId = userId; - } - - const website = await createWebsite(data); - - return ok(res, website); - } - - return methodNotAllowed(res); -}; diff --git a/src/queries/index.ts b/src/queries/index.ts index 8c7e564a..2f785528 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -1,39 +1,41 @@ -export * from 'queries/prisma/report'; -export * from 'queries/prisma/team'; -export * from 'queries/prisma/teamUser'; -export * from 'queries/prisma/user'; -export * from 'queries/prisma/website'; -export * from './analytics/events/getEventDataEvents'; -export * from './analytics/events/getEventDataFields'; -export * from './analytics/events/getEventDataProperties'; -export * from './analytics/events/getEventDataValues'; -export * from './analytics/events/getEventDataStats'; -export * from './analytics/events/getEventDataUsage'; -export * from './analytics/events/getEventMetrics'; -export * from './analytics/events/getWebsiteEvents'; -export * from './analytics/events/getEventUsage'; -export * from './analytics/events/saveEvent'; -export * from './analytics/reports/getFunnel'; -export * from './analytics/reports/getJourney'; -export * from './analytics/reports/getRetention'; -export * from './analytics/reports/getInsights'; -export * from './analytics/reports/getUTM'; -export * from './analytics/pageviews/getPageviewMetrics'; -export * from './analytics/pageviews/getPageviewStats'; -export * from './analytics/sessions/createSession'; -export * from './analytics/sessions/getWebsiteSession'; -export * from './analytics/sessions/getSessionData'; -export * from './analytics/sessions/getSessionDataProperties'; -export * from './analytics/sessions/getSessionDataValues'; -export * from './analytics/sessions/getSessionMetrics'; -export * from './analytics/sessions/getWebsiteSessions'; -export * from './analytics/sessions/getWebsiteSessionsWeekly'; -export * from './analytics/sessions/getSessionActivity'; -export * from './analytics/sessions/getSessionStats'; -export * from './analytics/sessions/saveSessionData'; -export * from './analytics/getActiveVisitors'; -export * from './analytics/getRealtimeActivity'; -export * from './analytics/getRealtimeData'; -export * from './analytics/getValues'; -export * from './analytics/getWebsiteDateRange'; -export * from './analytics/getWebsiteStats'; +export * from '@/queries/prisma/report'; +export * from '@/queries/prisma/team'; +export * from '@/queries/prisma/teamUser'; +export * from '@/queries/prisma/user'; +export * from '@/queries/prisma/website'; +export * from '@/queries/sql/events/getEventDataEvents'; +export * from '@/queries/sql/events/getEventDataFields'; +export * from '@/queries/sql/events/getEventDataProperties'; +export * from '@/queries/sql/events/getEventDataValues'; +export * from '@/queries/sql/events/getEventDataStats'; +export * from '@/queries/sql/events/getEventDataUsage'; +export * from '@/queries/sql/events/getEventMetrics'; +export * from '@/queries/sql/events/getWebsiteEvents'; +export * from '@/queries/sql/events/getEventUsage'; +export * from '@/queries/sql/events/saveEvent'; +export * from '@/queries/sql/reports/getFunnel'; +export * from '@/queries/sql/reports/getJourney'; +export * from '@/queries/sql/reports/getRetention'; +export * from '@/queries/sql/reports/getInsights'; +export * from '@/queries/sql/reports/getUTM'; +export * from '@/queries/sql/pageviews/getPageviewMetrics'; +export * from '@/queries/sql/pageviews/getPageviewStats'; +export * from '@/queries/sql/sessions/createSession'; +export * from '@/queries/sql/sessions/getWebsiteSession'; +export * from '@/queries/sql/sessions/getSessionData'; +export * from '@/queries/sql/sessions/getSessionDataProperties'; +export * from '@/queries/sql/sessions/getSessionDataValues'; +export * from '@/queries/sql/sessions/getSessionMetrics'; +export * from '@/queries/sql/sessions/getWebsiteSessions'; +export * from '@/queries/sql/sessions/getWebsiteSessionStats'; +export * from '@/queries/sql/sessions/getWebsiteSessionsWeekly'; +export * from '@/queries/sql/sessions/getSessionActivity'; +export * from '@/queries/sql/sessions/getSessionStats'; +export * from '@/queries/sql/sessions/saveSessionData'; +export * from '@/queries/sql/getActiveVisitors'; +export * from '@/queries/sql/getChannelMetrics'; +export * from '@/queries/sql/getRealtimeActivity'; +export * from '@/queries/sql/getRealtimeData'; +export * from '@/queries/sql/getValues'; +export * from '@/queries/sql/getWebsiteDateRange'; +export * from '@/queries/sql/getWebsiteStats'; diff --git a/src/queries/prisma/report.ts b/src/queries/prisma/report.ts index a0e6364c..4feb9fb8 100644 --- a/src/queries/prisma/report.ts +++ b/src/queries/prisma/report.ts @@ -1,6 +1,6 @@ import { Prisma, Report } from '@prisma/client'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import ReportFindManyArgs = Prisma.ReportFindManyArgs; async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise { @@ -19,11 +19,11 @@ export async function getReports( criteria: ReportFindManyArgs, pageParams: PageParams = {}, ): Promise> { - const { query } = pageParams; + const { search } = pageParams; const where: Prisma.ReportWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [ + ...prisma.getSearchParameters(search, [ { name: 'contains' }, { description: 'contains' }, { type: 'contains' }, diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts index e516c446..4fddd76a 100644 --- a/src/queries/prisma/team.ts +++ b/src/queries/prisma/team.ts @@ -1,8 +1,8 @@ import { Prisma, Team } from '@prisma/client'; -import { ROLES } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import { ROLES } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import TeamFindManyArgs = Prisma.TeamFindManyArgs; export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise { diff --git a/src/queries/prisma/teamUser.ts b/src/queries/prisma/teamUser.ts index d172dd5a..0695f01c 100644 --- a/src/queries/prisma/teamUser.ts +++ b/src/queries/prisma/teamUser.ts @@ -1,7 +1,7 @@ import { Prisma, TeamUser } from '@prisma/client'; -import { uuid } from 'lib/crypto'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import { uuid } from '@/lib/crypto'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs; export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise { diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 0c8e3520..3edae700 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -1,8 +1,8 @@ import { Prisma } from '@prisma/client'; -import { ROLES } from 'lib/constants'; -import prisma from 'lib/prisma'; -import { PageResult, Role, User, PageParams } from 'lib/types'; -import { getRandomChars } from 'next-basics'; +import { ROLES } from '@/lib/constants'; +import prisma from '@/lib/prisma'; +import { PageResult, Role, User, PageParams } from '@/lib/types'; +import { getRandomChars } from '@/lib/crypto'; import UserFindManyArgs = Prisma.UserFindManyArgs; export interface GetUserOptions { diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index dc1ec438..96463501 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -1,9 +1,9 @@ import { Prisma, Website } from '@prisma/client'; import { getClient } from '@umami/redis-client'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise { return prisma.client.website.findUnique(criteria); @@ -30,11 +30,11 @@ export async function getWebsites( criteria: WebsiteFindManyArgs, pageParams: PageParams, ): Promise> { - const { query } = pageParams; + const { search } = pageParams; const where: Prisma.WebsiteWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [ + ...prisma.getSearchParameters(search, [ { name: 'contains', }, diff --git a/src/queries/analytics/events/getEventDataEvents.ts b/src/queries/sql/events/getEventDataEvents.ts similarity index 93% rename from src/queries/analytics/events/getEventDataEvents.ts rename to src/queries/sql/events/getEventDataEvents.ts index 0b19c5be..432c93a2 100644 --- a/src/queries/analytics/events/getEventDataEvents.ts +++ b/src/queries/sql/events/getEventDataEvents.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataEvents( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/events/getEventDataFields.ts b/src/queries/sql/events/getEventDataFields.ts similarity index 90% rename from src/queries/analytics/events/getEventDataFields.ts rename to src/queries/sql/events/getEventDataFields.ts index 05fee072..33b4e0f5 100644 --- a/src/queries/analytics/events/getEventDataFields.ts +++ b/src/queries/sql/events/getEventDataFields.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataFields( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts similarity index 90% rename from src/queries/analytics/events/getEventDataProperties.ts rename to src/queries/sql/events/getEventDataProperties.ts index e2cf0828..73fb8fec 100644 --- a/src/queries/analytics/events/getEventDataProperties.ts +++ b/src/queries/sql/events/getEventDataProperties.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataProperties( ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] diff --git a/src/queries/analytics/events/getEventDataStats.ts b/src/queries/sql/events/getEventDataStats.ts similarity index 90% rename from src/queries/analytics/events/getEventDataStats.ts rename to src/queries/sql/events/getEventDataStats.ts index adeeda46..98347960 100644 --- a/src/queries/analytics/events/getEventDataStats.ts +++ b/src/queries/sql/events/getEventDataStats.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; export async function getEventDataStats( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/events/getEventDataUsage.ts b/src/queries/sql/events/getEventDataUsage.ts similarity index 86% rename from src/queries/analytics/events/getEventDataUsage.ts rename to src/queries/sql/events/getEventDataUsage.ts index 1d146c9c..1f2bf833 100644 --- a/src/queries/analytics/events/getEventDataUsage.ts +++ b/src/queries/sql/events/getEventDataUsage.ts @@ -1,5 +1,5 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from 'lib/db'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db'; export function getEventDataUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { return runQuery({ diff --git a/src/queries/analytics/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts similarity index 91% rename from src/queries/analytics/events/getEventDataValues.ts rename to src/queries/sql/events/getEventDataValues.ts index 63101824..c8d63362 100644 --- a/src/queries/analytics/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataValues( ...args: [ diff --git a/src/queries/analytics/events/getEventMetrics.ts b/src/queries/sql/events/getEventMetrics.ts similarity index 90% rename from src/queries/analytics/events/getEventMetrics.ts rename to src/queries/sql/events/getEventMetrics.ts index 504cea11..d06789f4 100644 --- a/src/queries/analytics/events/getEventMetrics.ts +++ b/src/queries/sql/events/getEventMetrics.ts @@ -1,8 +1,8 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_TYPE } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters, WebsiteEventMetric } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters, WebsiteEventMetric } from '@/lib/types'; export async function getEventMetrics( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/events/getEventUsage.ts b/src/queries/sql/events/getEventUsage.ts similarity index 88% rename from src/queries/analytics/events/getEventUsage.ts rename to src/queries/sql/events/getEventUsage.ts index 8baefe06..0e1806d6 100644 --- a/src/queries/analytics/events/getEventUsage.ts +++ b/src/queries/sql/events/getEventUsage.ts @@ -1,5 +1,5 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from 'lib/db'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db'; export function getEventUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { return runQuery({ diff --git a/src/queries/analytics/events/getWebsiteEvents.ts b/src/queries/sql/events/getWebsiteEvents.ts similarity index 94% rename from src/queries/analytics/events/getWebsiteEvents.ts rename to src/queries/sql/events/getWebsiteEvents.ts index 21e6270c..5559d5bd 100644 --- a/src/queries/analytics/events/getWebsiteEvents.ts +++ b/src/queries/sql/events/getWebsiteEvents.ts @@ -1,7 +1,7 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { PageParams, QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { PageParams, QueryFilters } from '@/lib/types'; export function getWebsiteEvents( ...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams] diff --git a/src/queries/analytics/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts similarity index 94% rename from src/queries/analytics/events/saveEvent.ts rename to src/queries/sql/events/saveEvent.ts index 2424186a..65ee1175 100644 --- a/src/queries/analytics/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -1,9 +1,9 @@ -import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import clickhouse from 'lib/clickhouse'; -import kafka from 'lib/kafka'; -import prisma from 'lib/prisma'; -import { uuid } from 'lib/crypto'; +import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import clickhouse from '@/lib/clickhouse'; +import kafka from '@/lib/kafka'; +import prisma from '@/lib/prisma'; +import { uuid } from '@/lib/crypto'; import { saveEventData } from './saveEventData'; export async function saveEvent(args: { diff --git a/src/queries/analytics/events/saveEventData.ts b/src/queries/sql/events/saveEventData.ts similarity index 85% rename from src/queries/analytics/events/saveEventData.ts rename to src/queries/sql/events/saveEventData.ts index cb75a91b..7c158da4 100644 --- a/src/queries/analytics/events/saveEventData.ts +++ b/src/queries/sql/events/saveEventData.ts @@ -1,12 +1,12 @@ import { Prisma } from '@prisma/client'; -import { DATA_TYPE } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { flattenJSON, getStringValue } from 'lib/data'; -import clickhouse from 'lib/clickhouse'; -import kafka from 'lib/kafka'; -import prisma from 'lib/prisma'; -import { DynamicData } from 'lib/types'; +import { DATA_TYPE } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { flattenJSON, getStringValue } from '@/lib/data'; +import clickhouse from '@/lib/clickhouse'; +import kafka from '@/lib/kafka'; +import prisma from '@/lib/prisma'; +import { DynamicData } from '@/lib/types'; export async function saveEventData(data: { websiteId: string; diff --git a/src/queries/analytics/getActiveVisitors.ts b/src/queries/sql/getActiveVisitors.ts similarity index 80% rename from src/queries/analytics/getActiveVisitors.ts rename to src/queries/sql/getActiveVisitors.ts index c59a265a..e0225f3a 100644 --- a/src/queries/analytics/getActiveVisitors.ts +++ b/src/queries/sql/getActiveVisitors.ts @@ -1,7 +1,7 @@ import { subMinutes } from 'date-fns'; -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; export async function getActiveVisitors(...args: [websiteId: string]) { return runQuery({ @@ -15,7 +15,7 @@ async function relationalQuery(websiteId: string) { const result = await rawQuery( ` - select count(distinct session_id) x + select count(distinct session_id) as visitors from website_event where website_id = {{websiteId::uuid}} and created_at >= {{startDate}} @@ -32,7 +32,7 @@ async function clickhouseQuery(websiteId: string): Promise<{ x: number }> { const result = await rawQuery( ` select - count(distinct session_id) x + count(distinct session_id) as "visitors" from website_event where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64} diff --git a/src/queries/sql/getChannelMetrics.ts b/src/queries/sql/getChannelMetrics.ts new file mode 100644 index 00000000..a7591a80 --- /dev/null +++ b/src/queries/sql/getChannelMetrics.ts @@ -0,0 +1,59 @@ +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; + +export async function getChannelMetrics(...args: [websiteId: string, filters?: QueryFilters]) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { rawQuery, parseFilters } = prisma; + const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters); + + return rawQuery( + ` + select + referrer_domain as domain, + referrer_query as query, + count(distinct session_id) as visitors + from website_event + where website_id = {websiteId:UUID} + ${filterQuery} + ${dateQuery} + group by 1, 2 + order by visitors desc + `, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + filters: QueryFilters, +): Promise<{ x: string; y: number }[]> { + const { rawQuery, parseFilters } = clickhouse; + const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters); + + const sql = ` + select + referrer_domain as domain, + referrer_query as query, + uniq(session_id) as visitors + from website_event + where website_id = {websiteId:UUID} + ${filterQuery} + ${dateQuery} + group by 1, 2 + order by visitors desc + `; + + return rawQuery(sql, params).then(a => { + return Object.values(a).map(a => { + return { ...a, visitors: Number(a.visitors) }; + }); + }); +} diff --git a/src/queries/analytics/getRealtimeActivity.ts b/src/queries/sql/getRealtimeActivity.ts similarity index 91% rename from src/queries/analytics/getRealtimeActivity.ts rename to src/queries/sql/getRealtimeActivity.ts index e30ce78f..10828885 100644 --- a/src/queries/analytics/getRealtimeActivity.ts +++ b/src/queries/sql/getRealtimeActivity.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { QueryFilters } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; export async function getRealtimeActivity(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ diff --git a/src/queries/analytics/getRealtimeData.ts b/src/queries/sql/getRealtimeData.ts similarity index 98% rename from src/queries/analytics/getRealtimeData.ts rename to src/queries/sql/getRealtimeData.ts index 1af63219..e07dfc31 100644 --- a/src/queries/analytics/getRealtimeData.ts +++ b/src/queries/sql/getRealtimeData.ts @@ -1,4 +1,4 @@ -import { getPageviewStats, getRealtimeActivity, getSessionStats } from 'queries/index'; +import { getPageviewStats, getRealtimeActivity, getSessionStats } from '@/queries/index'; function increment(data: object, key: string) { if (key) { diff --git a/src/queries/analytics/getValues.ts b/src/queries/sql/getValues.ts similarity index 91% rename from src/queries/analytics/getValues.ts rename to src/queries/sql/getValues.ts index f303faff..2d1286f3 100644 --- a/src/queries/analytics/getValues.ts +++ b/src/queries/sql/getValues.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; export async function getValues( ...args: [websiteId: string, column: string, startDate: Date, endDate: Date, search: string] @@ -42,7 +42,7 @@ async function relationalQuery( return rawQuery( ` - select ${column} as "value", count(*) + select ${column} as "value", count(*) as "count" from website_event inner join session on session.session_id = website_event.session_id @@ -98,7 +98,7 @@ async function clickhouseQuery( return rawQuery( ` - select ${column} as value, count(*) + select ${column} as "value", count(*) as "count" from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/src/queries/analytics/getWebsiteDateRange.ts b/src/queries/sql/getWebsiteDateRange.ts similarity index 86% rename from src/queries/analytics/getWebsiteDateRange.ts rename to src/queries/sql/getWebsiteDateRange.ts index ef07712e..953fa5eb 100644 --- a/src/queries/analytics/getWebsiteDateRange.ts +++ b/src/queries/sql/getWebsiteDateRange.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { DEFAULT_RESET_DATE } from 'lib/constants'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; +import { DEFAULT_RESET_DATE } from '@/lib/constants'; export async function getWebsiteDateRange(...args: [websiteId: string]) { return runQuery({ diff --git a/src/queries/analytics/getWebsiteStats.ts b/src/queries/sql/getWebsiteStats.ts similarity index 92% rename from src/queries/analytics/getWebsiteStats.ts rename to src/queries/sql/getWebsiteStats.ts index 061d487e..80f1d578 100644 --- a/src/queries/analytics/getWebsiteStats.ts +++ b/src/queries/sql/getWebsiteStats.ts @@ -1,9 +1,9 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_TYPE } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; -import { EVENT_COLUMNS } from 'lib/constants'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; +import { EVENT_COLUMNS } from '@/lib/constants'; export async function getWebsiteStats( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/pageviews/getPageviewMetrics.ts b/src/queries/sql/pageviews/getPageviewMetrics.ts similarity index 90% rename from src/queries/analytics/pageviews/getPageviewMetrics.ts rename to src/queries/sql/pageviews/getPageviewMetrics.ts index f734b1dd..f6041929 100644 --- a/src/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/src/queries/sql/pageviews/getPageviewMetrics.ts @@ -1,11 +1,17 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getPageviewMetrics( - ...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] + ...args: [ + websiteId: string, + type: string, + filters: QueryFilters, + limit?: number | string, + offset?: number | string, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -17,8 +23,8 @@ async function relationalQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ) { const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = prisma; @@ -80,8 +86,8 @@ async function clickhouseQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ): Promise<{ x: string; y: number }[]> { const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = clickhouse; diff --git a/src/queries/analytics/pageviews/getPageviewStats.ts b/src/queries/sql/pageviews/getPageviewStats.ts similarity index 90% rename from src/queries/analytics/pageviews/getPageviewStats.ts rename to src/queries/sql/pageviews/getPageviewStats.ts index 48b82000..f5ace52c 100644 --- a/src/queries/analytics/pageviews/getPageviewStats.ts +++ b/src/queries/sql/pageviews/getPageviewStats.ts @@ -1,8 +1,8 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { EVENT_COLUMNS, EVENT_TYPE } from 'lib/constants'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants'; +import { QueryFilters } from '@/lib/types'; export async function getPageviewStats(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ diff --git a/src/queries/analytics/reports/getFunnel.ts b/src/queries/sql/reports/getFunnel.ts similarity index 98% rename from src/queries/analytics/reports/getFunnel.ts rename to src/queries/sql/reports/getFunnel.ts index 3a81157f..70b51a9d 100644 --- a/src/queries/analytics/reports/getFunnel.ts +++ b/src/queries/sql/reports/getFunnel.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; const formatResults = (steps: { type: string; value: string }[]) => (results: unknown) => { return steps.map((step: { type: string; value: string }, i: number) => { diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/sql/reports/getGoals.ts similarity index 98% rename from src/queries/analytics/reports/getGoals.ts rename to src/queries/sql/reports/getGoals.ts index 2bb29d8e..eda76050 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/sql/reports/getGoals.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getGoals( ...args: [ diff --git a/src/queries/analytics/reports/getInsights.ts b/src/queries/sql/reports/getInsights.ts similarity index 93% rename from src/queries/analytics/reports/getInsights.ts rename to src/queries/sql/reports/getInsights.ts index 8e6e3289..7178072e 100644 --- a/src/queries/analytics/reports/getInsights.ts +++ b/src/queries/sql/reports/getInsights.ts @@ -1,8 +1,8 @@ -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { QueryFilters } from 'lib/types'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { QueryFilters } from '@/lib/types'; export async function getInsights( ...args: [websiteId: string, fields: { name: string; type?: string }[], filters: QueryFilters] diff --git a/src/queries/analytics/reports/getJourney.ts b/src/queries/sql/reports/getJourney.ts similarity index 97% rename from src/queries/analytics/reports/getJourney.ts rename to src/queries/sql/reports/getJourney.ts index eec500aa..4c43cc03 100644 --- a/src/queries/analytics/reports/getJourney.ts +++ b/src/queries/sql/reports/getJourney.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; interface JourneyResult { e1: string; diff --git a/src/queries/analytics/reports/getRetention.ts b/src/queries/sql/reports/getRetention.ts similarity index 96% rename from src/queries/analytics/reports/getRetention.ts rename to src/queries/sql/reports/getRetention.ts index d69a77d7..23854b60 100644 --- a/src/queries/analytics/reports/getRetention.ts +++ b/src/queries/sql/reports/getRetention.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getRetention( ...args: [ diff --git a/src/queries/analytics/reports/getRevenue.ts b/src/queries/sql/reports/getRevenue.ts similarity index 98% rename from src/queries/analytics/reports/getRevenue.ts rename to src/queries/sql/reports/getRevenue.ts index a2803e85..c9c7b74a 100644 --- a/src/queries/analytics/reports/getRevenue.ts +++ b/src/queries/sql/reports/getRevenue.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getRevenue( ...args: [ diff --git a/src/queries/analytics/reports/getRevenueValues.ts b/src/queries/sql/reports/getRevenueValues.ts similarity index 93% rename from src/queries/analytics/reports/getRevenueValues.ts rename to src/queries/sql/reports/getRevenueValues.ts index 4dcc4a22..a46bf0bf 100644 --- a/src/queries/analytics/reports/getRevenueValues.ts +++ b/src/queries/sql/reports/getRevenueValues.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA, getDatabaseType, POSTGRESQL } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA, getDatabaseType, POSTGRESQL } from '@/lib/db'; export async function getRevenueValues( ...args: [ diff --git a/src/queries/analytics/reports/getUTM.ts b/src/queries/sql/reports/getUTM.ts similarity index 89% rename from src/queries/analytics/reports/getUTM.ts rename to src/queries/sql/reports/getUTM.ts index 4e1af9f0..5463815b 100644 --- a/src/queries/analytics/reports/getUTM.ts +++ b/src/queries/sql/reports/getUTM.ts @@ -1,7 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { safeDecodeURIComponent } from 'next-basics'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getUTM( ...args: [ @@ -84,7 +83,7 @@ function parseParameters(data: any[]) { for (const [key, value] of searchParams) { if (key.match(/^utm_(\w+)$/)) { - const name = safeDecodeURIComponent(value); + const name = value; if (!obj[key]) { obj[key] = { [name]: Number(num) }; } else if (!obj[key][name]) { diff --git a/src/queries/analytics/sessions/createSession.ts b/src/queries/sql/sessions/createSession.ts similarity index 93% rename from src/queries/analytics/sessions/createSession.ts rename to src/queries/sql/sessions/createSession.ts index 7d614499..7605cffc 100644 --- a/src/queries/analytics/sessions/createSession.ts +++ b/src/queries/sql/sessions/createSession.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import prisma from 'lib/prisma'; +import prisma from '@/lib/prisma'; export async function createSession(data: Prisma.SessionCreateInput) { const { diff --git a/src/queries/analytics/sessions/getSessionActivity.ts b/src/queries/sql/sessions/getSessionActivity.ts similarity index 90% rename from src/queries/analytics/sessions/getSessionActivity.ts rename to src/queries/sql/sessions/getSessionActivity.ts index 1fe8bbd3..d7e2a413 100644 --- a/src/queries/analytics/sessions/getSessionActivity.ts +++ b/src/queries/sql/sessions/getSessionActivity.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getSessionActivity( ...args: [websiteId: string, sessionId: string, startDate: Date, endDate: Date] diff --git a/src/queries/analytics/sessions/getSessionData.ts b/src/queries/sql/sessions/getSessionData.ts similarity index 91% rename from src/queries/analytics/sessions/getSessionData.ts rename to src/queries/sql/sessions/getSessionData.ts index ce80b035..a3f1e113 100644 --- a/src/queries/analytics/sessions/getSessionData.ts +++ b/src/queries/sql/sessions/getSessionData.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db'; export async function getSessionData(...args: [websiteId: string, sessionId: string]) { return runQuery({ diff --git a/src/queries/analytics/sessions/getSessionDataProperties.ts b/src/queries/sql/sessions/getSessionDataProperties.ts similarity index 88% rename from src/queries/analytics/sessions/getSessionDataProperties.ts rename to src/queries/sql/sessions/getSessionDataProperties.ts index 1d15ea8d..da02c97e 100644 --- a/src/queries/analytics/sessions/getSessionDataProperties.ts +++ b/src/queries/sql/sessions/getSessionDataProperties.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getSessionDataProperties( ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] diff --git a/src/queries/analytics/sessions/getSessionDataValues.ts b/src/queries/sql/sessions/getSessionDataValues.ts similarity index 90% rename from src/queries/analytics/sessions/getSessionDataValues.ts rename to src/queries/sql/sessions/getSessionDataValues.ts index c02e4adb..3281521a 100644 --- a/src/queries/analytics/sessions/getSessionDataValues.ts +++ b/src/queries/sql/sessions/getSessionDataValues.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getSessionDataValues( ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] diff --git a/src/queries/analytics/sessions/getSessionMetrics.ts b/src/queries/sql/sessions/getSessionMetrics.ts similarity index 85% rename from src/queries/analytics/sessions/getSessionMetrics.ts rename to src/queries/sql/sessions/getSessionMetrics.ts index bb8bc4c5..e3bd1bba 100644 --- a/src/queries/analytics/sessions/getSessionMetrics.ts +++ b/src/queries/sql/sessions/getSessionMetrics.ts @@ -1,11 +1,17 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getSessionMetrics( - ...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] + ...args: [ + websiteId: string, + type: string, + filters: QueryFilters, + limit?: number | string, + offset?: number | string, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -17,8 +23,8 @@ async function relationalQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ) { const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = prisma; @@ -60,8 +66,8 @@ async function clickhouseQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ): Promise<{ x: string; y: number }[]> { const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = clickhouse; diff --git a/src/queries/analytics/sessions/getSessionStats.ts b/src/queries/sql/sessions/getSessionStats.ts similarity index 91% rename from src/queries/analytics/sessions/getSessionStats.ts rename to src/queries/sql/sessions/getSessionStats.ts index 212f15e9..22cc04a7 100644 --- a/src/queries/analytics/sessions/getSessionStats.ts +++ b/src/queries/sql/sessions/getSessionStats.ts @@ -1,8 +1,8 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_COLUMNS, EVENT_TYPE } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getSessionStats(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ diff --git a/src/queries/analytics/sessions/getWebsiteSession.ts b/src/queries/sql/sessions/getWebsiteSession.ts similarity index 96% rename from src/queries/analytics/sessions/getWebsiteSession.ts rename to src/queries/sql/sessions/getWebsiteSession.ts index 2c16741e..45e8640a 100644 --- a/src/queries/analytics/sessions/getWebsiteSession.ts +++ b/src/queries/sql/sessions/getWebsiteSession.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db'; export async function getWebsiteSession(...args: [websiteId: string, sessionId: string]) { return runQuery({ diff --git a/src/queries/analytics/sessions/getWebsiteSessionStats.ts b/src/queries/sql/sessions/getWebsiteSessionStats.ts similarity index 91% rename from src/queries/analytics/sessions/getWebsiteSessionStats.ts rename to src/queries/sql/sessions/getWebsiteSessionStats.ts index 648be140..2463b7ad 100644 --- a/src/queries/analytics/sessions/getWebsiteSessionStats.ts +++ b/src/queries/sql/sessions/getWebsiteSessionStats.ts @@ -1,7 +1,7 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getWebsiteSessionStats( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/sessions/getWebsiteSessions.ts b/src/queries/sql/sessions/getWebsiteSessions.ts similarity index 93% rename from src/queries/analytics/sessions/getWebsiteSessions.ts rename to src/queries/sql/sessions/getWebsiteSessions.ts index d2a827d0..264a084b 100644 --- a/src/queries/analytics/sessions/getWebsiteSessions.ts +++ b/src/queries/sql/sessions/getWebsiteSessions.ts @@ -1,7 +1,7 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { PageParams, QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { PageParams, QueryFilters } from '@/lib/types'; export async function getWebsiteSessions( ...args: [websiteId: string, filters?: QueryFilters, pageParams?: PageParams] diff --git a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts b/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts similarity index 90% rename from src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts rename to src/queries/sql/sessions/getWebsiteSessionsWeekly.ts index 48d4f7a9..58f8d692 100644 --- a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts +++ b/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; -import { QueryFilters } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; export async function getWebsiteSessionsWeekly( ...args: [websiteId: string, filters?: QueryFilters] diff --git a/src/queries/analytics/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts similarity index 87% rename from src/queries/analytics/sessions/saveSessionData.ts rename to src/queries/sql/sessions/saveSessionData.ts index 64bd1d93..35f0c712 100644 --- a/src/queries/analytics/sessions/saveSessionData.ts +++ b/src/queries/sql/sessions/saveSessionData.ts @@ -1,11 +1,11 @@ -import { DATA_TYPE } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { flattenJSON, getStringValue } from 'lib/data'; -import prisma from 'lib/prisma'; -import { DynamicData } from 'lib/types'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import kafka from 'lib/kafka'; -import clickhouse from 'lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { flattenJSON, getStringValue } from '@/lib/data'; +import prisma from '@/lib/prisma'; +import { DynamicData } from '@/lib/types'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import kafka from '@/lib/kafka'; +import clickhouse from '@/lib/clickhouse'; export async function saveSessionData(data: { websiteId: string; diff --git a/src/store/app.ts b/src/store/app.ts index 4d547d4e..0890b7e9 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -7,9 +7,9 @@ import { LOCALE_CONFIG, THEME_CONFIG, TIMEZONE_CONFIG, -} from 'lib/constants'; -import { getItem } from 'next-basics'; -import { getTimezone } from 'lib/date'; +} from '@/lib/constants'; +import { getItem } from '@/lib/storage'; +import { getTimezone } from '@/lib/date'; function getDefaultTheme() { return typeof window !== 'undefined' diff --git a/src/store/dashboard.ts b/src/store/dashboard.ts index 0cfc78b9..a34ec384 100644 --- a/src/store/dashboard.ts +++ b/src/store/dashboard.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -import { DASHBOARD_CONFIG, DEFAULT_WEBSITE_LIMIT } from 'lib/constants'; -import { getItem, setItem } from 'next-basics'; +import { DASHBOARD_CONFIG, DEFAULT_WEBSITE_LIMIT } from '@/lib/constants'; +import { getItem, setItem } from '@/lib/storage'; export const initialState = { showCharts: true, diff --git a/src/store/version.ts b/src/store/version.ts index 3b5afaac..9a889636 100644 --- a/src/store/version.ts +++ b/src/store/version.ts @@ -1,8 +1,8 @@ import { create } from 'zustand'; import { produce } from 'immer'; import semver from 'semver'; -import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from 'lib/constants'; -import { getItem } from 'next-basics'; +import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from '@/lib/constants'; +import { getItem } from '@/lib/storage'; const initialState = { current: CURRENT_VERSION, diff --git a/src/store/websites.ts b/src/store/websites.ts index 1c5c21fc..e9271abd 100644 --- a/src/store/websites.ts +++ b/src/store/websites.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; import { produce } from 'immer'; -import { DateRange } from 'lib/types'; +import { DateRange } from '@/lib/types'; const store = create(() => ({})); diff --git a/src/tracker/index.js b/src/tracker/index.js index 1a9d1b19..aaf05a25 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -5,6 +5,7 @@ location, document, history, + top, } = window; const { hostname, href, origin } = location; const { currentScript, referrer } = document; @@ -34,19 +35,8 @@ /* Helper functions */ - const parseURL = url => { - try { - const { pathname, search, hash } = new URL(url, location.href); - - return pathname + (excludeSearch ? '' : search) + (excludeHash ? '' : hash); - } catch (e) { - return url; - } - }; - const getPayload = () => ({ website, - hostname, screen, language, title, @@ -61,7 +51,17 @@ if (!url) return; currentRef = currentUrl; - currentUrl = parseURL(url.toString()); + currentUrl = new URL(url, location.href); + + if (excludeSearch) { + currentUrl.search = ''; + } + + if (excludeHash) { + currentUrl.hash = ''; + } + + currentUrl = currentUrl.toString(); if (currentUrl !== currentRef) { setTimeout(track, delayDuration); @@ -158,7 +158,9 @@ e.preventDefault(); } return trackElement(parentElement).then(() => { - if (!external) location.href = href; + if (!external) { + (target === '_top' ? top.location : location).href = href; + } }); } } else if (parentElement.tagName === 'BUTTON') { @@ -197,6 +199,7 @@ method: 'POST', body: JSON.stringify({ type, payload }), headers, + credentials: 'omit', }); const data = await res.json(); @@ -246,7 +249,7 @@ }; } - let currentUrl = parseURL(href); + let currentUrl = href; let currentRef = referrer.startsWith(origin) ? '' : referrer; let title = document.title; let cache; diff --git a/tsconfig.json b/tsconfig.json index 82e7166f..efe4861d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2021", + "target": "es2022", "outDir": "./build", "module": "esnext", "moduleResolution": "node", @@ -21,18 +21,10 @@ "noEmit": true, "jsx": "preserve", "incremental": false, - "baseUrl": "./src", "types": ["jest"], "typeRoots": ["node_modules/@types"], "paths": { - "react": ["./node_modules/@types/react"], - "assets/*": ["./assets/*"], - "components/*": ["./components/*"], - "lib/*": ["./lib/*"], - "pages/*": ["./pages/*"], - "queries/*": ["./queries/*"], - "store/*": ["./store/*"], - "styles/*": ["./styles/*"] + "@/*": ["./src/*"] }, "plugins": [ { diff --git a/yarn.lock b/yarn.lock index 9fe7d6e2..df5503d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1117,7 +1117,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== @@ -1796,6 +1796,16 @@ "@formatjs/intl-localematcher" "0.5.8" tslib "2" +"@formatjs/ecma402-abstract@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz#0ee291effe7ee2c340742a6c95d92eacb5e6c00a" + integrity sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg== + dependencies: + "@formatjs/fast-memoize" "2.2.6" + "@formatjs/intl-localematcher" "0.5.10" + decimal.js "10" + tslib "2" + "@formatjs/fast-memoize@2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz#74e64109279d5244f9fc281f3ae90c407cece823" @@ -1803,6 +1813,13 @@ dependencies: tslib "2" +"@formatjs/fast-memoize@2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz#fac0a84207a1396be1f1aa4ee2805b179e9343d1" + integrity sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw== + dependencies: + tslib "2" + "@formatjs/icu-messageformat-parser@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz#a54293dd7f098d6a6f6a084ab08b6d54a3e8c12d" @@ -1812,6 +1829,15 @@ "@formatjs/icu-skeleton-parser" "1.3.6" tslib "^2.1.0" +"@formatjs/icu-messageformat-parser@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.0.tgz#28d22a735114b7309c0d3e43d39f2660917867c8" + integrity sha512-Hp81uTjjdTk3FLh/dggU5NK7EIsVWc5/ZDWrIldmf2rBuPejuZ13CZ/wpVE2SToyi4EiroPTQ1XJcJuZFIxTtw== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/icu-skeleton-parser" "1.8.12" + tslib "2" + "@formatjs/icu-messageformat-parser@2.9.4": version "2.9.4" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz#52501fbdc122a86097644f03ae1117b9ced00872" @@ -1829,6 +1855,14 @@ "@formatjs/ecma402-abstract" "1.11.4" tslib "^2.1.0" +"@formatjs/icu-skeleton-parser@1.8.12": + version "1.8.12" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz#43076747cdbe0f23bfac2b2a956bd8219716680d" + integrity sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + tslib "2" + "@formatjs/icu-skeleton-parser@1.8.8": version "1.8.8" resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz#a16eff7fd040acf096fb1853c99527181d38cf90" @@ -1862,6 +1896,13 @@ dependencies: tslib "^2.1.0" +"@formatjs/intl-localematcher@0.5.10": + version "0.5.10" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz#1e0bd3fc1332c1fe4540cfa28f07e9227b659a58" + integrity sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q== + dependencies: + tslib "2" + "@formatjs/intl-localematcher@0.5.8": version "0.5.8" resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz#b11bbd04bd3551f7cadcb1ef1e231822d0e3c97e" @@ -1890,6 +1931,17 @@ intl-messageformat "10.7.7" tslib "2" +"@formatjs/intl@3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-3.1.3.tgz#457dba081679a5e899c900624331608742df138e" + integrity sha512-yWtB1L4vOr17MLII3bcNRmjx6qBkSupJuA6nJz1zVxpWbJXKQL5vgvjRCehTO3z7HolxFjtLdfV0/RN+bC34Fg== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/fast-memoize" "2.2.6" + "@formatjs/icu-messageformat-parser" "2.11.0" + intl-messageformat "10.7.14" + tslib "2" + "@formatjs/ts-transformer@3.9.4": version "3.9.4" resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.9.4.tgz#14b43628d082cb8cd8bc15c4893197b59903ec2c" @@ -2980,11 +3032,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.14.175": - version "4.14.200" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149" - integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q== - "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -3039,6 +3086,13 @@ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.2.tgz#ad21f9a1ee881817995fd3f7fd33659c87e7b1b7" integrity sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg== +"@types/react-intl@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-3.0.0.tgz#a2cce0024b6cfe403be28ccf67f49d720fa810ec" + integrity sha512-k8F3d05XQGEqSWIfK97bBjZe4z9RruXU9Wa7OZ2iUC5pdeIpzuQDZe/9C2J3Xir5//ZtAkhcv08Wfx3n5TBTQg== + dependencies: + react-intl "*" + "@types/react-window@^1.8.8": version "1.8.8" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" @@ -3061,6 +3115,13 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@16 || 17 || 18 || 19": + version "19.0.8" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.8.tgz#7098e6159f2a61e4f4cef2c1223c044a9bec590e" + integrity sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw== + dependencies: + csstype "^3.0.2" + "@types/resolve@1.20.2": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" @@ -4860,6 +4921,11 @@ decamelize@^5.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== +decimal.js@10: + version "10.5.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22" + integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw== + dedent@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" @@ -6498,6 +6564,16 @@ intl-messageformat-parser@^5.3.7: dependencies: "@formatjs/intl-numberformat" "^5.5.2" +intl-messageformat@10.7.14: + version "10.7.14" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.14.tgz#ddcbbfdb1682afe56da094f21a4ac74fc3c91552" + integrity sha512-mMGnE4E1otdEutV5vLUdCxRJygHB5ozUBxsPB5qhitewssrS/qGruq9bmvIRkkGsNeK5ZWLfYRld18UHGTIifQ== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/fast-memoize" "2.2.6" + "@formatjs/icu-messageformat-parser" "2.11.0" + tslib "2" + intl-messageformat@10.7.7: version "10.7.7" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.7.tgz#42085e1664729d02240a03346e31a2540b1112a0" @@ -7416,7 +7492,7 @@ jsonify@^0.0.1: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== -jsonwebtoken@^9.0.0: +jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -7617,11 +7693,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -7832,10 +7903,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -maxmind@^4.3.6: - version "4.3.23" - resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.3.23.tgz#e6920149c6104cdf0272d378ce3adf9837dc98f5" - integrity sha512-AMm4Eem0J0Y1EQJRVSdi2xevw5bJgUDd+lHyQwu0PvGUtK/4uOb8/uidmsrRZ/ST90UfF48H4ShAeFFWKvZ7bw== +maxmind@^4.3.24: + version "4.3.24" + resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.3.24.tgz#c67a4278777210c857434fa8e82bdd6774e5e661" + integrity sha512-dexrLcjfS2xDGOvdV8XcfQYmyQVpGidMwEG2ld19lXlsB+i+lXRWPzQi81HfwRXR4hxzFr5gT0oAIFyqAAb/Ww== dependencies: mmdb-lib "2.1.1" tiny-lru "11.2.11" @@ -8054,11 +8125,6 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoclone@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== - nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" @@ -8069,15 +8135,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next-basics@^0.39.0: - version "0.39.0" - resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.39.0.tgz#1ec448a1c12966a82067445bfb9319b7e883dd6a" - integrity sha512-5HWf3u7jgx5n4auIkArFP5+EVdyz7kSvxs86o2V4y8/t3J4scdIHgI8BBE6UhzB17WMbMgVql44IfcJH1CQc/w== - dependencies: - bcryptjs "^2.4.3" - jsonwebtoken "^9.0.0" - pure-rand "^6.0.2" - next@15.0.4: version "15.0.4" resolved "https://registry.yarnpkg.com/next/-/next-15.0.4.tgz#7ddad7299204f16c132d7e524cf903f1a513588e" @@ -9176,11 +9233,6 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -property-expr@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" - integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== - proxy-from-env@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" @@ -9199,11 +9251,16 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -pure-rand@^6.0.0, pure-rand@^6.0.2: +pure-rand@^6.0.0: version "6.0.4" resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== +pure-rand@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -9238,10 +9295,10 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -react-basics@^0.125.0: - version "0.125.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.125.0.tgz#6baf3fea503fb4475f51877efa05d1a734b232c6" - integrity sha512-8swjTaKfenwb+NunwzQo16V+dCA/38Kd+PSYWpBFyNmlFzs3Ax2ZgnysxDhW9IgfFr4wR6/0gzD3S31WzXq6Kw== +react-basics@^0.126.0: + version "0.126.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.126.0.tgz#44e7f5e5ab9d411e91e697dd39c6cb53b6222ae0" + integrity sha512-TQtNZMeH5FtJjYxSN72rBmZWlIcs9jK3oVSCUUxfZq9LnFdoFSagTLCrihs3YCnX8vZEJXaJHQsp7lKEfyH5sw== dependencies: "@react-spring/web" "^9.7.3" classnames "^2.3.1" @@ -9268,6 +9325,20 @@ react-hook-form@^7.34.2: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31" integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg== +react-intl@*: + version "7.1.5" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-7.1.5.tgz#086c1b7cfb00ab7c9f62241162aca86520a5dc4c" + integrity sha512-cVvsVdaOnZ85XBXU0Lc2PVGNhGlzl4UBV+aWAGe/zrV5Xr+CEW7izUsAp/fIuwvCsJl9R+aokppm+P7cdhnpUA== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/icu-messageformat-parser" "2.11.0" + "@formatjs/intl" "3.1.3" + "@types/hoist-non-react-statics" "3" + "@types/react" "16 || 17 || 18 || 19" + hoist-non-react-statics "3" + intl-messageformat "10.7.14" + tslib "2" + react-intl@^6.5.5: version "6.8.9" resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.8.9.tgz#ef36b2a19a0eb97afbeaeab9679273fcbf2ea261" @@ -10564,11 +10635,6 @@ topojson-client@^3.1.0: dependencies: commander "2" -toposort@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" - integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== - tough-cookie@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" @@ -11160,18 +11226,10 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yup@^0.32.11: - version "0.32.11" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== - dependencies: - "@babel/runtime" "^7.15.4" - "@types/lodash" "^4.14.175" - lodash "^4.17.21" - lodash-es "^4.17.21" - nanoclone "^0.2.1" - property-expr "^2.0.4" - toposort "^2.0.2" +zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== zustand@^4.5.5: version "4.5.6"