feat: website/send api is completed
This commit is contained in:
parent
421ee8edc1
commit
08ef1fea91
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,6 @@
|
|||||||
.env
|
.env
|
||||||
|
public/tracker.js
|
||||||
|
geo
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
@ -9,5 +9,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/client/main.tsx"></script>
|
<script type="module" src="/src/client/main.tsx"></script>
|
||||||
|
<script>
|
||||||
|
const el = document.createElement('script');
|
||||||
|
el.src = location.origin + '/tracker.js';
|
||||||
|
el.setAttribute('data-website-id', '53b9a4c1-4a4e-4ec9-8d42-2a2941825bd2'); // For test
|
||||||
|
document.head.append(el);
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
16
package.json
16
package.json
@ -5,8 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon src/server/main.ts -w src/server",
|
"dev": "nodemon src/server/main.ts -w src/server",
|
||||||
"start": "NODE_ENV=production ts-node src/server/main.ts",
|
"start": "NODE_ENV=production ts-node src/server/main.ts",
|
||||||
"build": "vite build && pnpm build:tracker",
|
"build": "vite build && pnpm build:tracker && pnpm build:geo",
|
||||||
"build:tracker": "ts-node scripts/build-tracker.ts",
|
"build:tracker": "ts-node scripts/build-tracker.ts",
|
||||||
|
"build:geo": "ts-node scripts/build-geo.ts",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:studio": "prisma studio"
|
"db:studio": "prisma studio"
|
||||||
@ -15,18 +16,23 @@
|
|||||||
"@ant-design/charts": "^1.4.2",
|
"@ant-design/charts": "^1.4.2",
|
||||||
"@ant-design/icons": "^5.2.5",
|
"@ant-design/icons": "^5.2.5",
|
||||||
"@prisma/client": "^5.2.0",
|
"@prisma/client": "^5.2.0",
|
||||||
|
"@types/uuid": "^9.0.3",
|
||||||
"antd": "^5.8.5",
|
"antd": "^5.8.5",
|
||||||
"axios": "^1.5.0",
|
"axios": "^1.5.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"compose-middleware": "^5.0.1",
|
"compose-middleware": "^5.0.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
|
"dayjs": "^1.11.9",
|
||||||
|
"detect-browser": "^5.3.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
|
"is-localhost-ip": "^2.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"maxmind": "^4.3.11",
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.6",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
@ -34,11 +40,14 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^6.15.0",
|
"react-router": "^6.15.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
|
"request-ip": "^3.3.0",
|
||||||
"socket.io": "^4.7.2",
|
"socket.io": "^4.7.2",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"vite-express": "^0.10.0"
|
"uuid": "^9.0.0",
|
||||||
|
"vite-express": "^0.10.0",
|
||||||
|
"yup": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.3",
|
"@types/bcryptjs": "^2.4.3",
|
||||||
@ -51,12 +60,15 @@
|
|||||||
"@types/passport-jwt": "^3.0.9",
|
"@types/passport-jwt": "^3.0.9",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@types/request-ip": "^0.0.38",
|
||||||
|
"@types/tar": "^6.1.5",
|
||||||
"@vitejs/plugin-react": "^4.0.4",
|
"@vitejs/plugin-react": "^4.0.4",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"postcss": "^8.4.29",
|
"postcss": "^8.4.29",
|
||||||
"prisma": "^5.2.0",
|
"prisma": "^5.2.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
|
"tar": "^6.1.15",
|
||||||
"vite": "^4.4.9"
|
"vite": "^4.4.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
160
pnpm-lock.yaml
160
pnpm-lock.yaml
@ -10,6 +10,9 @@ dependencies:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 5.2.0(prisma@5.2.0)
|
version: 5.2.0(prisma@5.2.0)
|
||||||
|
'@types/uuid':
|
||||||
|
specifier: ^9.0.3
|
||||||
|
version: 9.0.3
|
||||||
antd:
|
antd:
|
||||||
specifier: ^5.8.5
|
specifier: ^5.8.5
|
||||||
version: 5.8.5(react-dom@18.2.0)(react@18.2.0)
|
version: 5.8.5(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -28,6 +31,12 @@ dependencies:
|
|||||||
compression:
|
compression:
|
||||||
specifier: ^1.7.4
|
specifier: ^1.7.4
|
||||||
version: 1.7.4
|
version: 1.7.4
|
||||||
|
dayjs:
|
||||||
|
specifier: ^1.11.9
|
||||||
|
version: 1.11.9
|
||||||
|
detect-browser:
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.3.1
|
specifier: ^16.3.1
|
||||||
version: 16.3.1
|
version: 16.3.1
|
||||||
@ -40,12 +49,18 @@ dependencies:
|
|||||||
express-validator:
|
express-validator:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
|
is-localhost-ip:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
lodash-es:
|
lodash-es:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
maxmind:
|
||||||
|
specifier: ^4.3.11
|
||||||
|
version: 4.3.11
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^3.3.6
|
specifier: ^3.3.6
|
||||||
version: 3.3.6
|
version: 3.3.6
|
||||||
@ -67,6 +82,9 @@ dependencies:
|
|||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.15.0
|
specifier: ^6.15.0
|
||||||
version: 6.15.0(react-dom@18.2.0)(react@18.2.0)
|
version: 6.15.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
request-ip:
|
||||||
|
specifier: ^3.3.0
|
||||||
|
version: 3.3.0
|
||||||
socket.io:
|
socket.io:
|
||||||
specifier: ^4.7.2
|
specifier: ^4.7.2
|
||||||
version: 4.7.2
|
version: 4.7.2
|
||||||
@ -79,9 +97,15 @@ dependencies:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^4.9.5
|
specifier: ^4.9.5
|
||||||
version: 4.9.5
|
version: 4.9.5
|
||||||
|
uuid:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.0
|
||||||
vite-express:
|
vite-express:
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.0
|
version: 0.10.0
|
||||||
|
yup:
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/bcryptjs':
|
'@types/bcryptjs':
|
||||||
@ -114,6 +138,12 @@ devDependencies:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^18.2.7
|
specifier: ^18.2.7
|
||||||
version: 18.2.7
|
version: 18.2.7
|
||||||
|
'@types/request-ip':
|
||||||
|
specifier: ^0.0.38
|
||||||
|
version: 0.0.38
|
||||||
|
'@types/tar':
|
||||||
|
specifier: ^6.1.5
|
||||||
|
version: 6.1.5
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.0.4
|
specifier: ^4.0.4
|
||||||
version: 4.0.4(vite@4.4.9)
|
version: 4.0.4(vite@4.4.9)
|
||||||
@ -132,6 +162,9 @@ devDependencies:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.3.3
|
specifier: ^3.3.3
|
||||||
version: 3.3.3(ts-node@10.9.1)
|
version: 3.3.3(ts-node@10.9.1)
|
||||||
|
tar:
|
||||||
|
specifier: ^6.1.15
|
||||||
|
version: 6.1.15
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.4.9
|
specifier: ^4.4.9
|
||||||
version: 4.4.9(@types/node@18.17.12)
|
version: 4.4.9(@types/node@18.17.12)
|
||||||
@ -1951,6 +1984,12 @@ packages:
|
|||||||
csstype: 3.1.2
|
csstype: 3.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/request-ip@0.0.38:
|
||||||
|
resolution: {integrity: sha512-1yeq8UuK/tUBqLXRY24gjeFvrSNaGNcOcZLQjHlnuw8iu+qE/vTQ64TUcLWorr607NKLfFakdoYEXXHXrLLKCw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.17.12
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/scheduler@0.16.3:
|
/@types/scheduler@0.16.3:
|
||||||
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1970,6 +2009,17 @@ packages:
|
|||||||
'@types/node': 18.17.12
|
'@types/node': 18.17.12
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/tar@6.1.5:
|
||||||
|
resolution: {integrity: sha512-qm2I/RlZij5RofuY7vohTpYNaYcrSQlN2MyjucQc7ZweDwaEWkdN/EeNh6e9zjK6uEm6PwjdMXkcj05BxZdX1Q==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.17.12
|
||||||
|
minipass: 4.2.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/uuid@9.0.3:
|
||||||
|
resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@vitejs/plugin-react@4.0.4(vite@4.4.9):
|
/@vitejs/plugin-react@4.0.4(vite@4.4.9):
|
||||||
resolution: {integrity: sha512-7wU921ABnNYkETiMaZy7XqpueMnpu5VxvVps13MjmCo+utBdD79sZzrApHawHtVX66cCJQQTXFcjH0y9dSUK8g==}
|
resolution: {integrity: sha512-7wU921ABnNYkETiMaZy7XqpueMnpu5VxvVps13MjmCo+utBdD79sZzrApHawHtVX66cCJQQTXFcjH0y9dSUK8g==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
@ -2368,6 +2418,11 @@ packages:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/chownr@2.0.0:
|
||||||
|
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/clamp@1.0.1:
|
/clamp@1.0.1:
|
||||||
resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==}
|
resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -3147,6 +3202,13 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/fs-minipass@2.1.0:
|
||||||
|
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
dependencies:
|
||||||
|
minipass: 3.3.6
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fs.realpath@1.0.0:
|
/fs.realpath@1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
|
||||||
@ -3482,6 +3544,11 @@ packages:
|
|||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-localhost-ip@2.0.0:
|
||||||
|
resolution: {integrity: sha512-vlgs2cSgMOfnKU8c1ewgKPyum9rVrjjLLW2HBdL5i0iAJjOs8NY55ZBd/hqUTaYR0EO9CKZd3hVSC2HlIbygTQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/is-negative-zero@2.0.2:
|
/is-negative-zero@2.0.2:
|
||||||
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
|
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -3784,6 +3851,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==}
|
resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/maxmind@4.3.11:
|
||||||
|
resolution: {integrity: sha512-tJDrKbUzN6PSA88tWgg0L2R4Ln00XwecYQJPFI+RvlF2k1sx6VQYtuQ1SVxm8+bw5tF7GWV4xyb+3/KyzEpPUw==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
mmdb-lib: 2.0.2
|
||||||
|
tiny-lru: 11.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mdn-data@2.0.14:
|
/mdn-data@2.0.14:
|
||||||
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -3842,6 +3917,37 @@ packages:
|
|||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/minipass@3.3.6:
|
||||||
|
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
yallist: 4.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/minipass@4.2.8:
|
||||||
|
resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/minipass@5.0.0:
|
||||||
|
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/minizlib@2.1.2:
|
||||||
|
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
dependencies:
|
||||||
|
minipass: 3.3.6
|
||||||
|
yallist: 4.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/mkdirp@1.0.4:
|
||||||
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ml-array-max@1.2.4:
|
/ml-array-max@1.2.4:
|
||||||
resolution: {integrity: sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==}
|
resolution: {integrity: sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3869,6 +3975,11 @@ packages:
|
|||||||
ml-array-rescale: 1.3.7
|
ml-array-rescale: 1.3.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/mmdb-lib@2.0.2:
|
||||||
|
resolution: {integrity: sha512-shi1I+fCPQonhTi7qyb6hr7hi87R7YS69FlfJiMFuJ12+grx0JyL56gLNzGTYXPU7EhAPkMLliGeyHer0K+AVA==}
|
||||||
|
engines: {node: '>=10', npm: '>=6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/moment@2.29.4:
|
/moment@2.29.4:
|
||||||
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -4207,6 +4318,10 @@ packages:
|
|||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/property-expr@2.0.5:
|
||||||
|
resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/protocol-buffers-schema@3.6.0:
|
/protocol-buffers-schema@3.6.0:
|
||||||
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
|
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -5017,6 +5132,10 @@ packages:
|
|||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/request-ip@3.3.0:
|
||||||
|
resolution: {integrity: sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/resize-observer-polyfill@1.5.1:
|
/resize-observer-polyfill@1.5.1:
|
||||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -5484,6 +5603,18 @@ packages:
|
|||||||
through: 2.3.8
|
through: 2.3.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tar@6.1.15:
|
||||||
|
resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
chownr: 2.0.0
|
||||||
|
fs-minipass: 2.1.0
|
||||||
|
minipass: 5.0.0
|
||||||
|
minizlib: 2.1.2
|
||||||
|
mkdirp: 1.0.4
|
||||||
|
yallist: 4.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/thenify-all@1.6.0:
|
/thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@ -5511,6 +5642,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tiny-case@1.0.3:
|
||||||
|
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/tiny-lru@11.0.1:
|
||||||
|
resolution: {integrity: sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tinycolor2@1.6.0:
|
/tinycolor2@1.6.0:
|
||||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -5604,6 +5744,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/type-fest@2.19.0:
|
||||||
|
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||||
|
engines: {node: '>=12.20'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/type-is@1.6.18:
|
/type-is@1.6.18:
|
||||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -5725,6 +5870,11 @@ packages:
|
|||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/uuid@9.0.0:
|
||||||
|
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/v8-compile-cache-lib@3.0.1:
|
/v8-compile-cache-lib@3.0.1:
|
||||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
|
|
||||||
@ -5857,7 +6007,6 @@ packages:
|
|||||||
|
|
||||||
/yallist@4.0.0:
|
/yallist@4.0.0:
|
||||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/yaml@2.3.2:
|
/yaml@2.3.2:
|
||||||
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
||||||
@ -5876,3 +6025,12 @@ packages:
|
|||||||
/yn@3.1.1:
|
/yn@3.1.1:
|
||||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
/yup@1.2.0:
|
||||||
|
resolution: {integrity: sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==}
|
||||||
|
dependencies:
|
||||||
|
property-expr: 2.0.5
|
||||||
|
tiny-case: 1.0.3
|
||||||
|
toposort: 2.0.2
|
||||||
|
type-fest: 2.19.0
|
||||||
|
dev: false
|
||||||
|
@ -8,16 +8,16 @@ generator client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @default(uuid()) @db.Uuid
|
id String @id @unique @default(uuid()) @db.Uuid
|
||||||
username String @unique @db.VarChar(255)
|
username String @unique @db.VarChar(255)
|
||||||
password String @db.VarChar(60)
|
password String @db.VarChar(60)
|
||||||
role String @db.VarChar(50)
|
role String @db.VarChar(50)
|
||||||
createdAt DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
updatedAt DateTime? @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime? @updatedAt @db.Timestamptz(6)
|
||||||
deletedAt DateTime? @db.Timestamptz(6)
|
deletedAt DateTime? @db.Timestamptz(6)
|
||||||
currentWorkspaceId String? @db.Uuid
|
currentWorkspaceId String? @db.Uuid
|
||||||
|
|
||||||
currentWorkspace Workspace? @relation(fields: [currentWorkspaceId], references: [id])
|
currentWorkspace Workspace? @relation(fields: [currentWorkspaceId], references: [id])
|
||||||
|
|
||||||
workspaces WorkspacesOnUsers[]
|
workspaces WorkspacesOnUsers[]
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ model WebsiteSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model WebsiteEvent {
|
model WebsiteEvent {
|
||||||
id String @id() @db.Uuid
|
id String @id() @default(uuid()) @db.Uuid
|
||||||
websiteId String @db.Uuid
|
websiteId String @db.Uuid
|
||||||
sessionId String @db.Uuid
|
sessionId String @db.Uuid
|
||||||
createdAt DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
@ -130,7 +130,7 @@ model WebsiteEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model WebsiteEventData {
|
model WebsiteEventData {
|
||||||
id String @id() @db.Uuid
|
id String @id() @default(uuid()) @db.Uuid
|
||||||
websiteId String @db.Uuid
|
websiteId String @db.Uuid
|
||||||
websiteEventId String @db.Uuid
|
websiteEventId String @db.Uuid
|
||||||
eventKey String @db.VarChar(500)
|
eventKey String @db.VarChar(500)
|
||||||
@ -151,10 +151,9 @@ model WebsiteEventData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model WebsiteSessionData {
|
model WebsiteSessionData {
|
||||||
id String @id() @db.Uuid
|
id String @id() @default(uuid()) @db.Uuid
|
||||||
websiteId String @db.Uuid
|
websiteId String @db.Uuid
|
||||||
sessionId String @db.Uuid
|
sessionId String @db.Uuid
|
||||||
sessionKey String @db.VarChar(500)
|
|
||||||
stringValue String? @db.VarChar(500)
|
stringValue String? @db.VarChar(500)
|
||||||
numberValue Decimal? @db.Decimal(19, 4)
|
numberValue Decimal? @db.Decimal(19, 4)
|
||||||
dateValue DateTime? @db.Timestamptz(6)
|
dateValue DateTime? @db.Timestamptz(6)
|
||||||
|
55
scripts/build-geo.ts
Normal file
55
scripts/build-geo.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import https from 'https';
|
||||||
|
import zlib from 'zlib';
|
||||||
|
import tar from 'tar';
|
||||||
|
|
||||||
|
if (process.env.VERCEL) {
|
||||||
|
console.log('Vercel environment detected. Skipping geo setup.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = 'GeoLite2-City';
|
||||||
|
|
||||||
|
let url = `https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/master/redist/${db}.tar.gz`;
|
||||||
|
|
||||||
|
if (process.env.MAXMIND_LICENSE_KEY) {
|
||||||
|
url =
|
||||||
|
`https://download.maxmind.com/app/geoip_download` +
|
||||||
|
`?edition_id=${db}&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = path.resolve(__dirname, '../geo');
|
||||||
|
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.mkdirSync(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = (url: string): Promise<NodeJS.WritableStream> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
https.get(url, (res) => {
|
||||||
|
resolve(res.pipe(zlib.createGunzip({})).pipe(tar.t()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
download(url).then(
|
||||||
|
(res) =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
res.on('entry', (entry) => {
|
||||||
|
if (entry.path.endsWith('.mmdb')) {
|
||||||
|
const filename = path.join(dest, path.basename(entry.path));
|
||||||
|
entry.pipe(fs.createWriteStream(filename));
|
||||||
|
|
||||||
|
console.log('Saved geo database:', filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('error', (e) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
res.on('finish', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
@ -13,6 +13,7 @@ vite
|
|||||||
formats: ['iife'],
|
formats: ['iife'],
|
||||||
},
|
},
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
|
outDir: resolve(__dirname, '../public'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
@ -9,8 +9,9 @@ interface HealthBarProps {
|
|||||||
export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
|
export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{props.beats.map((beat) => (
|
{props.beats.map((beat, i) => (
|
||||||
<div
|
<div
|
||||||
|
key={i}
|
||||||
title={beat.title}
|
title={beat.title}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-full w-1 h-4 m-0.5 hover:scale-150 transition-transform',
|
'rounded-full w-1 h-4 m-0.5 hover:scale-150 transition-transform',
|
||||||
|
@ -3,8 +3,9 @@ import express from 'express';
|
|||||||
import 'express-async-errors';
|
import 'express-async-errors';
|
||||||
import ViteExpress from 'vite-express';
|
import ViteExpress from 'vite-express';
|
||||||
import compression from 'compression';
|
import compression from 'compression';
|
||||||
import { userRouter } from './router/user';
|
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
|
import { userRouter } from './router/user';
|
||||||
|
import { websiteRouter } from './router/website';
|
||||||
|
|
||||||
const port = Number(process.env.PORT || 3000);
|
const port = Number(process.env.PORT || 3000);
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ app.use(passport.initialize());
|
|||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
app.use('/api/user', userRouter);
|
app.use('/api/user', userRouter);
|
||||||
|
app.use('/api/website', websiteRouter);
|
||||||
|
|
||||||
app.use((err: any, req: any, res: any, next: any) => {
|
app.use((err: any, req: any, res: any, next: any) => {
|
||||||
res.status(500);
|
res.status(500);
|
||||||
|
246
src/server/model/website.ts
Normal file
246
src/server/model/website.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { Website, WebsiteSession } from '@prisma/client';
|
||||||
|
import { flattenJSON, hashUuid, isUuid } from '../utils/common';
|
||||||
|
import { prisma } from './_client';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { getClientInfo } from '../utils/detect';
|
||||||
|
import {
|
||||||
|
DATA_TYPE,
|
||||||
|
EVENT_NAME_LENGTH,
|
||||||
|
EVENT_TYPE,
|
||||||
|
URL_LENGTH,
|
||||||
|
} from '../utils/const';
|
||||||
|
import type { DynamicData } from '../utils/types';
|
||||||
|
|
||||||
|
export interface WebsiteEventPayload {
|
||||||
|
data?: object;
|
||||||
|
hostname: string;
|
||||||
|
language?: string;
|
||||||
|
referrer?: string;
|
||||||
|
screen?: string;
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
website: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findSession(req: Request): Promise<{
|
||||||
|
id: any;
|
||||||
|
websiteId: string;
|
||||||
|
hostname: string;
|
||||||
|
browser: string;
|
||||||
|
os: any;
|
||||||
|
device: string;
|
||||||
|
screen: string;
|
||||||
|
language: string;
|
||||||
|
country: any;
|
||||||
|
subdivision1: any;
|
||||||
|
subdivision2: any;
|
||||||
|
city: any;
|
||||||
|
workspaceId: string;
|
||||||
|
}> {
|
||||||
|
// Verify payload
|
||||||
|
const { payload } = req.body;
|
||||||
|
|
||||||
|
const {
|
||||||
|
website: websiteId,
|
||||||
|
hostname,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
} = payload as WebsiteEventPayload;
|
||||||
|
|
||||||
|
// Check the hostname value for legality to eliminate dirty data
|
||||||
|
const validHostnameRegex = /^[\w-.]+$/;
|
||||||
|
if (typeof hostname === 'string' && !validHostnameRegex.test(hostname)) {
|
||||||
|
throw new Error('Invalid hostname.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUuid(websiteId)) {
|
||||||
|
throw new Error('Invalid website ID.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find website
|
||||||
|
const website = await loadWebsite(websiteId);
|
||||||
|
|
||||||
|
if (!website) {
|
||||||
|
throw new Error(`Website not found: ${websiteId}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
userAgent,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
ip,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
subdivision2,
|
||||||
|
city,
|
||||||
|
device,
|
||||||
|
} = await getClientInfo(req, payload);
|
||||||
|
|
||||||
|
const sessionId = hashUuid(websiteId, hostname!, ip, userAgent!);
|
||||||
|
|
||||||
|
// Find session
|
||||||
|
let session = await loadSession(sessionId);
|
||||||
|
|
||||||
|
// Create a session if not found
|
||||||
|
if (!session) {
|
||||||
|
try {
|
||||||
|
session = await prisma.websiteSession.create({
|
||||||
|
data: {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = { ...session!, workspaceId: website.workspaceId };
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWebsite(websiteId: string): Promise<Website | null> {
|
||||||
|
const website = await prisma.website.findUnique({
|
||||||
|
where: {
|
||||||
|
id: websiteId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!website || website.deletedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return website;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSession(sessionId: string): Promise<WebsiteSession | null> {
|
||||||
|
const session = await prisma.websiteSession.findUnique({
|
||||||
|
where: {
|
||||||
|
id: sessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveWebsiteEvent(data: {
|
||||||
|
sessionId: string;
|
||||||
|
websiteId: string;
|
||||||
|
urlPath: string;
|
||||||
|
urlQuery?: string;
|
||||||
|
referrerPath?: string;
|
||||||
|
referrerQuery?: string;
|
||||||
|
referrerDomain?: string;
|
||||||
|
pageTitle?: string;
|
||||||
|
eventName?: string;
|
||||||
|
eventData?: any;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
websiteId,
|
||||||
|
sessionId,
|
||||||
|
urlPath,
|
||||||
|
urlQuery,
|
||||||
|
referrerPath,
|
||||||
|
referrerQuery,
|
||||||
|
referrerDomain,
|
||||||
|
eventName,
|
||||||
|
eventData,
|
||||||
|
pageTitle,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const websiteEvent = await prisma.websiteEvent.create({
|
||||||
|
data: {
|
||||||
|
websiteId,
|
||||||
|
sessionId,
|
||||||
|
urlPath: urlPath?.substring(0, URL_LENGTH),
|
||||||
|
urlQuery: urlQuery?.substring(0, URL_LENGTH),
|
||||||
|
referrerPath: referrerPath?.substring(0, URL_LENGTH),
|
||||||
|
referrerQuery: referrerQuery?.substring(0, URL_LENGTH),
|
||||||
|
referrerDomain: referrerDomain?.substring(0, URL_LENGTH),
|
||||||
|
pageTitle,
|
||||||
|
eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||||
|
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventData) {
|
||||||
|
const jsonKeys = flattenJSON(eventData);
|
||||||
|
|
||||||
|
// id, websiteEventId, eventStringValue
|
||||||
|
const flattendData = jsonKeys.map((a) => ({
|
||||||
|
websiteEventId: websiteEvent.id,
|
||||||
|
websiteId,
|
||||||
|
eventKey: a.key,
|
||||||
|
stringValue:
|
||||||
|
a.dynamicDataType === DATA_TYPE.number
|
||||||
|
? parseFloat(a.value).toFixed(4)
|
||||||
|
: a.dynamicDataType === DATA_TYPE.date
|
||||||
|
? a.value.split('.')[0] + 'Z'
|
||||||
|
: a.value.toString(),
|
||||||
|
numberValue: a.dynamicDataType === DATA_TYPE.number ? a.value : null,
|
||||||
|
dateValue:
|
||||||
|
a.dynamicDataType === DATA_TYPE.date ? new Date(a.value) : null,
|
||||||
|
dataType: a.dynamicDataType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await prisma.websiteEventData.createMany({
|
||||||
|
data: flattendData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return websiteEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveWebsiteSessionData(data: {
|
||||||
|
websiteId: string;
|
||||||
|
sessionId: string;
|
||||||
|
sessionData: DynamicData;
|
||||||
|
}) {
|
||||||
|
const { websiteId, sessionId, sessionData } = data;
|
||||||
|
|
||||||
|
const jsonKeys = flattenJSON(sessionData);
|
||||||
|
|
||||||
|
const flattendData = jsonKeys.map((a) => ({
|
||||||
|
websiteId,
|
||||||
|
sessionId,
|
||||||
|
key: a.key,
|
||||||
|
stringValue:
|
||||||
|
a.dynamicDataType === DATA_TYPE.number
|
||||||
|
? parseFloat(a.value).toFixed(4)
|
||||||
|
: a.dynamicDataType === DATA_TYPE.date
|
||||||
|
? a.value.split('.')[0] + 'Z'
|
||||||
|
: a.value.toString(),
|
||||||
|
numberValue: a.dynamicDataType === DATA_TYPE.number ? a.value : null,
|
||||||
|
dateValue: a.dynamicDataType === DATA_TYPE.date ? new Date(a.value) : null,
|
||||||
|
dataType: a.dynamicDataType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return prisma.$transaction([
|
||||||
|
prisma.websiteSessionData.deleteMany({
|
||||||
|
where: {
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.websiteSessionData.createMany({
|
||||||
|
data: flattendData,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
106
src/server/router/website.ts
Normal file
106
src/server/router/website.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { body, validate } from '../middleware/validate';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { COLLECTION_TYPE, HOSTNAME_REGEX } from '../utils/const';
|
||||||
|
import {
|
||||||
|
findSession,
|
||||||
|
saveWebsiteEvent,
|
||||||
|
saveWebsiteSessionData,
|
||||||
|
} from '../model/website';
|
||||||
|
|
||||||
|
export const websiteRouter = Router();
|
||||||
|
|
||||||
|
websiteRouter.post(
|
||||||
|
'/send',
|
||||||
|
validate(
|
||||||
|
body('payload')
|
||||||
|
.exists()
|
||||||
|
.withMessage('payload should be existed')
|
||||||
|
.isObject()
|
||||||
|
.custom(async (input) => {
|
||||||
|
return yup
|
||||||
|
.object()
|
||||||
|
.shape({
|
||||||
|
data: yup.object(),
|
||||||
|
hostname: yup.string().matches(HOSTNAME_REGEX).max(100),
|
||||||
|
language: yup.string().max(35),
|
||||||
|
referrer: yup.string().max(500),
|
||||||
|
screen: yup.string().max(11),
|
||||||
|
title: yup.string().max(500),
|
||||||
|
url: yup.string().max(500),
|
||||||
|
website: yup.string().uuid().required(),
|
||||||
|
name: yup.string().max(50),
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
.validate(input)
|
||||||
|
.catch((err) => {});
|
||||||
|
}),
|
||||||
|
body('type')
|
||||||
|
.exists()
|
||||||
|
.withMessage('type should be existed')
|
||||||
|
.isString()
|
||||||
|
.matches(/event|identify/i)
|
||||||
|
),
|
||||||
|
async (req, res) => {
|
||||||
|
// https://github1s.com/umami-software/umami/blob/master/src/pages/api/send.ts
|
||||||
|
|
||||||
|
const { type, payload } = req.body;
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
referrer,
|
||||||
|
name: eventName,
|
||||||
|
data: eventData,
|
||||||
|
title: pageTitle,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const session = await findSession(req);
|
||||||
|
|
||||||
|
if (type === COLLECTION_TYPE.event) {
|
||||||
|
let [urlPath, urlQuery] = url?.split('?') || [];
|
||||||
|
let [referrerPath, referrerQuery] = referrer?.split('?') || [];
|
||||||
|
let referrerDomain;
|
||||||
|
|
||||||
|
if (!urlPath) {
|
||||||
|
urlPath = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referrerPath?.startsWith('http')) {
|
||||||
|
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(/.+\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWebsiteEvent({
|
||||||
|
urlPath,
|
||||||
|
urlQuery,
|
||||||
|
referrerPath,
|
||||||
|
referrerQuery,
|
||||||
|
referrerDomain,
|
||||||
|
pageTitle,
|
||||||
|
eventName,
|
||||||
|
eventData,
|
||||||
|
...session,
|
||||||
|
sessionId: session.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === COLLECTION_TYPE.identify) {
|
||||||
|
if (!eventData) {
|
||||||
|
throw new Error('Data required');
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWebsiteSessionData({
|
||||||
|
...session,
|
||||||
|
sessionData: eventData,
|
||||||
|
sessionId: session.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send();
|
||||||
|
}
|
||||||
|
);
|
131
src/server/utils/common.ts
Normal file
131
src/server/utils/common.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { v4, v5, validate } from 'uuid';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { DATA_TYPE } from './const';
|
||||||
|
import { DynamicDataType } from './types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export function isUuid(value: string) {
|
||||||
|
return validate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeDecodeURIComponent(
|
||||||
|
s: string | string[] | undefined | null
|
||||||
|
): string | undefined | null {
|
||||||
|
if (s === undefined || s === null) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(s)) {
|
||||||
|
s = String(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(s);
|
||||||
|
} catch (e) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(...args: string[]) {
|
||||||
|
return crypto.createHash('sha512').update(args.join('')).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashUuid(...args: string[]) {
|
||||||
|
if (!args.length) {
|
||||||
|
return v4();
|
||||||
|
}
|
||||||
|
|
||||||
|
return v5(hash(...args), v5.DNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidDate(input: any) {
|
||||||
|
return dayjs(input).isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenJSON(
|
||||||
|
eventData: { [key: string]: any },
|
||||||
|
keyValues: {
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
dynamicDataType: DynamicDataType;
|
||||||
|
}[] = [],
|
||||||
|
parentKey = ''
|
||||||
|
): { key: string; value: any; dynamicDataType: DynamicDataType }[] {
|
||||||
|
return Object.keys(eventData).reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
const value = eventData[key];
|
||||||
|
const type = typeof eventData[key];
|
||||||
|
|
||||||
|
// nested object
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
type === 'object' &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
!isValidDate(value)
|
||||||
|
) {
|
||||||
|
flattenJSON(value, acc.keyValues, getKeyName(key, parentKey));
|
||||||
|
} else {
|
||||||
|
createKey(getKeyName(key, parentKey), value, acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ keyValues, parentKey }
|
||||||
|
).keyValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeyName(key: string, parentKey?: string) {
|
||||||
|
if (!parentKey) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${parentKey}.${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKey(
|
||||||
|
key: string,
|
||||||
|
value: any,
|
||||||
|
acc: { keyValues: any[]; parentKey: string }
|
||||||
|
) {
|
||||||
|
const type = getDataType(value);
|
||||||
|
|
||||||
|
let dynamicDataType = null;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'number':
|
||||||
|
dynamicDataType = DATA_TYPE.number;
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
dynamicDataType = DATA_TYPE.string;
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
dynamicDataType = DATA_TYPE.boolean;
|
||||||
|
value = value ? 'true' : 'false';
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
dynamicDataType = DATA_TYPE.date;
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
dynamicDataType = DATA_TYPE.array;
|
||||||
|
value = JSON.stringify(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dynamicDataType = DATA_TYPE.string;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.keyValues.push({ key, value, dynamicDataType });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataType(value: any): string {
|
||||||
|
let type: string = typeof value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(type === 'string' && isValidDate(value)) ||
|
||||||
|
isValidDate(dayjs(value).toISOString())
|
||||||
|
) {
|
||||||
|
type = 'date';
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
@ -7,3 +7,62 @@ export const ROLES = {
|
|||||||
owner: 'owner',
|
owner: 'owner',
|
||||||
readOnly: 'readOnly',
|
readOnly: 'readOnly',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const HOSTNAME_REGEX =
|
||||||
|
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
||||||
|
|
||||||
|
export const COLLECTION_TYPE = {
|
||||||
|
event: 'event',
|
||||||
|
identify: 'identify',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DESKTOP_OS = [
|
||||||
|
'BeOS',
|
||||||
|
'Chrome OS',
|
||||||
|
'Linux',
|
||||||
|
'Mac OS',
|
||||||
|
'Open BSD',
|
||||||
|
'OS/2',
|
||||||
|
'QNX',
|
||||||
|
'Sun OS',
|
||||||
|
'Windows 10',
|
||||||
|
'Windows 2000',
|
||||||
|
'Windows 3.11',
|
||||||
|
'Windows 7',
|
||||||
|
'Windows 8',
|
||||||
|
'Windows 8.1',
|
||||||
|
'Windows 95',
|
||||||
|
'Windows 98',
|
||||||
|
'Windows ME',
|
||||||
|
'Windows Server 2003',
|
||||||
|
'Windows Vista',
|
||||||
|
'Windows XP',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOBILE_OS = [
|
||||||
|
'Amazon OS',
|
||||||
|
'Android OS',
|
||||||
|
'BlackBerry OS',
|
||||||
|
'iOS',
|
||||||
|
'Windows Mobile',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DESKTOP_SCREEN_WIDTH = 1920;
|
||||||
|
export const LAPTOP_SCREEN_WIDTH = 1024;
|
||||||
|
export const MOBILE_SCREEN_WIDTH = 479;
|
||||||
|
|
||||||
|
export const URL_LENGTH = 500;
|
||||||
|
export const EVENT_NAME_LENGTH = 50;
|
||||||
|
|
||||||
|
export const EVENT_TYPE = {
|
||||||
|
pageView: 1,
|
||||||
|
customEvent: 2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DATA_TYPE = {
|
||||||
|
string: 1,
|
||||||
|
number: 2,
|
||||||
|
boolean: 3,
|
||||||
|
date: 4,
|
||||||
|
array: 5,
|
||||||
|
} as const;
|
||||||
|
157
src/server/utils/detect.ts
Normal file
157
src/server/utils/detect.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { Request } from 'express';
|
||||||
|
import type { WebsiteEventPayload } from '../model/website';
|
||||||
|
import { getClientIp } from 'request-ip';
|
||||||
|
import isLocalhost from 'is-localhost-ip';
|
||||||
|
import { safeDecodeURIComponent } from './common';
|
||||||
|
import { browserName, detectOS, OperatingSystem } from 'detect-browser';
|
||||||
|
import {
|
||||||
|
DESKTOP_OS,
|
||||||
|
DESKTOP_SCREEN_WIDTH,
|
||||||
|
LAPTOP_SCREEN_WIDTH,
|
||||||
|
MOBILE_OS,
|
||||||
|
MOBILE_SCREEN_WIDTH,
|
||||||
|
} from './const';
|
||||||
|
import path from 'path';
|
||||||
|
import maxmind, { Reader, CityResponse } from 'maxmind';
|
||||||
|
|
||||||
|
let lookup: Reader<CityResponse>;
|
||||||
|
|
||||||
|
export async function getClientInfo(
|
||||||
|
req: Request,
|
||||||
|
payload: WebsiteEventPayload
|
||||||
|
) {
|
||||||
|
const userAgent = req.headers['user-agent'];
|
||||||
|
const ip = getIpAddress(req);
|
||||||
|
const location = await getLocation(ip, req);
|
||||||
|
const country = location?.country;
|
||||||
|
const subdivision1 = location?.subdivision1;
|
||||||
|
const subdivision2 = location?.subdivision2;
|
||||||
|
const city = location?.city;
|
||||||
|
const browser = browserName(userAgent ?? '');
|
||||||
|
const os = detectOS(userAgent ?? '');
|
||||||
|
const device = getDevice(payload.screen, os);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userAgent,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
ip,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
subdivision2,
|
||||||
|
city,
|
||||||
|
device,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIpAddress(req: Request): string {
|
||||||
|
// Custom header
|
||||||
|
if (
|
||||||
|
process.env.CLIENT_IP_HEADER &&
|
||||||
|
req.headers[process.env.CLIENT_IP_HEADER]
|
||||||
|
) {
|
||||||
|
return String(req.headers[process.env.CLIENT_IP_HEADER]);
|
||||||
|
}
|
||||||
|
// Cloudflare
|
||||||
|
else if (req.headers['cf-connecting-ip']) {
|
||||||
|
return String(req.headers['cf-connecting-ip']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getClientIp(req)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLocation(ip: string, req: Request) {
|
||||||
|
// Ignore local ips
|
||||||
|
if (await isLocalhost(ip)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cloudflare headers
|
||||||
|
if (req.headers['cf-ipcountry']) {
|
||||||
|
const country = safeDecodeURIComponent(req.headers['cf-ipcountry']);
|
||||||
|
const subdivision1 = safeDecodeURIComponent(req.headers['cf-region-code']);
|
||||||
|
const city = safeDecodeURIComponent(req.headers['cf-ipcity']);
|
||||||
|
|
||||||
|
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']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
country,
|
||||||
|
subdivision1: getRegionCode(country, subdivision1),
|
||||||
|
city,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database lookup
|
||||||
|
if (!lookup) {
|
||||||
|
const dir = path.join(process.cwd(), 'geo');
|
||||||
|
|
||||||
|
lookup = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = lookup.get(ip);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
country: result.country?.iso_code ?? result?.registered_country?.iso_code,
|
||||||
|
subdivision1: result.subdivisions?.[0]?.iso_code,
|
||||||
|
subdivision2: result.subdivisions?.[1]?.names?.en,
|
||||||
|
city: result.city?.names?.en,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegionCode(
|
||||||
|
country: string | null | undefined,
|
||||||
|
region: string | null | undefined
|
||||||
|
) {
|
||||||
|
if (!country || !region) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return region.includes('-') ? region : `${country}-${region}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDevice(
|
||||||
|
screen: string | undefined,
|
||||||
|
os: OperatingSystem | null
|
||||||
|
) {
|
||||||
|
if (!screen) return;
|
||||||
|
if (!os) return;
|
||||||
|
|
||||||
|
const [width] = screen.split('x').map((n) => Number(n));
|
||||||
|
|
||||||
|
if (DESKTOP_OS.includes(os)) {
|
||||||
|
if (os === 'Chrome OS' || width < DESKTOP_SCREEN_WIDTH) {
|
||||||
|
return 'laptop';
|
||||||
|
}
|
||||||
|
return 'desktop';
|
||||||
|
} else if (MOBILE_OS.includes(os)) {
|
||||||
|
if (os === 'Amazon OS' || width > MOBILE_SCREEN_WIDTH) {
|
||||||
|
return 'tablet';
|
||||||
|
}
|
||||||
|
return 'mobile';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width >= DESKTOP_SCREEN_WIDTH) {
|
||||||
|
return 'desktop';
|
||||||
|
} else if (width >= LAPTOP_SCREEN_WIDTH) {
|
||||||
|
return 'laptop';
|
||||||
|
} else if (width >= MOBILE_SCREEN_WIDTH) {
|
||||||
|
return 'tablet';
|
||||||
|
} else {
|
||||||
|
return 'mobile';
|
||||||
|
}
|
||||||
|
}
|
14
src/server/utils/types.ts
Normal file
14
src/server/utils/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { DATA_TYPE } from './const';
|
||||||
|
|
||||||
|
type ObjectValues<T> = T[keyof T];
|
||||||
|
export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
|
||||||
|
|
||||||
|
export interface DynamicData {
|
||||||
|
[key: string]:
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| DynamicData
|
||||||
|
| number[]
|
||||||
|
| string[]
|
||||||
|
| DynamicData[];
|
||||||
|
}
|
@ -28,7 +28,7 @@
|
|||||||
const root = hostUrl
|
const root = hostUrl
|
||||||
? hostUrl.replace(/\/$/, '')
|
? hostUrl.replace(/\/$/, '')
|
||||||
: currentScript.src.split('/').slice(0, -1).join('/');
|
: currentScript.src.split('/').slice(0, -1).join('/');
|
||||||
const endpoint = `${root}/api/send`;
|
const endpoint = `${root}/api/website/send`;
|
||||||
const screen = `${width}x${height}`;
|
const screen = `${width}x${height}`;
|
||||||
const eventRegex = /data-tianji-event-([\w-_]+)/;
|
const eventRegex = /data-tianji-event-([\w-_]+)/;
|
||||||
const eventNameAttribute = _data + 'tianji-event';
|
const eventNameAttribute = _data + 'tianji-event';
|
||||||
|
Loading…
Reference in New Issue
Block a user