From bface463e2ac4471f9074bed2ba0306ca83910f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Fri, 12 Apr 2024 20:32:12 +0200 Subject: [PATCH] change how we create/edit clients --- apps/api/src/utils/auth.ts | 75 +++++-------- apps/dashboard/package.json | 1 + .../src/app/(app)/create-organization.tsx | 54 ++++------ .../src/components/animate-height.tsx | 21 ++++ .../clients/create-client-success.tsx | 42 +++++--- .../src/components/clients/table.tsx | 2 +- .../overview/overview-live-histogram.tsx | 5 +- apps/dashboard/src/components/ui/switch.tsx | 26 +++++ apps/dashboard/src/modals/AddClient.tsx | 101 +++++++++++------- apps/dashboard/src/modals/EditClient.tsx | 4 +- apps/dashboard/src/trpc/api/routers/client.ts | 27 ++--- .../src/trpc/api/routers/onboarding.ts | 7 +- apps/public/src/styles/globals.css | 2 +- .../migration.sql | 3 + packages/db/prisma/schema.prisma | 2 +- pnpm-lock.yaml | 30 ++++++ 16 files changed, 243 insertions(+), 159 deletions(-) create mode 100644 apps/dashboard/src/components/animate-height.tsx create mode 100644 apps/dashboard/src/components/ui/switch.tsx create mode 100644 packages/db/prisma/migrations/20240412180636_cors_nullable/migration.sql diff --git a/apps/api/src/utils/auth.ts b/apps/api/src/utils/auth.ts index d925be7b..1a0df274 100644 --- a/apps/api/src/utils/auth.ts +++ b/apps/api/src/utils/auth.ts @@ -23,17 +23,14 @@ export async function validateSdkRequest( const clientSecret = clientSecretNew || clientSecretOld; const origin = headers.origin; + // Temp log + logger.info( + { clientId, origin: origin ? origin : 'empty' }, + 'validateSdkRequest' + ); + if (!clientId) { - logger.error( - { - clientId, - clientSecret, - origin, - headers, - }, - 'validateSdkRequest: Missing client id' - ); - throw new Error('Missing client id'); + throw new Error('Ingestion: Missing client id'); } const client = await db.client.findUnique({ @@ -43,58 +40,34 @@ export async function validateSdkRequest( }); if (!client) { - logger.error( - { - clientId, - clientSecret, - origin, - headers, - }, - 'validateSdkRequest: Invalid client id' - ); - throw new Error('Invalid client id'); + throw new Error('Ingestion: Invalid client id'); } - if (client.secret) { - if (!(await verifyPassword(clientSecret || '', client.secret))) { - logger.error( - { - clientId, - clientSecret, - origin, - headers, - }, - 'validateSdkRequest: Invalid client secret' - ); - throw new Error('Invalid client secret'); - } - } else if (client.cors !== '*') { + if (!client.projectId) { + throw new Error('Ingestion: Client has no project'); + } + + if (client.cors) { const domainAllowed = client.cors.split(',').find((domain) => { if (cleanDomain(domain) === cleanDomain(origin || '')) { return true; } }); - if (!domainAllowed) { - logger.error( - { - clientId, - clientSecret, - client, - origin, - headers, - }, - 'validateSdkRequest: Invalid cors settings' - ); - throw new Error('Invalid cors settings'); + if (domainAllowed) { + return client.projectId; + } + + // Check if cors is a wildcard + } + + if (client.secret && clientSecret) { + if (await verifyPassword(clientSecret, client.secret)) { + return client.projectId; } } - if (!client.projectId) { - throw new Error('No project id found for client'); - } - - return client.projectId; + throw new Error('Ingestion: Invalid client secret'); } export async function validateExportRequest( diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index cc97c829..384c5254 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toggle": "^1.0.3", diff --git a/apps/dashboard/src/app/(app)/create-organization.tsx b/apps/dashboard/src/app/(app)/create-organization.tsx index 46e4d3a1..553f2f81 100644 --- a/apps/dashboard/src/app/(app)/create-organization.tsx +++ b/apps/dashboard/src/app/(app)/create-organization.tsx @@ -1,12 +1,14 @@ 'use client'; +import { useState } from 'react'; +import AnimateHeight from '@/components/animate-height'; import { CreateClientSuccess } from '@/components/clients/create-client-success'; import { LogoSquare } from '@/components/logo'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button, buttonVariants } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Switch } from '@/components/ui/switch'; import { api, handleError } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -16,33 +18,23 @@ import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -const validation = z - .object({ - organization: z.string().min(3), - project: z.string().min(3), - cors: z.string().nullable(), - tab: z.string(), - }) - .refine( - (data) => - data.tab === 'other' || (data.tab === 'website' && data.cors !== ''), - { - message: 'Cors is required', - path: ['cors'], - } - ); +const validation = z.object({ + organization: z.string().min(3), + project: z.string().min(3), + cors: z.string().url().or(z.literal('')), +}); type IForm = z.infer; export function CreateOrganization() { const router = useRouter(); + const [hasDomain, setHasDomain] = useState(true); const form = useForm({ resolver: zodResolver(validation), defaultValues: { organization: '', project: '', cors: '', - tab: 'website', }, }); const mutation = api.onboarding.organziation.useMutation({ @@ -51,7 +43,7 @@ export function CreateOrganization() { const onSubmit: SubmitHandler = (values) => { mutation.mutate({ ...values, - cors: values.tab === 'website' ? values.cors : null, + cors: hasDomain ? values.cors : null, }); }; @@ -128,29 +120,19 @@ export function CreateOrganization() { /> - form.setValue('tab', val)} - className="h-28" - > - - Website - Other - - - +
+ + - - -
- 🔑 You will get a secret to use for your API requests. -
-
- +
+
- {secret ? ( - - ) : ( + {secret && ( +
+ + {cors && ( +

+ You will only need the secret if you want to send server events. +

+ )} +
+ )} + {cors && (
- +
{cors}
@@ -39,7 +49,15 @@ export function CreateClientSuccess({ id, secret, cors }: Props) { Get started! - Read our documentation to get started. Easy peasy! + Read our{' '} + + documentation + {' '} + to get started. Easy peasy!
diff --git a/apps/dashboard/src/components/clients/table.tsx b/apps/dashboard/src/components/clients/table.tsx index a0f69d68..68dfe4c1 100644 --- a/apps/dashboard/src/components/clients/table.tsx +++ b/apps/dashboard/src/components/clients/table.tsx @@ -14,7 +14,7 @@ export const columns: ColumnDef[] = [
{row.original.name}
- {row.original.project.name} + {row.original.project?.name ?? 'No project'}
); diff --git a/apps/dashboard/src/components/overview/overview-live-histogram.tsx b/apps/dashboard/src/components/overview/overview-live-histogram.tsx index d6067c14..0ab74ac8 100644 --- a/apps/dashboard/src/components/overview/overview-live-histogram.tsx +++ b/apps/dashboard/src/components/overview/overview-live-histogram.tsx @@ -1,12 +1,11 @@ 'use client'; -import { Fragment } from 'react'; import { api } from '@/trpc/client'; import { cn } from '@/utils/cn'; -import AnimateHeight from 'react-animate-height'; import type { IChartInput } from '@openpanel/validation'; +import AnimateHeight from '../animate-height'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { useOverviewOptions } from './useOverviewOptions'; @@ -135,7 +134,7 @@ interface WrapperProps { function Wrapper({ open, children, count }: WrapperProps) { return ( - +
Last 30 minutes
diff --git a/apps/dashboard/src/components/ui/switch.tsx b/apps/dashboard/src/components/ui/switch.tsx new file mode 100644 index 00000000..6ce8b65a --- /dev/null +++ b/apps/dashboard/src/components/ui/switch.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { cn } from '@/utils/cn'; +import * as SwitchPrimitives from '@radix-ui/react-switch'; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/apps/dashboard/src/modals/AddClient.tsx b/apps/dashboard/src/modals/AddClient.tsx index da19d258..f2c4d68e 100644 --- a/apps/dashboard/src/modals/AddClient.tsx +++ b/apps/dashboard/src/modals/AddClient.tsx @@ -1,12 +1,14 @@ 'use client'; +import { useState } from 'react'; +import AnimateHeight from '@/components/animate-height'; import { CreateClientSuccess } from '@/components/clients/create-client-success'; import { Button, buttonVariants } from '@/components/ui/button'; import { Combobox } from '@/components/ui/combobox'; import { DialogFooter } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Switch } from '@/components/ui/switch'; import { useAppParams } from '@/hooks/useAppParams'; import { api, handleError } from '@/trpc/client'; import { cn } from '@/utils/cn'; @@ -21,21 +23,12 @@ import { z } from 'zod'; import { popModal } from '.'; import { ModalContent, ModalHeader } from './Modal/Container'; -const validation = z - .object({ - name: z.string().min(1), - cors: z.string().nullable(), - tab: z.string(), - projectId: z.string(), - }) - .refine( - (data) => - data.tab === 'other' || (data.tab === 'website' && data.cors !== ''), - { - message: 'Cors is required', - path: ['cors'], - } - ); +const validation = z.object({ + name: z.string().min(1), + cors: z.string().url().or(z.literal('')), + projectId: z.string(), + type: z.enum(['read', 'write', 'root']), +}); type IForm = z.infer; @@ -50,10 +43,11 @@ export default function AddClient(props: Props) { defaultValues: { name: '', cors: '', - tab: 'website', projectId: props.projectId ?? projectId, + type: 'write', }, }); + const [hasDomain, setHasDomain] = useState(true); const mutation = api.client.create.useMutation({ onError: handleError, onSuccess() { @@ -67,7 +61,7 @@ export default function AddClient(props: Props) { const onSubmit: SubmitHandler = (values) => { mutation.mutate({ name: values.name, - cors: values.tab === 'website' ? values.cors : null, + cors: hasDomain ? values.cors : null, projectId: values.projectId, organizationSlug, }); @@ -134,29 +128,64 @@ export default function AddClient(props: Props) { {...form.register('name')} />
- form.setValue('tab', val)} - className="h-28" - > - - Website - Other - - - + +
+ + - - -
- 🔑 You will get a secret to use for your API requests. -
-
- +
+
+ +
+ { + return ( +
+ + { + field.onChange(value); + }} + items={[ + { + value: 'write', + label: 'Write (for ingestion)', + }, + { + value: 'read', + label: 'Read (access export API)', + }, + { + value: 'root', + label: 'Root (access export API)', + }, + ]} + placeholder="Select a project" + /> +

+ {field.value === 'write' && + 'Write: Is the default client type and is used for ingestion of data'} + {field.value === 'read' && + 'Read: You can access the current projects data from the export API'} + {field.value === 'root' && + 'Root: You can access any projects data from the export API'} +

+
+ ); + }} + /> +
diff --git a/apps/dashboard/src/trpc/api/routers/client.ts b/apps/dashboard/src/trpc/api/routers/client.ts index 0d9ecdbb..a04b0e1d 100644 --- a/apps/dashboard/src/trpc/api/routers/client.ts +++ b/apps/dashboard/src/trpc/api/routers/client.ts @@ -3,6 +3,7 @@ import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; import { z } from 'zod'; import { hashPassword, stripTrailingSlash } from '@openpanel/common'; +import type { Prisma } from '@openpanel/db'; import { db } from '@openpanel/db'; export const clientRouter = createTRPCRouter({ @@ -11,7 +12,7 @@ export const clientRouter = createTRPCRouter({ z.object({ id: z.string(), name: z.string(), - cors: z.string(), + cors: z.string().nullable(), }) ) .mutation(({ input }) => { @@ -21,7 +22,7 @@ export const clientRouter = createTRPCRouter({ }, data: { name: input.name, - cors: input.cors, + cors: input.cors ?? null, }, }); }), @@ -32,23 +33,25 @@ export const clientRouter = createTRPCRouter({ projectId: z.string(), organizationSlug: z.string(), cors: z.string().nullable(), + type: z.enum(['read', 'write', 'root']).optional(), }) ) .mutation(async ({ input }) => { const secret = randomUUID(); - const client = await db.client.create({ - data: { - organizationSlug: input.organizationSlug, - projectId: input.projectId, - name: input.name, - secret: input.cors ? null : await hashPassword(secret), - cors: input.cors ? stripTrailingSlash(input.cors) : '*', - }, - }); + const data: Prisma.ClientCreateArgs['data'] = { + organizationSlug: input.organizationSlug, + projectId: input.projectId, + name: input.name, + type: input.type ?? 'write', + cors: input.cors ? stripTrailingSlash(input.cors) : null, + secret: await hashPassword(secret), + }; + + const client = await db.client.create({ data }); return { ...client, - secret: input.cors ? null : secret, + secret, }; }), remove: protectedProcedure diff --git a/apps/dashboard/src/trpc/api/routers/onboarding.ts b/apps/dashboard/src/trpc/api/routers/onboarding.ts index e1b71776..9ba35064 100644 --- a/apps/dashboard/src/trpc/api/routers/onboarding.ts +++ b/apps/dashboard/src/trpc/api/routers/onboarding.ts @@ -35,15 +35,16 @@ export const onboardingRouter = createTRPCRouter({ name: `${project.name} Client`, organizationSlug: org.slug, projectId: project.id, - cors: input.cors ? stripTrailingSlash(input.cors) : '*', - secret: input.cors ? null : await hashPassword(secret), + type: 'write', + cors: input.cors ? stripTrailingSlash(input.cors) : null, + secret: await hashPassword(secret), }, }); return { client: { ...client, - secret: input.cors ? null : secret, + secret, }, project, organization: transformOrganization(org), diff --git a/apps/public/src/styles/globals.css b/apps/public/src/styles/globals.css index dd8639b1..404dd36e 100644 --- a/apps/public/src/styles/globals.css +++ b/apps/public/src/styles/globals.css @@ -73,7 +73,7 @@ } .fancy-text { - @apply text-transparent inline-block bg-gradient-to-br from-blue-200 to-blue-400 bg-clip-text; + @apply inline-block bg-gradient-to-br from-blue-200 to-blue-400 bg-clip-text text-transparent; } strong { diff --git a/packages/db/prisma/migrations/20240412180636_cors_nullable/migration.sql b/packages/db/prisma/migrations/20240412180636_cors_nullable/migration.sql new file mode 100644 index 00000000..9aada791 --- /dev/null +++ b/packages/db/prisma/migrations/20240412180636_cors_nullable/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "clients" ALTER COLUMN "cors" DROP NOT NULL, +ALTER COLUMN "cors" DROP DEFAULT; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 4081143e..cf3a1be5 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -104,7 +104,7 @@ model Client { projectId String? project Project? @relation(fields: [projectId], references: [id]) organizationSlug String - cors String @default("*") + cors String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 034343a3..e9248aa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.56)(react@18.2.0) + '@radix-ui/react-switch': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0) @@ -5444,6 +5447,33 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.56)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.56)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.56)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.56)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.56)(react@18.2.0) + '@types/react': 18.2.56 + '@types/react-dom': 18.2.19 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} peerDependencies: