diff --git a/index.html b/index.html index b3d805c..fbb2a4c 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ diff --git a/package.json b/package.json index e81f4d5..a395282 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@ant-design/charts": "^1.4.2", "@ant-design/icons": "^5.2.5", + "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.2.0", "@tanstack/react-query": "^4.33.0", "@trpc/client": "^10.38.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8de1189..7f35978 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ dependencies: '@ant-design/icons': specifier: ^5.2.5 version: 5.2.5(react-dom@18.2.0)(react@18.2.0) + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.2.2 '@prisma/client': specifier: ^5.2.0 version: 5.2.0(prisma@5.2.0) @@ -1651,6 +1654,11 @@ packages: engines: {node: '>=6.0.0'} dev: false + /@noble/hashes@1.3.2: + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1672,6 +1680,12 @@ packages: fastq: 1.15.0 dev: true + /@paralleldrive/cuid2@2.2.2: + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + dependencies: + '@noble/hashes': 1.3.2 + dev: false + /@prisma/client@5.2.0(prisma@5.2.0): resolution: {integrity: sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==} engines: {node: '>=16.13'} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 908df5c..3a0616a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,14 +8,14 @@ generator client { } model User { - id String @id @unique @default(uuid()) @db.Uuid + id String @id @unique @default(cuid()) @db.VarChar(30) username String @unique @db.VarChar(255) password String @db.VarChar(60) role String @db.VarChar(50) createdAt DateTime? @default(now()) @db.Timestamptz(6) updatedAt DateTime? @updatedAt @db.Timestamptz(6) deletedAt DateTime? @db.Timestamptz(6) - currentWorkspaceId String? @db.Uuid + currentWorkspaceId String? @db.VarChar(30) currentWorkspace Workspace? @relation(fields: [currentWorkspaceId], references: [id]) @@ -23,7 +23,7 @@ model User { } model Workspace { - id String @id @unique @default(uuid()) @db.Uuid + id String @id @unique @default(cuid()) @db.VarChar(30) name String @db.VarChar(100) createdAt DateTime? @default(now()) @db.Timestamptz(6) updatedAt DateTime? @updatedAt @db.Timestamptz(6) @@ -38,8 +38,8 @@ model Workspace { } model WorkspacesOnUsers { - userId String @db.Uuid - workspaceId String @db.Uuid + userId String @db.VarChar(30) + workspaceId String @db.VarChar(30) role String @db.VarChar(100) createdAt DateTime? @default(now()) @db.Timestamptz(6) updatedAt DateTime? @updatedAt @db.Timestamptz(6) @@ -53,12 +53,12 @@ model WorkspacesOnUsers { } model Website { - id String @id @unique @default(uuid()) @db.Uuid + id String @id @unique @default(cuid()) @db.VarChar(30) name String @db.VarChar(100) domain String? @db.VarChar(500) shareId String? @unique @db.VarChar(50) resetAt DateTime? @db.Timestamptz(6) - workspaceId String @db.Uuid + workspaceId String @db.VarChar(30) createdAt DateTime? @default(now()) @db.Timestamptz(6) updatedAt DateTime? @updatedAt @db.Timestamptz(6) deletedAt DateTime? @db.Timestamptz(6) @@ -76,7 +76,7 @@ model Website { model WebsiteSession { id String @id @unique @db.Uuid - websiteId String @db.Uuid + websiteId String @db.VarChar(30) hostname String? @db.VarChar(100) browser String? @db.VarChar(20) os String? @db.VarChar(20) @@ -109,8 +109,8 @@ model WebsiteSession { } model WebsiteEvent { - id String @id() @default(uuid()) @db.Uuid - websiteId String @db.Uuid + id String @id() @default(cuid()) @db.VarChar(30) + websiteId String @db.VarChar(30) sessionId String @db.Uuid urlPath String @db.VarChar(500) urlQuery String? @db.VarChar(500) @@ -138,9 +138,9 @@ model WebsiteEvent { } model WebsiteEventData { - id String @id() @default(uuid()) @db.Uuid - websiteId String @db.Uuid - websiteEventId String @db.Uuid + id String @id() @default(cuid()) @db.VarChar(30) + websiteId String @db.VarChar(30) + websiteEventId String @db.VarChar(30) eventKey String @db.VarChar(500) stringValue String? @db.VarChar(500) numberValue Decimal? @db.Decimal(19, 4) @@ -159,8 +159,8 @@ model WebsiteEventData { } model WebsiteSessionData { - id String @id() @default(uuid()) @db.Uuid - websiteId String @db.Uuid + id String @id() @default(cuid()) @db.VarChar(30) + websiteId String @db.VarChar(30) sessionId String @db.Uuid stringValue String? @db.VarChar(500) numberValue Decimal? @db.Decimal(19, 4) @@ -179,7 +179,7 @@ model WebsiteSessionData { model TelemetrySession { id String @id @unique @db.Uuid - workspaceId String @db.Uuid + workspaceId String @db.VarChar(30) hostname String? @db.VarChar(100) browser String? @db.VarChar(20) os String? @db.VarChar(20) @@ -196,9 +196,9 @@ model TelemetrySession { } model TelemetryEvent { - id String @id() @default(uuid()) @db.Uuid + id String @id() @default(cuid()) @db.VarChar(30) sessionId String @db.Uuid - workspaceId String @db.Uuid + workspaceId String @db.VarChar(30) eventName String? @db.VarChar(100) urlOrigin String @db.VarChar(500) urlPath String @db.VarChar(500) @@ -213,8 +213,8 @@ model TelemetryEvent { } model Notification { - id String @id() @default(uuid()) @db.Uuid - workspaceId String @db.Uuid + id String @id() @default(cuid()) @db.VarChar(30) + workspaceId String @db.VarChar(30) name String @db.VarChar(100) type String @db.VarChar(100) payload Json @db.Json @@ -228,17 +228,17 @@ model Notification { } model Monitor { - id String @id() @default(uuid()) @db.Uuid - workspaceId String @db.Uuid - name String @db.VarChar(100) - type String @db.VarChar(100) - active Boolean @default(true) @db.Boolean - interval Int @default(20) @db.Integer + id String @id() @default(cuid()) @db.VarChar(30) + workspaceId String @db.VarChar(30) + name String @db.VarChar(100) + type String @db.VarChar(100) + active Boolean @default(true) @db.Boolean + interval Int @default(20) @db.Integer // TODO // maxRetry Int @default(0) @db.Integer // retryInterval Int @default(0) @db.Integer - payload Json @db.Json - createdAt DateTime? @default(now()) @db.Timestamptz(6) + payload Json @db.Json + createdAt DateTime? @default(now()) @db.Timestamptz(6) workspace Workspace @relation(fields: [workspaceId], references: [id]) @@ -250,9 +250,9 @@ model Monitor { } model MonitorEvent { - id String @id @default(uuid()) @db.Uuid + id String @id @default(cuid()) @db.VarChar(30) message String @db.VarChar(500) - monitorId String @db.Uuid + monitorId String @db.VarChar(30) type String @db.VarChar(100) // UP or DOWN createdAt DateTime? @default(now()) @db.Timestamptz(6) @@ -260,8 +260,8 @@ model MonitorEvent { } model MonitorData { - id String @id @default(uuid()) @db.Uuid - monitorId String @db.Uuid + id String @id @default(cuid()) @db.VarChar(30) + monitorId String @db.VarChar(30) value Int @default(0) @db.Integer // -1 means error createdAt DateTime? @default(now()) @db.Timestamptz(6) diff --git a/src/server/model/website.ts b/src/server/model/website.ts index 4922330..14f9d66 100644 --- a/src/server/model/website.ts +++ b/src/server/model/website.ts @@ -1,5 +1,5 @@ import { Website, WebsiteSession } from '@prisma/client'; -import { flattenJSON, hashUuid, isUuid, parseToken } from '../utils/common'; +import { flattenJSON, hashUuid, isCuid, parseToken } from '../utils/common'; import { prisma } from './_client'; import { Request } from 'express'; import { getClientInfo } from '../utils/detect'; @@ -66,7 +66,7 @@ export async function findSession(req: Request): Promise<{ throw new Error('Invalid hostname.'); } - if (!isUuid(websiteId)) { + if (!isCuid(websiteId)) { throw new Error('Invalid website ID.'); } @@ -268,7 +268,7 @@ export async function getWebsiteOnlineUserCount( const res = await prisma.$queryRaw< Ret[] - >`SELECT count(distinct "sessionId") x FROM "WebsiteEvent" where "websiteId" = ${websiteId}::uuid AND "createdAt" >= ${startAt}`; + >`SELECT count(distinct "sessionId") x FROM "WebsiteEvent" where "websiteId" = ${websiteId} AND "createdAt" >= ${startAt}`; return res?.[0].x ?? 0; } diff --git a/src/server/model/workspace.ts b/src/server/model/workspace.ts index c64d601..9ac1cf7 100644 --- a/src/server/model/workspace.ts +++ b/src/server/model/workspace.ts @@ -157,7 +157,7 @@ export async function getWorkspaceWebsitePageview( count(1) y from "WebsiteEvent" ${joinSession} - where "WebsiteEvent"."websiteId" = ${params.websiteId}::uuid + where "WebsiteEvent"."websiteId" = ${params.websiteId} and "WebsiteEvent"."createdAt" between ${ params.startDate }::timestamptz and ${params.endDate}::timestamptz @@ -182,7 +182,7 @@ export async function getWorkspaceWebsiteSession( count(distinct "WebsiteEvent"."sessionId") y from "WebsiteEvent" ${joinSession} - where "WebsiteEvent"."websiteId" = ${params.websiteId}::uuid + where "WebsiteEvent"."websiteId" = ${params.websiteId} and "WebsiteEvent"."createdAt" between ${ params.startDate }::timestamptz and ${params.endDate}::timestamptz @@ -216,7 +216,7 @@ export async function getWorkspaceWebsiteStats( join "Website" on "WebsiteEvent"."websiteId" = "Website"."id" ${joinSession} - where "Website"."id" = ${params.websiteId}::uuid + where "Website"."id" = ${params.websiteId} and "WebsiteEvent"."createdAt" between ${ params.startDate }::timestamptz and ${params.endDate}::timestamptz diff --git a/src/server/router/website.ts b/src/server/router/website.ts index a8c041c..ef545a6 100644 --- a/src/server/router/website.ts +++ b/src/server/router/website.ts @@ -29,7 +29,7 @@ websiteRouter.post( screen: yup.string().max(11), title: yup.string().max(500), url: yup.string().max(500), - website: yup.string().uuid().required(), + website: yup.string().required(), name: yup.string().max(50), }) .required() diff --git a/src/server/router/workspace.ts b/src/server/router/workspace.ts index 54fb000..2a5bad4 100644 --- a/src/server/router/workspace.ts +++ b/src/server/router/workspace.ts @@ -22,11 +22,7 @@ export const workspaceRouter = Router(); workspaceRouter.get( '/websites', validate( - query('workspaceId') - .isString() - .withMessage('workspaceId should be string') - .isUUID() - .withMessage('workspaceId should be UUID') + query('workspaceId').isString().withMessage('workspaceId should be string') ), auth(), workspacePermission(), @@ -42,7 +38,7 @@ workspaceRouter.get( workspaceRouter.post( '/website', validate( - body('workspaceId').isUUID().withMessage('workspaceId should be UUID'), + body('workspaceId').isString(), body('name') .isString() .withMessage('name should be string') @@ -67,10 +63,7 @@ workspaceRouter.post( workspaceRouter.get( '/website/:websiteId', - validate( - query('workspaceId').isUUID().withMessage('workspaceId should be UUID'), - param('websiteId').isUUID().withMessage('workspaceId should be UUID') - ), + validate(query('workspaceId').isString(), param('websiteId').isString()), auth(), workspacePermission(), async (req, res) => { @@ -86,7 +79,7 @@ workspaceRouter.get( workspaceRouter.post( '/website/:websiteId', validate( - body('workspaceId').isUUID().withMessage('workspaceId should be UUID'), + body('workspaceId').isString(), param('websiteId') .isString() .isUUID() @@ -122,10 +115,7 @@ workspaceRouter.post( workspaceRouter.delete( '/:workspaceId/website/:websiteId', - validate( - param('workspaceId').isUUID().withMessage('workspaceId should be UUID'), - param('websiteId').isUUID().withMessage('workspaceId should be UUID') - ), + validate(param('workspaceId').isString(), param('websiteId').isString()), auth(), workspacePermission([ROLES.owner]), async (req, res) => { @@ -141,8 +131,8 @@ workspaceRouter.delete( workspaceRouter.get( '/:workspaceId/website/:websiteId/pageviews', validate( - param('workspaceId').isUUID().withMessage('workspaceId should be UUID'), - param('websiteId').isUUID().withMessage('workspaceId should be UUID'), + param('workspaceId').isString(), + param('websiteId').isString(), query('startAt').isNumeric().withMessage('startAt should be number'), query('endAt').isNumeric().withMessage('startAt should be number') ), @@ -201,8 +191,8 @@ workspaceRouter.get( workspaceRouter.get( '/:workspaceId/website/:websiteId/stats', validate( - param('workspaceId').isUUID().withMessage('workspaceId should be UUID'), - param('websiteId').isUUID().withMessage('workspaceId should be UUID'), + param('workspaceId').isString(), + param('websiteId').isString(), query('startAt').isNumeric().withMessage('startAt should be number'), query('endAt').isNumeric().withMessage('startAt should be number') ), diff --git a/src/server/trpc/trpc.ts b/src/server/trpc/trpc.ts index 0dc0677..ccb9ed9 100644 --- a/src/server/trpc/trpc.ts +++ b/src/server/trpc/trpc.ts @@ -56,14 +56,14 @@ export const systemAdminProcedure = t.procedure.use(isSystemAdmin); export const workspaceProcedure = protectProedure .input( z.object({ - workspaceId: z.string().uuid(), + workspaceId: z.string().cuid2(), }) ) .use(createWorkspacePermissionMiddleware()); export const workspaceOwnerProcedure = protectProedure .input( z.object({ - workspaceId: z.string().uuid(), + workspaceId: z.string().cuid2(), }) ) .use(createWorkspacePermissionMiddleware([ROLES.owner])); diff --git a/src/server/utils/common.ts b/src/server/utils/common.ts index f93a274..558b116 100644 --- a/src/server/utils/common.ts +++ b/src/server/utils/common.ts @@ -7,6 +7,9 @@ import minMax from 'dayjs/plugin/minMax'; import jwt from 'jsonwebtoken'; import _ from 'lodash'; import { getWorkspaceWebsiteDateRange } from '../model/workspace'; +import { isCuid } from '@paralleldrive/cuid2'; + +export { isCuid }; dayjs.extend(minMax); diff --git a/src/server/ws/index.ts b/src/server/ws/index.ts index 278a544..d9125ee 100644 --- a/src/server/ws/index.ts +++ b/src/server/ws/index.ts @@ -2,7 +2,7 @@ import { Server as SocketIOServer } from 'socket.io'; import { Server as HTTPServer } from 'http'; import { jwtVerify } from '../middleware/auth'; import { socketEventBus } from './shared'; -import { isUuid } from '../utils/common'; +import { isCuid } from '../utils/common'; export function initSocketio(httpServer: HTTPServer) { const io = new SocketIOServer(httpServer, { @@ -17,7 +17,7 @@ export function initSocketio(httpServer: HTTPServer) { io.of((name, auth, next) => { const workspaceId = name.replace(/^\//, ''); - next(null, isUuid(workspaceId)); // or false, when the creation is denied + next(null, isCuid(workspaceId)); // or false, when the creation is denied }) .use(async (socket, next) => { // Auth