diff --git a/apps/api/src/controllers/event.controller.ts b/apps/api/src/controllers/event.controller.ts index 2ce8ef35..749dee7a 100644 --- a/apps/api/src/controllers/event.controller.ts +++ b/apps/api/src/controllers/event.controller.ts @@ -15,14 +15,34 @@ export async function postEvent( const ip = getClientIp(request)!; const ua = request.headers['user-agent']!; const origin = request.headers.origin!; - const salts = await getSalts(); + const projectId = request.client?.projectId; + + if (!projectId) { + reply.status(400).send('missing origin'); + return; + } + + const [salts, geo] = await Promise.all([getSalts(), parseIp(ip)]); const currentDeviceId = generateDeviceId({ salt: salts.current, - origin: origin, + origin: projectId, ip, ua, }); const previousDeviceId = generateDeviceId({ + salt: salts.previous, + origin: projectId, + ip, + ua, + }); + // TODO: Remove after 2024-09-26 + const currentDeviceIdDeprecated = generateDeviceId({ + salt: salts.current, + origin, + ip, + ua, + }); + const previousDeviceIdDeprecated = generateDeviceId({ salt: salts.previous, origin, ip, @@ -34,13 +54,15 @@ export async function postEvent( payload: { projectId: request.projectId, headers: { - origin, ua, }, event: request.body, - geo: await parseIp(ip), + geo, currentDeviceId, previousDeviceId, + // TODO: Remove after 2024-09-26 + currentDeviceIdDeprecated, + previousDeviceIdDeprecated, }, }); diff --git a/apps/api/src/routes/event.router.ts b/apps/api/src/routes/event.router.ts index 59c31bac..b48f2cb2 100644 --- a/apps/api/src/routes/event.router.ts +++ b/apps/api/src/routes/event.router.ts @@ -17,16 +17,15 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => { reply ) => { try { - const projectId = await validateSdkRequest(req.headers).catch( - (error) => { - logger.error(error, 'Failed to validate sdk request'); - return null; - } - ); - if (!projectId) { + const client = await validateSdkRequest(req.headers).catch((error) => { + logger.error(error, 'Failed to validate sdk request'); + return null; + }); + if (!client?.projectId) { return reply.status(401).send(); } - req.projectId = projectId; + req.projectId = client.projectId; + req.client = client; const bot = req.headers['user-agent'] ? isBot(req.headers['user-agent']) @@ -37,7 +36,7 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => { req.body?.properties?.path) as string | undefined; await createBotEvent({ ...bot, - projectId, + projectId: client.projectId, path: path ?? '', createdAt: new Date(req.body?.timestamp), }); diff --git a/apps/api/src/routes/profile.router.ts b/apps/api/src/routes/profile.router.ts index dfc66105..2c9f1111 100644 --- a/apps/api/src/routes/profile.router.ts +++ b/apps/api/src/routes/profile.router.ts @@ -7,24 +7,25 @@ import type { FastifyPluginCallback } from 'fastify'; const eventRouter: FastifyPluginCallback = (fastify, opts, done) => { fastify.addHook('preHandler', async (req, reply) => { try { - const projectId = await validateSdkRequest(req.headers).catch((error) => { + const client = await validateSdkRequest(req.headers).catch((error) => { logger.error(error, 'Failed to validate sdk request'); return null; }); - if (!projectId) { + if (!client?.projectId) { return reply.status(401).send(); } - req.projectId = projectId; + req.projectId = client.projectId; + req.client = client; const bot = req.headers['user-agent'] ? isBot(req.headers['user-agent']) : null; if (bot) { - reply.log.warn({ ...req.headers, bot }, 'Bot detected (profile)'); - reply.status(202).send('OK'); + return reply.status(202).send('OK'); } } catch (e) { + logger.error(e, 'Failed to create bot event'); reply.status(401).send(); return; } diff --git a/apps/api/src/utils/auth.ts b/apps/api/src/utils/auth.ts index f7883cb0..470edf6f 100644 --- a/apps/api/src/utils/auth.ts +++ b/apps/api/src/utils/auth.ts @@ -2,7 +2,7 @@ import type { RawRequestDefaultExpression } from 'fastify'; import jwt from 'jsonwebtoken'; import { verifyPassword } from '@openpanel/common'; -import type { IServiceClient } from '@openpanel/db'; +import type { Client, IServiceClient } from '@openpanel/db'; import { ClientType, db } from '@openpanel/db'; import { logger } from './logger'; @@ -36,7 +36,7 @@ class SdkAuthError extends Error { export async function validateSdkRequest( headers: RawRequestDefaultExpression['headers'] -): Promise { +): Promise { const clientIdNew = headers['openpanel-client-id'] as string; const clientIdOld = headers['mixan-client-id'] as string; const clientSecretNew = headers['openpanel-client-secret'] as string; @@ -83,17 +83,17 @@ export async function validateSdkRequest( }); if (domainAllowed) { - return client.projectId; + return client; } if (client.cors === '*' && origin) { - return client.projectId; + return client; } } if (client.secret && clientSecret) { if (await verifyPassword(clientSecret, client.secret)) { - return client.projectId; + return client; } } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-details.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-details.tsx index 89e4d8b2..22ef20c5 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-details.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-details.tsx @@ -33,6 +33,10 @@ export function EventDetails({ event, open, setOpen }: Props) { const [, setEvents] = useEventQueryNamesFilter({ shallow: false }); const common = [ + { + name: 'Origin', + value: event.origin, + }, { name: 'Duration', value: event.duration ? round(event.duration / 1000, 1) : undefined, @@ -154,7 +158,7 @@ export function EventDetails({ event, open, setOpen }: Props) { {properties.map((item) => ( { setFilter( diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/list-projects.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/list-projects.tsx index 04f94a49..673c0332 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/list-projects.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/list-projects.tsx @@ -83,9 +83,9 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) { Client ID: ...{item.id.slice(-12)}
- {item.secret - ? 'Secret: Hidden' - : `Website: ${item.cors}`} + {item.cors && + item.cors !== '*' && + `Website: ${item.cors}`}
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx index 2cc3e0b4..a1e8cf99 100644 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx +++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx @@ -1,7 +1,7 @@ 'use client'; import { ButtonContainer } from '@/components/button-container'; -import { InputWithLabel } from '@/components/forms/input-with-label'; +import CopyInput from '@/components/forms/copy-input'; import { LinkButton } from '@/components/ui/button'; import { useClientSecret } from '@/hooks/useClientSecret'; import { LockIcon } from 'lucide-react'; @@ -36,13 +36,13 @@ const Connect = ({ project }: Props) => { } > -
+
Credentials
- - + +
{project.types.map((type) => { const Component = { diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx index e403ec87..e0758b36 100644 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx +++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx @@ -1,5 +1,3 @@ -import { cookies } from 'next/headers'; - import { getCurrentOrganizations, getProjectWithClients } from '@openpanel/db'; import OnboardingConnect from './onboarding-connect'; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx index a92d15a3..2a234f6c 100644 --- a/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx +++ b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx @@ -4,7 +4,8 @@ import { useEffect } from 'react'; import AnimateHeight from '@/components/animate-height'; import { ButtonContainer } from '@/components/button-container'; import { CheckboxItem } from '@/components/forms/checkbox-item'; -import { InputWithLabel } from '@/components/forms/input-with-label'; +import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label'; +import TagInput from '@/components/forms/tag-input'; import { Button } from '@/components/ui/button'; import { Combobox } from '@/components/ui/combobox'; import { Label } from '@/components/ui/label'; @@ -155,11 +156,41 @@ const Tracking = ({ >
- ( + + + tag === '*' ? 'Allow domains' : tag + } + onChange={(newValue) => { + field.onChange( + newValue + .map((item) => { + const trimmed = item.trim(); + if ( + trimmed.startsWith('http://') || + trimmed.startsWith('https://') || + trimmed === '*' + ) { + return trimmed; + } + return `https://${trimmed}`; + }) + .join(',') + ); + }} + /> + + )} />
diff --git a/apps/dashboard/src/components/clients/create-client-success.tsx b/apps/dashboard/src/components/clients/create-client-success.tsx index 29363d5f..ce151734 100644 --- a/apps/dashboard/src/components/clients/create-client-success.tsx +++ b/apps/dashboard/src/components/clients/create-client-success.tsx @@ -1,9 +1,9 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { clipboard } from '@/utils/clipboard'; -import { Copy, RocketIcon } from 'lucide-react'; +import { RocketIcon } from 'lucide-react'; import type { IServiceClient } from '@openpanel/db'; +import CopyInput from '../forms/copy-input'; import { Label } from '../ui/label'; type Props = IServiceClient; @@ -11,25 +11,10 @@ type Props = IServiceClient; export function CreateClientSuccess({ id, secret, cors }: Props) { return (
- + {secret && (
- + {cors && (

You will only need the secret if you want to send server events. @@ -40,7 +25,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) { {cors && (

-
+
{cors}
diff --git a/apps/dashboard/src/components/forms/copy-input.tsx b/apps/dashboard/src/components/forms/copy-input.tsx new file mode 100644 index 00000000..d80c3b0f --- /dev/null +++ b/apps/dashboard/src/components/forms/copy-input.tsx @@ -0,0 +1,28 @@ +import { clipboard } from '@/utils/clipboard'; +import { cn } from '@/utils/cn'; +import { CopyIcon } from 'lucide-react'; + +import { Label } from '../ui/label'; + +type Props = { + label: React.ReactNode; + value: string; + className?: string; +}; + +const CopyInput = ({ label, value, className }: Props) => { + return ( + + ); +}; + +export default CopyInput; diff --git a/apps/dashboard/src/components/forms/input-with-label.tsx b/apps/dashboard/src/components/forms/input-with-label.tsx index da47ba3e..1c698d16 100644 --- a/apps/dashboard/src/components/forms/input-with-label.tsx +++ b/apps/dashboard/src/components/forms/input-with-label.tsx @@ -6,39 +6,56 @@ import type { InputProps } from '../ui/input'; import { Label } from '../ui/label'; import { Tooltiper } from '../ui/tooltip'; -type InputWithLabelProps = InputProps & { +type WithLabel = { + children: React.ReactNode; label: string; error?: string | undefined; info?: string; + className?: string; +}; +type InputWithLabelProps = InputProps & Omit; + +export const WithLabel = ({ + children, + className, + label, + info, + error, +}: WithLabel) => { + return ( +
+
+ + {error && ( + +
+ Issues + +
+
+ )} +
+ {children} +
+ ); }; export const InputWithLabel = forwardRef( - ({ label, className, info, ...props }, ref) => { + (props, ref) => { return ( -
-
- - {props.error && ( - -
- Issues - -
-
- )} -
- -
+ + + ); } ); diff --git a/apps/dashboard/src/components/forms/tag-input.tsx b/apps/dashboard/src/components/forms/tag-input.tsx new file mode 100644 index 00000000..738e22d6 --- /dev/null +++ b/apps/dashboard/src/components/forms/tag-input.tsx @@ -0,0 +1,139 @@ +// Based on Christin Alares tag input component (https://github.com/christianalares/seventy-seven) + +import type { ElementRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/utils/cn'; +import { useAnimate } from 'framer-motion'; +import { XIcon } from 'lucide-react'; + +type Props = { + placeholder: string; + value: string[]; + error?: string; + className?: string; + onChange: (value: string[]) => void; + renderTag?: (tag: string) => string; +}; + +const TagInput = ({ + value: propValue, + onChange, + renderTag, + placeholder, + error, +}: Props) => { + const value = ( + Array.isArray(propValue) ? propValue : propValue ? [propValue] : [] + ).filter(Boolean); + + const [isMarkedForDeletion, setIsMarkedForDeletion] = useState(false); + const inputRef = useRef>(null); + const [inputValue, setInputValue] = useState(''); + + const [scope, animate] = useAnimate(); + + const appendTag = (tag: string) => { + onChange([...value, tag]); + }; + + const removeTag = (tag: string) => { + onChange(value.filter((t) => t !== tag)); + inputRef.current?.focus(); + }; + + useEffect(() => { + if (inputValue.length > 0) { + setIsMarkedForDeletion(false); + } + }, [inputValue]); + + return ( +
+ {value.map((tag, i) => { + const isCreating = false; + + return ( + + {renderTag ? renderTag(tag) : tag} + + + ); + })} + + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + + const tagAlreadyExists = value.some( + (tag) => tag.toLowerCase() === inputValue.toLowerCase() + ); + + if (inputValue) { + if (tagAlreadyExists) { + animate( + `span[data-tag="${inputValue.toLowerCase()}"]`, + { + scale: [1, 1.3, 1], + }, + { + duration: 0.3, + } + ); + return; + } + + appendTag(inputValue); + setInputValue(''); + } + } + + if (e.key === 'Backspace' && inputValue === '') { + if (!isMarkedForDeletion) { + setIsMarkedForDeletion(true); + return; + } + const last = value[value.length - 1]; + if (last) { + removeTag(last); + } + setIsMarkedForDeletion(false); + setInputValue(''); + } + }} + /> +
+ ); +}; + +export default TagInput; diff --git a/apps/dashboard/src/components/ui/checkbox.tsx b/apps/dashboard/src/components/ui/checkbox.tsx index aa91bd4b..098485e9 100644 --- a/apps/dashboard/src/components/ui/checkbox.tsx +++ b/apps/dashboard/src/components/ui/checkbox.tsx @@ -12,7 +12,7 @@ const Checkbox = React.forwardRef< , React.ComponentPropsWithoutRef ->((props, ref) => ( -