diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b1f36f0 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" diff --git a/.gitignore b/.gitignore index a547bf3..2bd1803 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.env + # Logs logs *.log diff --git a/package.json b/package.json index 456675c..40a5521 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,19 @@ "scripts": { "dev": "nodemon src/server/main.ts -w src/server", "start": "NODE_ENV=production ts-node src/server/main.ts", - "build": "vite build" + "build": "vite build", + "db:push": "prisma db push", + "db:generate": "prisma generate", + "db:studio": "prisma studio" }, "dependencies": { "@ant-design/charts": "^1.4.2", "@ant-design/icons": "^5.2.5", + "@prisma/client": "^5.2.0", "antd": "^5.8.5", + "bcryptjs": "^2.4.3", "clsx": "^2.0.0", + "dotenv": "^16.3.1", "express": "^4.18.2", "lodash-es": "^4.17.21", "react": "^18.2.0", @@ -23,6 +29,7 @@ "vite-express": "^0.10.0" }, "devDependencies": { + "@types/bcryptjs": "^2.4.3", "@types/express": "^4.17.17", "@types/lodash-es": "^4.17.9", "@types/node": "^18.17.12", @@ -32,6 +39,7 @@ "autoprefixer": "^10.4.15", "nodemon": "^2.0.22", "postcss": "^8.4.29", + "prisma": "^5.2.0", "tailwindcss": "^3.3.3", "vite": "^4.4.9" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05323a2..446f833 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,12 +7,21 @@ dependencies: '@ant-design/icons': specifier: ^5.2.5 version: 5.2.5(react-dom@18.2.0)(react@18.2.0) + '@prisma/client': + specifier: ^5.2.0 + version: 5.2.0(prisma@5.2.0) antd: specifier: ^5.8.5 version: 5.8.5(react-dom@18.2.0)(react@18.2.0) + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 clsx: specifier: ^2.0.0 version: 2.0.0 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 express: specifier: ^4.18.2 version: 4.18.2 @@ -42,6 +51,9 @@ dependencies: version: 0.10.0 devDependencies: + '@types/bcryptjs': + specifier: ^2.4.3 + version: 2.4.3 '@types/express': specifier: ^4.17.17 version: 4.17.17 @@ -69,6 +81,9 @@ devDependencies: postcss: specifier: ^8.4.29 version: 8.4.29 + prisma: + specifier: ^5.2.0 + version: 5.2.0 tailwindcss: specifier: ^3.3.3 version: 3.3.3(ts-node@10.9.1) @@ -1519,6 +1534,28 @@ packages: fastq: 1.15.0 dev: true + /@prisma/client@5.2.0(prisma@5.2.0): + resolution: {integrity: sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==} + engines: {node: '>=16.13'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f + prisma: 5.2.0 + dev: false + + /@prisma/engines-version@5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f: + resolution: {integrity: sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg==} + dev: false + + /@prisma/engines@5.2.0: + resolution: {integrity: sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==} + requiresBuild: true + /@probe.gl/env@3.6.0: resolution: {integrity: sha512-4tTZYUg/8BICC3Yyb9rOeoKeijKbZHRXBEKObrfPmX4sQmYB15ZOUpoVBhAyJkOYVAM8EkPci6Uw5dLCwx2BEQ==} dependencies: @@ -1710,6 +1747,10 @@ packages: resolution: {integrity: sha512-q/qvE40hkg9gcfFBR5JwlWepK+eh2RB93dwUEHtNID+nx+UPsBBK2ilzYtP8QOutw89eR2F0PQ0RfypFSSdz2w==} dev: false + /@types/bcryptjs@2.4.3: + resolution: {integrity: sha512-XTnH9E/rp51aKGsiMtQCHV/owDLW2E9QAxI7ItpWWm6Gi6XO1e4o3VuEYDma0lbitj1vNOBj05Qk8l2BYoiN4A==} + dev: true + /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: @@ -2077,6 +2118,10 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -2536,6 +2581,11 @@ packages: resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==} dev: false + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + /dotignore@0.1.2: resolution: {integrity: sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==} hasBin: true @@ -3774,6 +3824,14 @@ packages: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} dev: false + /prisma@5.2.0: + resolution: {integrity: sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==} + engines: {node: '>=16.13'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 5.2.0 + /probe.gl@3.6.0: resolution: {integrity: sha512-19JydJWI7+DtR4feV+pu4Mn1I5TAc0xojuxVgZdXIyfmTLfUaFnk4OloWK1bKbPtkgGKLr2lnbnCXmpZEcEp9g==} dependencies: diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..8d8f27f --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,142 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id String @id @unique @db.Uuid @default(uuid()) + 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) + + website Website[] +} + +model Website { + id String @id @unique @db.Uuid @default(uuid()) + name String @db.VarChar(100) + domain String? @db.VarChar(500) + shareId String? @unique @db.VarChar(50) + resetAt DateTime? @db.Timestamptz(6) + userId String? @db.Uuid + createdAt DateTime? @default(now()) @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @db.Timestamptz(6) + deletedAt DateTime? @db.Timestamptz(6) + + user User? @relation(fields: [userId], references: [id]) + eventData EventData[] + sessionData SessionData[] + + @@index([userId]) + @@index([createdAt]) + @@index([shareId]) +} + +model Session { + id String @id @unique @db.Uuid + websiteId String @db.Uuid + hostname String? @db.VarChar(100) + browser String? @db.VarChar(20) + os String? @db.VarChar(20) + device String? @db.VarChar(20) + screen String? @db.VarChar(11) + language String? @db.VarChar(35) + country String? @db.Char(2) + subdivision1 String? @db.VarChar(20) + subdivision2 String? @db.VarChar(50) + city String? @db.VarChar(50) + createdAt DateTime? @default(now()) @db.Timestamptz(6) + + websiteEvent WebsiteEvent[] + sessionData SessionData[] + + @@index([createdAt]) + @@index([websiteId]) + @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, hostname]) + @@index([websiteId, createdAt, browser]) + @@index([websiteId, createdAt, os]) + @@index([websiteId, createdAt, device]) + @@index([websiteId, createdAt, screen]) + @@index([websiteId, createdAt, language]) + @@index([websiteId, createdAt, country]) + @@index([websiteId, createdAt, subdivision1]) + @@index([websiteId, createdAt, city]) +} + +model WebsiteEvent { + id String @id() @db.Uuid + websiteId String @db.Uuid + sessionId String @db.Uuid + createdAt DateTime? @default(now()) @db.Timestamptz(6) + urlPath String @db.VarChar(500) + urlQuery String? @db.VarChar(500) + referrerPath String? @db.VarChar(500) + referrerQuery String? @db.VarChar(500) + referrerDomain String? @db.VarChar(500) + pageTitle String? @db.VarChar(500) + eventType Int @default(1) @db.Integer + eventName String? @db.VarChar(50) + + eventData EventData[] + session Session @relation(fields: [sessionId], references: [id]) + + @@index([createdAt]) + @@index([sessionId]) + @@index([websiteId]) + @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, urlPath]) + @@index([websiteId, createdAt, urlQuery]) + @@index([websiteId, createdAt, referrerDomain]) + @@index([websiteId, createdAt, pageTitle]) + @@index([websiteId, createdAt, eventName]) + @@index([websiteId, sessionId, createdAt]) +} + +model EventData { + id String @id() @db.Uuid + websiteId String @db.Uuid + websiteEventId String @db.Uuid + eventKey String @db.VarChar(500) + stringValue String? @db.VarChar(500) + numberValue Decimal? @db.Decimal(19, 4) + dateValue DateTime? @db.Timestamptz(6) + dataType Int @db.Integer + createdAt DateTime? @default(now()) @db.Timestamptz(6) + + website Website @relation(fields: [websiteId], references: [id]) + websiteEvent WebsiteEvent @relation(fields: [websiteEventId], references: [id]) + + @@index([createdAt]) + @@index([websiteId]) + @@index([websiteEventId]) + @@index([websiteId, createdAt]) + @@index([websiteId, createdAt, eventKey]) +} + +model SessionData { + id String @id() @db.Uuid + websiteId String @db.Uuid + sessionId String @db.Uuid + sessionKey String @db.VarChar(500) + stringValue String? @db.VarChar(500) + numberValue Decimal? @db.Decimal(19, 4) + dateValue DateTime? @db.Timestamptz(6) + dataType Int @db.Integer + createdAt DateTime? @default(now()) @db.Timestamptz(6) + deletedAt DateTime? @default(now()) @db.Timestamptz(6) + + website Website @relation(fields: [websiteId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) + + @@index([createdAt]) + @@index([websiteId]) + @@index([sessionId]) +} diff --git a/src/server/main.ts b/src/server/main.ts index 879a7a0..78f5578 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -1,3 +1,4 @@ +import 'dotenv/config'; import express from 'express'; import ViteExpress from 'vite-express'; diff --git a/src/server/model/_client.ts b/src/server/model/_client.ts new file mode 100644 index 0000000..9b6c4ce --- /dev/null +++ b/src/server/model/_client.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); diff --git a/src/server/model/user.ts b/src/server/model/user.ts new file mode 100644 index 0000000..a002af4 --- /dev/null +++ b/src/server/model/user.ts @@ -0,0 +1,46 @@ +import { prisma } from './_client'; +import bcryptjs from 'bcryptjs'; + +async function hashPassword(password: string) { + return await bcryptjs.hash(password, 10); +} + +/** + * Create User + */ +export async function createAdminUser(username: string, password: string) { + const count = await prisma.user.count(); + + if (count > 0) { + throw new Error('Create Admin User Just Only allow in non people exist'); + } + + await prisma.user.create({ + data: { + username, + password: await hashPassword(password), + role: 'admin', + }, + }); +} + +export async function createUser(username: string, password: string) { + await prisma.user.create({ + data: { + username, + password: await hashPassword(password), + role: 'normal', + }, + }); +} + +export async function authUser(username: string, password: string) { + const user = await prisma.user.findFirstOrThrow({ + where: { + username, + password: await hashPassword(password), + }, + }); + + return user; +}