mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
Merge branch 'refactoring'
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ docker
|
|||||||
dev
|
dev
|
||||||
docker-compose.admin.yml
|
docker-compose.admin.yml
|
||||||
full_reload.sh
|
full_reload.sh
|
||||||
build-all.sh
|
build-all.sh
|
||||||
|
tmp
|
||||||
|
ecosystem.config.js
|
||||||
12
consumer/.gitignore
vendored
12
consumer/.gitignore
vendored
@@ -1,8 +1,10 @@
|
|||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
static
|
|
||||||
ecosystem.config.cjs
|
ecosystem.config.cjs
|
||||||
dist
|
ecosystem.config.js
|
||||||
|
|
||||||
scripts/start_dev.js
|
scripts/start_dev.js
|
||||||
package-lock.json
|
scripts/start_dev_prod.js
|
||||||
build_all.bat
|
dist
|
||||||
tests
|
src/shared
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
apps: [
|
|
||||||
{
|
|
||||||
name: 'consumer',
|
|
||||||
port: '3031',
|
|
||||||
exec_mode: 'cluster',
|
|
||||||
instances: '2',
|
|
||||||
script: './dist/consumer/src/index.js',
|
|
||||||
env: {
|
|
||||||
EMAIL_SERVICE: '',
|
|
||||||
BREVO_API_KEY: '',
|
|
||||||
MONGO_CONNECTION_STRING: '',
|
|
||||||
REDIS_URL: "",
|
|
||||||
REDIS_USERNAME: "",
|
|
||||||
REDIS_PASSWORD: "",
|
|
||||||
STREAM_NAME: "",
|
|
||||||
GROUP_NAME: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"mongoose": "^8.9.5",
|
||||||
|
"redis": "^4.7.0",
|
||||||
"ua-parser-js": "^1.0.37"
|
"ua-parser-js": "^1.0.37"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "^20.12.13",
|
"@types/node": "^20.12.13",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
@@ -14,12 +18,14 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/start_dev.js",
|
"dev": "node scripts/start_dev.js",
|
||||||
|
"dev_prod": "node scripts/start_dev_prod.js",
|
||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"build_project": "node ../scripts/build.js",
|
"build": "npm run compile && npm run create_db",
|
||||||
"build": "npm run compile && npm run build_project && npm run create_db",
|
|
||||||
"create_db": "cd scripts && ts-node create_database.ts",
|
"create_db": "cd scripts && ts-node create_database.ts",
|
||||||
"docker-build": "docker build -t litlyx-consumer -f Dockerfile ../",
|
"docker-build": "docker build -t litlyx-consumer -f Dockerfile ../",
|
||||||
"docker-inspect": "docker run -it litlyx-consumer sh"
|
"docker-inspect": "docker run -it litlyx-consumer sh",
|
||||||
|
"workspace:shared": "ts-node ../scripts/consumer/shared.ts",
|
||||||
|
"workspace:deploy": "ts-node ../scripts/consumer/deploy.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Emily",
|
"author": "Emily",
|
||||||
|
|||||||
1493
consumer/pnpm-lock.yaml
generated
1493
consumer/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,10 @@
|
|||||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
import { ProjectModel } from "./shared/schema/project/ProjectSchema";
|
||||||
import { UserModel } from "@schema/UserSchema";
|
import { UserModel } from "./shared/schema/UserSchema";
|
||||||
import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
|
import { LimitNotifyModel } from "./shared/schema/broker/LimitNotifySchema";
|
||||||
import EmailService from '@services/EmailService';
|
import { EmailService } from './shared/services/EmailService';
|
||||||
import { requireEnv } from "@utils/requireEnv";
|
import { TProjectLimit } from "./shared/schema/project/ProjectsLimits";
|
||||||
import { TProjectLimit } from "@schema/project/ProjectsLimits";
|
import { EmailServiceHelper } from "./EmailServiceHelper";
|
||||||
|
|
||||||
if (process.env.EMAIL_SERVICE) {
|
|
||||||
EmailService.init(requireEnv('BREVO_API_KEY'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
||||||
|
|
||||||
@@ -27,7 +24,14 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
|||||||
const owner = await UserModel.findById(project.owner);
|
const owner = await UserModel.findById(project.owner);
|
||||||
if (!owner) return;
|
if (!owner) return;
|
||||||
|
|
||||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmailMax(owner.email, project.name);
|
setImmediate(() => {
|
||||||
|
const emailData = EmailService.getEmailServerInfo('limit_max', {
|
||||||
|
target: owner.email,
|
||||||
|
projectName: project.name
|
||||||
|
});
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
|
});
|
||||||
|
|
||||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: true });
|
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: true });
|
||||||
|
|
||||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.9)) {
|
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.9)) {
|
||||||
@@ -40,7 +44,14 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
|||||||
const owner = await UserModel.findById(project.owner);
|
const owner = await UserModel.findById(project.owner);
|
||||||
if (!owner) return;
|
if (!owner) return;
|
||||||
|
|
||||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmail90(owner.email, project.name);
|
setImmediate(() => {
|
||||||
|
const emailData = EmailService.getEmailServerInfo('limit_90', {
|
||||||
|
target: owner.email,
|
||||||
|
projectName: project.name
|
||||||
|
});
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
|
});
|
||||||
|
|
||||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: false });
|
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: false });
|
||||||
|
|
||||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.5)) {
|
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.5)) {
|
||||||
@@ -53,7 +64,14 @@ export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
|||||||
const owner = await UserModel.findById(project.owner);
|
const owner = await UserModel.findById(project.owner);
|
||||||
if (!owner) return;
|
if (!owner) return;
|
||||||
|
|
||||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmail50(owner.email, project.name);
|
setImmediate(() => {
|
||||||
|
const emailData = EmailService.getEmailServerInfo('limit_50', {
|
||||||
|
target: owner.email,
|
||||||
|
projectName: project.name
|
||||||
|
});
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
|
});
|
||||||
|
|
||||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: false, limit3: false });
|
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: false, limit3: false });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
consumer/src/EmailServiceHelper.ts
Normal file
19
consumer/src/EmailServiceHelper.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
import { EmailServerInfo } from './shared/services/EmailService'
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const EMAIL_SECRET = process.env.EMAIL_SECRET;
|
||||||
|
|
||||||
|
export class EmailServiceHelper {
|
||||||
|
static async sendEmail(data: EmailServerInfo) {
|
||||||
|
try {
|
||||||
|
await axios(data.url, {
|
||||||
|
method: 'POST',
|
||||||
|
data: data.body,
|
||||||
|
headers: { ...data.headers, 'x-litlyx-token': EMAIL_SECRET }
|
||||||
|
})
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
|
import { ProjectLimitModel } from './shared/schema/project/ProjectsLimits';
|
||||||
import { MAX_LOG_LIMIT_PERCENT } from '@data/broker/Limits';
|
import { MAX_LOG_LIMIT_PERCENT } from './shared/data/broker/Limits';
|
||||||
import { checkLimitsForEmail } from './EmailController';
|
import { checkLimitsForEmail } from './EmailController';
|
||||||
|
|
||||||
export async function checkLimits(project_id: string) {
|
export async function checkLimits(project_id: string) {
|
||||||
|
|||||||
28
consumer/src/Metrics.ts
Normal file
28
consumer/src/Metrics.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { RedisStreamService } from './shared/services/RedisStreamService';
|
||||||
|
import { requireEnv } from './shared/utils/requireEnv';
|
||||||
|
|
||||||
|
const stream_name = requireEnv('STREAM_NAME');
|
||||||
|
|
||||||
|
export const metricsRouter = Router();
|
||||||
|
|
||||||
|
metricsRouter.get('/queue', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const size = await RedisStreamService.getQueueInfo(stream_name);
|
||||||
|
res.json({ size });
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
res.status(500).json({ error: ex.message });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsRouter.get('/durations', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const durations = RedisStreamService.METRICS_get()
|
||||||
|
res.json({ durations });
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
res.status(500).json({ error: ex.message });
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,39 +1,31 @@
|
|||||||
|
|
||||||
import { requireEnv } from '@utils/requireEnv';
|
import { requireEnv } from './shared/utils/requireEnv';
|
||||||
import { connectDatabase } from '@services/DatabaseService';
|
import { connectDatabase } from './shared/services/DatabaseService';
|
||||||
import { RedisStreamService } from '@services/RedisStreamService';
|
import { RedisStreamService } from './shared/services/RedisStreamService';
|
||||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
import { ProjectModel } from "./shared/schema/project/ProjectSchema";
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "./shared/schema/metrics/VisitSchema";
|
||||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
import { SessionModel } from "./shared/schema/metrics/SessionSchema";
|
||||||
import { EventModel } from "@schema/metrics/EventSchema";
|
import { EventModel } from "./shared/schema/metrics/EventSchema";
|
||||||
import { lookup } from './lookup';
|
import { lookup } from './lookup';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
import { checkLimits } from './LimitChecker';
|
import { checkLimits } from './LimitChecker';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
|
import { ProjectLimitModel } from './shared/schema/project/ProjectsLimits';
|
||||||
import { ProjectCountModel } from '@schema/project/ProjectsCounts';
|
import { ProjectCountModel } from './shared/schema/project/ProjectsCounts';
|
||||||
|
import { metricsRouter } from './Metrics';
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
let durations: number[] = [];
|
app.use('/metrics', metricsRouter);
|
||||||
|
|
||||||
app.get('/status', async (req, res) => {
|
app.listen(process.env.PORT, () => console.log(`Listening on port ${process.env.PORT}`));
|
||||||
try {
|
|
||||||
return res.json({ status: 'ALIVE', durations })
|
|
||||||
} catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
return res.setStatus(500).json({ error: ex.message });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(process.env.PORT);
|
|
||||||
|
|
||||||
connectDatabase(requireEnv('MONGO_CONNECTION_STRING'));
|
connectDatabase(requireEnv('MONGO_CONNECTION_STRING'));
|
||||||
main();
|
main();
|
||||||
|
|
||||||
|
const CONSUMER_NAME = `CONSUMER_${process.env.NODE_APP_INSTANCE || 'DEFAULT'}`
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
|
||||||
@@ -43,7 +35,7 @@ async function main() {
|
|||||||
const group_name = requireEnv('GROUP_NAME') as any; // Checks are inside "startReadingLoop"
|
const group_name = requireEnv('GROUP_NAME') as any; // Checks are inside "startReadingLoop"
|
||||||
|
|
||||||
await RedisStreamService.startReadingLoop({
|
await RedisStreamService.startReadingLoop({
|
||||||
stream_name, group_name, consumer_name: `CONSUMER_${process.env.NODE_APP_INSTANCE || 'DEFAULT'}`
|
stream_name, group_name, consumer_name: CONSUMER_NAME
|
||||||
}, processStreamEntry);
|
}, processStreamEntry);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -55,7 +47,7 @@ async function processStreamEntry(data: Record<string, string>) {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
const eventType = data._type;
|
const eventType = data._type;
|
||||||
if (!eventType) return;
|
if (!eventType) return console.log('No type');
|
||||||
|
|
||||||
const { pid, sessionHash } = data;
|
const { pid, sessionHash } = data;
|
||||||
|
|
||||||
@@ -73,18 +65,13 @@ async function processStreamEntry(data: Record<string, string>) {
|
|||||||
await process_visit(data, sessionHash);
|
await process_visit(data, sessionHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('Entry processed in', duration, 'ms');
|
|
||||||
|
|
||||||
} catch (ex: any) {
|
} catch (ex: any) {
|
||||||
console.error('ERROR PROCESSING STREAM EVENT', ex.message);
|
console.error('ERROR PROCESSING STREAM EVENT', ex.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
durations.push(duration);
|
RedisStreamService.METRICS_onProcess(CONSUMER_NAME, duration);
|
||||||
if (durations.length > 1000) {
|
|
||||||
durations = durations.splice(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"esModuleInterop": true,
|
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
8
dashboard/.gitignore
vendored
8
dashboard/.gitignore
vendored
@@ -24,7 +24,6 @@ winston-*.ndjson
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
|
||||||
# Test reports
|
# Test reports
|
||||||
*.report.txt
|
*.report.txt
|
||||||
|
|
||||||
@@ -35,5 +34,10 @@ out.pdf
|
|||||||
tests
|
tests
|
||||||
|
|
||||||
# EXPLAINS MONGODB
|
# EXPLAINS MONGODB
|
||||||
|
explains
|
||||||
|
|
||||||
explains
|
#Ecosystem
|
||||||
|
ecosystem.config.cjs
|
||||||
|
ecosystem.config.js
|
||||||
|
|
||||||
|
shared
|
||||||
|
|||||||
13
dashboard/assets/main.css
Normal file
13
dashboard/assets/main.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@import './font-awesome/css/all.css';
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&display=swap');
|
||||||
|
@import url('https://fonts.cdnfonts.com/css/brockmann');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||||
|
|
||||||
|
|
||||||
|
@import url('https://fonts.cdnfonts.com/css/geometric-sans-serif-v1');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0');
|
||||||
@@ -1,20 +1,6 @@
|
|||||||
@use './utilities.scss';
|
@use './utilities.scss';
|
||||||
@use './colors.scss';
|
@use './colors.scss';
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&display=swap');
|
|
||||||
@import url('https://fonts.cdnfonts.com/css/brockmann');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
|
||||||
|
|
||||||
@import '../font-awesome/css/all.css';
|
|
||||||
|
|
||||||
@import url('https://fonts.cdnfonts.com/css/geometric-sans-serif-v1');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0');
|
|
||||||
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Geist";
|
font-family: "Geist";
|
||||||
src: url("../fonts/GeistVF.ttf");
|
src: url("../fonts/GeistVF.ttf");
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function openExternalLink(link: string) {
|
|||||||
<i v-else :class="iconProvider(element)?.[1]"></i>
|
<i v-else :class="iconProvider(element)?.[1]"></i>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-lyx-lightmode-text-dark dark:text-text/70">
|
class="text-ellipsis line-clamp-1 ui-font z-[19] text-[.95rem] text-lyx-lightmode-text-dark dark:text-text/70">
|
||||||
{{ elementTextTransformer?.(element._id) || element._id }}
|
{{ elementTextTransformer?.(element._id) || element._id }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ async function showMore() {
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 h-full">
|
||||||
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
|
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
|
||||||
desc="The browsers most used to search your website." :dataIcons="true" :iconProvider="iconProvider"
|
desc="The browsers most used to search your website." :dataIcons="true" :iconProvider="iconProvider"
|
||||||
:loading="browsersData.pending.value" label="Browsers" sub-label="Browsers">
|
:loading="browsersData.pending.value" label="Browsers" sub-label="Browsers">
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async function showMore() {
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 h-full">
|
||||||
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
|
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
|
||||||
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
|
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
|
||||||
label="Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
label="Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
|
||||||
|
|||||||
44
dashboard/components/BarCard/Pages.vue
Normal file
44
dashboard/components/BarCard/Pages.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const pagesData = useFetch('/api/data/pages', {
|
||||||
|
headers: useComputedHeaders({
|
||||||
|
limit: 10,
|
||||||
|
}), lazy: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
async function showMore() {
|
||||||
|
|
||||||
|
dialogBarData.value = [];
|
||||||
|
|
||||||
|
showDialog.value = true;
|
||||||
|
isDataLoading.value = true;
|
||||||
|
|
||||||
|
const res = await $fetch('/api/data/pages', {
|
||||||
|
headers: useComputedHeaders({ limit: 1000 }).value
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogBarData.value = (res || []);
|
||||||
|
|
||||||
|
isDataLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToView() {
|
||||||
|
router.push('/dashboard/visits');
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full">
|
||||||
|
<BarCardBase @showRawData="goToView()" @showMore="showMore()" @dataReload="pagesData.refresh()" :showLink=true
|
||||||
|
:data="pagesData.data.value || []" :interactive="false" desc="Most visited pages."
|
||||||
|
:rawButton="!isLiveDemo"
|
||||||
|
:dataIcons="true" :loading="pagesData.pending.value" label="Top Pages" sub-label="Referrers">
|
||||||
|
</BarCardBase>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -43,7 +43,7 @@ async function showMore() {
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2 h-full">
|
||||||
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
||||||
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
|
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
|
||||||
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
|
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ const activeTabIndex = ref<number>(0);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex">
|
<div class="flex overflow-y-auto hide-scrollbars">
|
||||||
<div v-for="(tab, index) of items" @click="activeTabIndex = index"
|
<div class="flex">
|
||||||
class="px-6 pb-3 poppins font-medium text-lyx-lightmode-text dark:text-lyx-text-darker border-b-[1px] border-lyx-text-darker" :class="{
|
<div v-for="(tab, index) of items" @click="activeTabIndex = index"
|
||||||
'!border-[#88A7FF] !text-[#88A7FF]': activeTabIndex === index,
|
class="px-6 pb-3 poppins font-medium text-lyx-lightmode-text dark:text-lyx-text-darker border-b-[1px] border-lyx-text-darker"
|
||||||
'hover:border-lyx-lightmode-text-dark hover:text-lyx-lightmode-text-dark/60 dark:hover:border-lyx-text-dark dark:hover:text-lyx-text-dark cursor-pointer': activeTabIndex !== index
|
:class="{
|
||||||
}">
|
'!border-[#88A7FF] !text-[#88A7FF]': activeTabIndex === index,
|
||||||
{{ tab.label }}
|
'hover:border-lyx-lightmode-text-dark hover:text-lyx-lightmode-text-dark/60 dark:hover:border-lyx-text-dark dark:hover:text-lyx-text-dark cursor-pointer': activeTabIndex !== index
|
||||||
|
}">
|
||||||
|
{{ tab.label }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-b-[1px] border-lyx-text-darker w-full">
|
<div class="border-b-[1px] border-lyx-text-darker w-full">
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function reloadPage() {
|
|||||||
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center mt-10">
|
<div class="flex items-center justify-center mt-10">
|
||||||
<div class="flex flex-col-reverse gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
|
|
||||||
<div class="flex gap-6 xl:flex-row flex-col">
|
<div class="flex gap-6 xl:flex-row flex-col">
|
||||||
|
|
||||||
@@ -135,8 +135,8 @@ function reloadPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitled class="w-full h-full" title="Documentation"
|
<CardTitled class="w-full h-full" title="Modules"
|
||||||
sub="Learn how to use Litlyx in every tech stack">
|
sub="Get started with your favorite framework.">
|
||||||
<template #header>
|
<template #header>
|
||||||
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
||||||
to="https://docs.litlyx.com">
|
to="https://docs.litlyx.com">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import DateService, { type Slice } from '@services/DateService';
|
import DateService, { type Slice } from '@services/DateService';
|
||||||
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
|
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
|
||||||
import { useLineChart, LineChart } from 'vue-chart-3';
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
@@ -113,7 +112,7 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Unique sessions',
|
label: 'Unique visitors',
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: ['#4abde8'],
|
backgroundColor: ['#4abde8'],
|
||||||
borderColor: '#4abde8',
|
borderColor: '#4abde8',
|
||||||
@@ -254,8 +253,6 @@ watch(readyToDisplay, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function onDataReady() {
|
function onDataReady() {
|
||||||
if (!visitsData.data.value) return;
|
if (!visitsData.data.value) return;
|
||||||
if (!eventsData.data.value) return;
|
if (!eventsData.data.value) return;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const { showDrawer } = useDrawer();
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!ready" class="flex justify-center items-center w-full h-full flex-col gap-2">
|
<div v-if="!ready" class="flex justify-center items-center w-full h-full flex-col gap-2">
|
||||||
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||||
<div v-if="props.slow"> Can be very slow on large snapshots </div>
|
<div v-if="props.slow"> Can be very slow on large timeframes </div>
|
||||||
</div>
|
</div>
|
||||||
</LyxUiCard>
|
</LyxUiCard>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
import DateService from '@services/DateService';
|
import DateService, { type Slice } from '../../shared/services/DateService';
|
||||||
import type { Slice } from '@services/DateService';
|
|
||||||
|
|
||||||
const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()
|
const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ const avgSessionDuration = computed(() => {
|
|||||||
.filter(e => e > 0)
|
.filter(e => e > 0)
|
||||||
.reduce((a, e) => e + a, 0);
|
.reduce((a, e) => e + a, 0);
|
||||||
|
|
||||||
const avg = counts / Math.max(sessionsDurationData.data.value.data.filter(e => e > 0).length, 1);
|
const avg = counts / (Math.max(sessionsDurationData.data.value.data.filter(e => e > 0).length, 1)) / 5;
|
||||||
|
|
||||||
let hours = 0;
|
let hours = 0;
|
||||||
let minutes = 0;
|
let minutes = 0;
|
||||||
|
|||||||
@@ -18,18 +18,6 @@ function copyProjectId() {
|
|||||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function showAnomalyInfoAlert() {
|
|
||||||
createAlert('AI Anomaly Detector info',
|
|
||||||
`Anomaly detector is running. It helps you detect a spike in visits or events, it could mean an
|
|
||||||
attack or simply higher traffic due to good performance. Additionally, it can detect if someone is
|
|
||||||
stealing parts of your website and hosting a duplicate version—an unfortunately common practice.
|
|
||||||
Litlyx will notify you via email with actionable advices`,
|
|
||||||
'far fa-shield',
|
|
||||||
10000
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +26,7 @@ function showAnomalyInfoAlert() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex gap-2 items-center text-lyx-lightmode-text/90 dark:text-lyx-text/90 justify-center md:justify-start">
|
class="flex gap-2 items-center text-lyx-lightmode-text/90 dark:text-lyx-text/90 justify-center md:justify-start">
|
||||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
<div class="animate-pulse w-[.8rem] h-[.8rem] bg-green-400 rounded-full"> </div>
|
||||||
<div class="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div>
|
<div class="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,7 +50,7 @@ function showAnomalyInfoAlert() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
<!--
|
||||||
<div v-if="!selfhosted"
|
<div v-if="!selfhosted"
|
||||||
class="flex gap-2 items-center text-lyx-lightmode-text/90 dark:text-lyx-text/90 justify-center md:justify-start">
|
class="flex gap-2 items-center text-lyx-lightmode-text/90 dark:text-lyx-text/90 justify-center md:justify-start">
|
||||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||||
@@ -71,7 +59,7 @@ function showAnomalyInfoAlert() {
|
|||||||
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
||||||
@click="showAnomalyInfoAlert"></i>
|
@click="showAnomalyInfoAlert"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -53,7 +53,7 @@ async function confirmSnapshot() {
|
|||||||
|
|
||||||
await updateSnapshots();
|
await updateSnapshots();
|
||||||
closeDialog();
|
closeDialog();
|
||||||
createAlert('Snapshot created', 'Snapshot created successfully', 'far fa-circle-check', 5000);
|
createAlert('Timeframe created', 'Timeframe created successfully', 'far fa-circle-check', 5000);
|
||||||
const newSnapshot = snapshots.value.at(-1);
|
const newSnapshot = snapshots.value.at(-1);
|
||||||
if (newSnapshot) snapshot.value = newSnapshot;
|
if (newSnapshot) snapshot.value = newSnapshot;
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ async function confirmSnapshot() {
|
|||||||
<div class="w-full h-full flex flex-col">
|
<div class="w-full h-full flex flex-col">
|
||||||
|
|
||||||
<div class="poppins text-center text-lyx-lightmode-text dark:text-lyx-text">
|
<div class="poppins text-center text-lyx-lightmode-text dark:text-lyx-text">
|
||||||
Create a snapshot
|
Create a timeframe
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-10 flex items-center gap-2">
|
<div class="mt-10 flex items-center gap-2">
|
||||||
@@ -74,7 +74,7 @@ async function confirmSnapshot() {
|
|||||||
<input @input="onColorChange" ref="colorpicker" class="relative w-0 h-0 z-[-100]" type="color">
|
<input @input="onColorChange" ref="colorpicker" class="relative w-0 h-0 z-[-100]" type="color">
|
||||||
</div>
|
</div>
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<LyxUiInput placeholder="Snapshot name" v-model="snapshotName" class="px-4 py-1 w-full"></LyxUiInput>
|
<LyxUiInput placeholder="Timeframe name" v-model="snapshotName" class="px-4 py-1 w-full"></LyxUiInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
58
dashboard/components/dialog/Help.vue
Normal file
58
dashboard/components/dialog/Help.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const { createAlert } = useAlert();
|
||||||
|
const { close } = useModal()
|
||||||
|
|
||||||
|
function copyEmail() {
|
||||||
|
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||||
|
navigator.clipboard.writeText('help@litlyx.com');
|
||||||
|
createAlert('Success', 'Email copied successfully.', 'far fa-circle-check', 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal :ui="{
|
||||||
|
strategy: 'override',
|
||||||
|
overlay: {
|
||||||
|
background: 'bg-lyx-background/85'
|
||||||
|
},
|
||||||
|
background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
|
||||||
|
ring: 'border-solid border-[1px] border-[#262626]'
|
||||||
|
}">
|
||||||
|
<div class="h-full flex flex-col gap-2 p-4">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
|
||||||
|
<div class="font-medium">
|
||||||
|
Contact Support
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dark:text-lyx-text-dark">
|
||||||
|
Contact Support for any questions or issues you have.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dark:bg-lyx-widget-lighter bg-lyx-lightmode-widget h-[1px]"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div class="p-2 bg-lyx-lightmode-widget dark:bg-[#1c1b1b] rounded-md w-full">
|
||||||
|
<div class="w-full text-[.9rem] dark:text-[#acacac]"> help@litlyx.com </div>
|
||||||
|
</div>
|
||||||
|
<LyxUiButton type="secondary" @click="copyEmail()"> Copy </LyxUiButton>
|
||||||
|
<LyxUiButton type="secondary" to="mailto:help@litlyx.com"> Send </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dark:text-lyx-text-dark mt-2">
|
||||||
|
or text us on Discord, we will reply to you personally.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LyxUiButton to="https://discord.gg/9cQykjsmWX" target="_blank" type="secondary">
|
||||||
|
Discord Support
|
||||||
|
</LyxUiButton>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
import DateService, { type Slice } from '@services/DateService';
|
import { type Slice } from '@services/DateService';
|
||||||
|
|
||||||
const props = defineProps<{ slice: Slice }>();
|
const props = defineProps<{ slice: Slice }>();
|
||||||
const slice = computed(() => props.slice);
|
const slice = computed(() => props.slice);
|
||||||
@@ -10,45 +10,23 @@ const { safeSnapshotDates } = useSnapshot()
|
|||||||
|
|
||||||
function transformResponse(input: { _id: string, name: string, count: number }[]) {
|
function transformResponse(input: { _id: string, name: string, count: number }[]) {
|
||||||
|
|
||||||
const fixed = fixMetrics({
|
const fixed = fixMetrics(
|
||||||
data: input,
|
{ data: input, from: input[0]._id, to: safeSnapshotDates.value.to },
|
||||||
from: input[0]._id,
|
|
||||||
to: safeSnapshotDates.value.to
|
|
||||||
},
|
|
||||||
slice.value,
|
slice.value,
|
||||||
{ advanced: true, advancedGroupKey: 'name' });
|
{ advanced: true, advancedGroupKey: 'name' }
|
||||||
|
);
|
||||||
|
|
||||||
const parsedDatasets: any[] = [];
|
const parsedDatasets: any[] = [];
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
"#5655d0",
|
"#5655d0", "#6bbbe3", "#a6d5cb", "#fae0b9", "#f28e8e",
|
||||||
"#6bbbe3",
|
"#e3a7e4", "#c4a8e1", "#8cc1d8", "#f9c2cd", "#b4e3b2",
|
||||||
"#a6d5cb",
|
"#ffdfba", "#e9c3b5", "#d5b8d6", "#add7f6", "#ffd1dc",
|
||||||
"#fae0b9",
|
"#ffe7a1", "#a8e6cf", "#d4a5a5", "#f3d6e4", "#c3aed6"
|
||||||
"#f28e8e",
|
|
||||||
"#e3a7e4",
|
|
||||||
"#c4a8e1",
|
|
||||||
"#8cc1d8",
|
|
||||||
"#f9c2cd",
|
|
||||||
"#b4e3b2",
|
|
||||||
"#ffdfba",
|
|
||||||
"#e9c3b5",
|
|
||||||
"#d5b8d6",
|
|
||||||
"#add7f6",
|
|
||||||
"#ffd1dc",
|
|
||||||
"#ffe7a1",
|
|
||||||
"#a8e6cf",
|
|
||||||
"#d4a5a5",
|
|
||||||
"#f3d6e4",
|
|
||||||
"#c3aed6"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < fixed.allKeys.length; i++) {
|
for (let i = 0; i < fixed.allKeys.length; i++) {
|
||||||
const line: any = {
|
const line: any = { data: [], color: colors[i] || '#FF0000', label: fixed.allKeys[i] };
|
||||||
data: [],
|
|
||||||
color: colors[i] || '#FF0000',
|
|
||||||
label: fixed.allKeys[i]
|
|
||||||
};
|
|
||||||
parsedDatasets.push(line)
|
parsedDatasets.push(line)
|
||||||
fixed.data.forEach((e: { key: string, value: number }[]) => {
|
fixed.data.forEach((e: { key: string, value: number }[]) => {
|
||||||
const target = e.find(e => e.key == fixed.allKeys[i]);
|
const target = e.find(e => e.key == fixed.allKeys[i]);
|
||||||
@@ -56,12 +34,7 @@ function transformResponse(input: { _id: string, name: string, count: number }[]
|
|||||||
line.data.push(target.value);
|
line.data.push(target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return { datasets: parsedDatasets, labels: fixed.labels }
|
||||||
return {
|
|
||||||
datasets: parsedDatasets,
|
|
||||||
labels: fixed.labels
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorData = ref<{ errored: boolean, text: string }>({
|
const errorData = ref<{ errored: boolean, text: string }>({
|
||||||
@@ -88,7 +61,6 @@ const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
|
|||||||
onResponse
|
onResponse
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
eventsStackedData.execute();
|
eventsStackedData.execute();
|
||||||
});
|
});
|
||||||
|
|||||||
53
dashboard/components/layout/TopNavigation.vue
Normal file
53
dashboard/components/layout/TopNavigation.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { DialogFeedback, DialogHelp } from '#components';
|
||||||
|
|
||||||
|
const modal = useModal();
|
||||||
|
const selfhosted = useSelfhosted();
|
||||||
|
|
||||||
|
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
const isDark = computed({
|
||||||
|
get() {
|
||||||
|
return colorMode.value === 'dark'
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="w-full overflow-y-auto hide-scrollbars h-[4rem] border-solid border-[#D9D9E0] dark:border-[#202020] border-b-[1px] bg-lyx-lightmode-background dark:bg-lyx-background flex dark:shadow-[1px_0_10px_#000000]">
|
||||||
|
|
||||||
|
<div class="flex items-center px-6">
|
||||||
|
<SelectorDomainSelector></SelectorDomainSelector>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grow"></div>
|
||||||
|
<div class="flex items-center gap-6 mr-10">
|
||||||
|
<div v-if="!selfhosted" @click="modal.open(DialogFeedback, {});"
|
||||||
|
class="flex gap-2 items-center cursor-pointer">
|
||||||
|
<i class="far fa-message"></i>
|
||||||
|
Feedback
|
||||||
|
</div>
|
||||||
|
<div @click="modal.open(DialogHelp, {});" class="cursor-pointer"> Help </div>
|
||||||
|
<NuxtLink to="https://docs.litlyx.com" target="_blank" class="cursor-pointer">
|
||||||
|
Docs
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UTooltip :text="isDark ? 'Toggle light mode' : 'Toggle dark mode'">
|
||||||
|
<i @click="isDark = !isDark"
|
||||||
|
class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark"
|
||||||
|
:class="isDark ? 'far fa-moon' : 'far fa-sun'"></i>
|
||||||
|
</UTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
import CreateSnapshot from './dialog/CreateSnapshot.vue';
|
import CreateSnapshot from '../dialog/CreateSnapshot.vue';
|
||||||
|
|
||||||
export type Entry = {
|
export type Entry = {
|
||||||
label: string,
|
label: string,
|
||||||
@@ -24,18 +24,6 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const isDark = computed({
|
|
||||||
get() {
|
|
||||||
return colorMode.value === 'dark'
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
@@ -139,7 +127,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
|
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="flex items-center gap-2 w-full">
|
||||||
|
|
||||||
<ProjectSelector></ProjectSelector>
|
<SelectorProjectSelector></SelectorProjectSelector>
|
||||||
|
|
||||||
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
|
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
|
||||||
<i @click="close()" class="fas fa-close"></i>
|
<i @click="close()" class="fas fa-close"></i>
|
||||||
@@ -172,7 +160,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
|
|
||||||
<div class="flex mb-2 items-center justify-between text-lyx-lightmode-text dark:text-lyx-text">
|
<div class="flex mb-2 items-center justify-between text-lyx-lightmode-text dark:text-lyx-text">
|
||||||
<div class="poppins text-[.8rem]">
|
<div class="poppins text-[.8rem]">
|
||||||
Snapshots
|
Timeframes
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -181,7 +169,7 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
<div><i class="far fa-download text-[.8rem]"></i></div>
|
<div><i class="far fa-download text-[.8rem]"></i></div>
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</UTooltip> -->
|
</UTooltip> -->
|
||||||
<UTooltip text="Create new snapshot">
|
<UTooltip text="Create new timeframe">
|
||||||
<LyxUiButton @click="openSnapshotDialog()" type="outlined" class="!px-3 !py-1">
|
<LyxUiButton @click="openSnapshotDialog()" type="outlined" class="!px-3 !py-1">
|
||||||
<div><i class="fas fa-plus text-[.8rem]"></i></div>
|
<div><i class="fas fa-plus text-[.8rem]"></i></div>
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
@@ -301,11 +289,6 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
|
|
||||||
<div class="grow flex gap-3">
|
<div class="grow flex gap-3">
|
||||||
|
|
||||||
<div>
|
|
||||||
<i @click="isDark = !isDark" class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark"
|
|
||||||
:class="isDark ? 'far fa-moon' : 'far fa-sun'"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NuxtLink to="/admin" v-if="userRoles.isAdmin.value"
|
<NuxtLink to="/admin" v-if="userRoles.isAdmin.value"
|
||||||
class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark">
|
class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark">
|
||||||
<i class="far fa-cat"></i>
|
<i class="far fa-cat"></i>
|
||||||
48
dashboard/components/selector/DomainSelector.vue
Normal file
48
dashboard/components/selector/DomainSelector.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const { domainList, domain, setActiveDomain, refreshDomains, refreshingDomains } = useDomain();
|
||||||
|
|
||||||
|
function onChange(e: string) {
|
||||||
|
setActiveDomain(e);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex gap-2 absolute">
|
||||||
|
<USelectMenu :uiMenu="{
|
||||||
|
select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
|
||||||
|
base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget w-max',
|
||||||
|
option: {
|
||||||
|
base: 'z-[990] hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||||
|
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
|
||||||
|
},
|
||||||
|
input: 'z-[999] !bg-lyx-lightmode-widget dark:!bg-lyx-widget-light'
|
||||||
|
}" class="w-full" searchable searchable-placeholder="Search domain..." v-if="domainList" @change="onChange"
|
||||||
|
:value="domain" value-attribute="_id" :options="domainList">
|
||||||
|
|
||||||
|
<template #option="{ option, active, selected }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||||
|
</div>
|
||||||
|
<div> {{ option._id }} </div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #label="e">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ domain || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
<div @click="refreshDomains" v-if="!refreshingDomains"
|
||||||
|
class="flex items-center hover:rotate-[60deg] transition-all duration-200 ease-in-out cursor-pointer">
|
||||||
|
<i class="far fa-refresh"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,7 +5,7 @@ import type { TProject } from '@schema/project/ProjectSchema';
|
|||||||
const { user } = useLoggedUser()
|
const { user } = useLoggedUser()
|
||||||
|
|
||||||
const { projectList, guestProjectList, allProjectList, actions, project } = useProject();
|
const { projectList, guestProjectList, allProjectList, actions, project } = useProject();
|
||||||
|
const { setActiveDomain } = useDomain();
|
||||||
|
|
||||||
function isProjectMine(owner?: string) {
|
function isProjectMine(owner?: string) {
|
||||||
if (!owner) return false;
|
if (!owner) return false;
|
||||||
@@ -16,6 +16,7 @@ function isProjectMine(owner?: string) {
|
|||||||
|
|
||||||
function onChange(e: TProject) {
|
function onChange(e: TProject) {
|
||||||
actions.setActiveProject(e._id.toString());
|
actions.setActiveProject(e._id.toString());
|
||||||
|
setActiveDomain('ALL DOMAINS');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { TApiSettings } from '@schema/ApiSettingsSchema';
|
import type { TApiSettings } from '@schema/ApiSettingsSchema';
|
||||||
import type { SettingsTemplateEntry } from './Template.vue';
|
import type { SettingsTemplateEntry } from './Template.vue';
|
||||||
|
|
||||||
const { project } = useProject();
|
const { project, isGuest } = useProject();
|
||||||
|
|
||||||
const entries: SettingsTemplateEntry[] = [
|
const entries: SettingsTemplateEntry[] = [
|
||||||
{ id: 'acodes', title: 'Appsumo codes', text: 'Redeem appsumo codes' },
|
{ id: 'acodes', title: 'Appsumo codes', text: 'Redeem appsumo codes' },
|
||||||
@@ -39,7 +39,7 @@ async function redeemCode() {
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SettingsTemplate :entries="entries" :key="project?.name || 'NONE'">
|
<SettingsTemplate v-if="!isGuest" :entries="entries" :key="project?.name || 'NONE'">
|
||||||
<template #acodes>
|
<template #acodes>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<LyxUiInput class="w-full px-4 py-2" placeholder="Appsumo code" v-model="currentCode"></LyxUiInput>
|
<LyxUiInput class="w-full px-4 py-2" placeholder="Appsumo code" v-model="currentCode"></LyxUiInput>
|
||||||
@@ -58,4 +58,9 @@ async function redeemCode() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SettingsTemplate>
|
</SettingsTemplate>
|
||||||
|
|
||||||
|
<div v-if="isGuest" class="text-lyx-text-darker flex w-full h-full justify-center mt-20">
|
||||||
|
Guests cannot view billing
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import DeleteDomainData from '../dialog/DeleteDomainData.vue';
|
import DeleteDomainData from '../dialog/DeleteDomainData.vue';
|
||||||
import type { SettingsTemplateEntry } from './Template.vue';
|
import type { SettingsTemplateEntry } from './Template.vue';
|
||||||
|
|
||||||
|
|
||||||
|
const { isGuest } = useProject();
|
||||||
|
|
||||||
const entries: SettingsTemplateEntry[] = [
|
const entries: SettingsTemplateEntry[] = [
|
||||||
{ id: 'delete_dns', title: 'Delete domain data', text: 'Delete data of a specific domain from this project' },
|
{ id: 'delete_dns', title: 'Delete domain data', text: 'Delete data of a specific domain from this project' },
|
||||||
{ id: 'delete_data', title: 'Delete project data', text: 'Delete all data from this project' },
|
{ id: 'delete_data', title: 'Delete project data', text: 'Delete all data from this project' },
|
||||||
@@ -105,15 +108,17 @@ const sessionsLabel = computed(() => {
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|
||||||
<!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> -->
|
<!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> -->
|
||||||
<USelectMenu placeholder="Select a domain" :uiMenu="{
|
<USelectMenu v-if="!isGuest" placeholder="Select a domain" :uiMenu="{
|
||||||
select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
|
select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
|
||||||
base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
|
base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget',
|
||||||
option: {
|
option: {
|
||||||
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
|
base: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||||
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
|
active: '!bg-lyx-lightmode-widget-light dark:!bg-lyx-widget-lighter'
|
||||||
}
|
}
|
||||||
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
|
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
|
||||||
|
|
||||||
|
<div v-if="isGuest" class="text-lyx-text-darker"> Guests cannot delete data</div>
|
||||||
|
|
||||||
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
|
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
|
||||||
<div class="text-[.9rem] text-lyx-text-dark"> Select data to delete </div>
|
<div class="text-[.9rem] text-lyx-text-dark"> Select data to delete </div>
|
||||||
|
|
||||||
@@ -141,7 +146,7 @@ const sessionsLabel = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template #delete_data>
|
<template #delete_data>
|
||||||
|
|
||||||
<div
|
<div v-if="!isGuest"
|
||||||
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-lyx-lightmode-widget-light dark:bg-[#1e1412]">
|
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-lyx-lightmode-widget-light dark:bg-[#1e1412]">
|
||||||
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
|
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
|
||||||
visits 0 events 0 sessions) </div>
|
visits 0 events 0 sessions) </div>
|
||||||
@@ -151,6 +156,7 @@ const sessionsLabel = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isGuest" class="text-lyx-text-darker"> Guests cannot delete data</div>
|
||||||
</template>
|
</template>
|
||||||
</SettingsTemplate>
|
</SettingsTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -156,20 +156,28 @@ function copyProjectId() {
|
|||||||
<template>
|
<template>
|
||||||
<SettingsTemplate :entries="entries" :key="project?.name || 'NONE'">
|
<SettingsTemplate :entries="entries" :key="project?.name || 'NONE'">
|
||||||
<template #pname>
|
<template #pname>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex flex-col gap-2">
|
||||||
<LyxUiInput class="w-full px-4 py-2" :disabled="isGuest" v-model="projectNameInputVal"></LyxUiInput>
|
<div class="flex items-center gap-4">
|
||||||
<LyxUiButton v-if="!isGuest" @click="changeProjectName()" :disabled="!canChange" type="primary"> Change
|
<LyxUiInput class="w-full px-4 py-2" :disabled="isGuest" v-model="projectNameInputVal"></LyxUiInput>
|
||||||
</LyxUiButton>
|
<LyxUiButton v-if="!isGuest" @click="changeProjectName()" :disabled="!canChange" type="primary">
|
||||||
|
Change
|
||||||
|
</LyxUiButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="isGuest" class="text-lyx-text-darker"> *Guests cannot change project name </div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #api>
|
<template #api>
|
||||||
<div class="flex items-center gap-4" v-if="apiKeys && apiKeys.length < 5">
|
<div class="flex flex-col gap-2" v-if="apiKeys && apiKeys.length < 5">
|
||||||
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName" v-model="newApiKeyName">
|
<div class="flex items-center gap-4">
|
||||||
</LyxUiInput>
|
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName"
|
||||||
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3"
|
v-model="newApiKeyName">
|
||||||
type="primary">
|
</LyxUiInput>
|
||||||
<i class="far fa-plus"></i>
|
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3"
|
||||||
</LyxUiButton>
|
type="primary">
|
||||||
|
<i class="far fa-plus"></i>
|
||||||
|
</LyxUiButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="isGuest" class="text-lyx-text-darker"> *Guests cannot manage api keys </div>
|
||||||
</div>
|
</div>
|
||||||
<LyxUiCard v-if="apiKeys && apiKeys.length > 0" class="w-full flex flex-col gap-4 items-center mt-4">
|
<LyxUiCard v-if="apiKeys && apiKeys.length > 0" class="w-full flex flex-col gap-4 items-center mt-4">
|
||||||
<div v-for="apiKey of apiKeys" class="flex flex-col w-full">
|
<div v-for="apiKey of apiKeys" class="flex flex-col w-full">
|
||||||
@@ -201,10 +209,10 @@ function copyProjectId() {
|
|||||||
<div class="hidden lg:flex"><i class="far fa-copy" @click="copyScript()"></i></div>
|
<div class="hidden lg:flex"><i class="far fa-copy" @click="copyScript()"></i></div>
|
||||||
</LyxUiCard>
|
</LyxUiCard>
|
||||||
<div class="flex justify-end w-full">
|
<div class="flex justify-end w-full">
|
||||||
<LyxUiButton type="outline" class="flex lg:hidden mt-4">
|
<LyxUiButton type="outline" class="flex lg:hidden mt-4">
|
||||||
Copy script
|
Copy script
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #pdelete>
|
<template #pdelete>
|
||||||
<div class="flex lg:justify-end" v-if="!isGuest">
|
<div class="flex lg:justify-end" v-if="!isGuest">
|
||||||
@@ -212,6 +220,7 @@ function copyProjectId() {
|
|||||||
Delete project
|
Delete project
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isGuest"> *Guests cannot delete project </div>
|
||||||
</template>
|
</template>
|
||||||
</SettingsTemplate>
|
</SettingsTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const { showDrawer } = useDrawer();
|
|||||||
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsTemplate v-if="!invoicesPending && !planPending" :entries="entries">
|
<SettingsTemplate v-if="!invoicesPending && !planPending && !isGuest" :entries="entries">
|
||||||
<template #info>
|
<template #info>
|
||||||
<div v-if="!isGuest">
|
<div v-if="!isGuest">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
@@ -175,7 +175,8 @@ const { showDrawer } = useDrawer();
|
|||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div class="poppins font-semibold text-[2rem]"> €
|
<div class="poppins font-semibold text-[2rem]"> €
|
||||||
{{ getPremiumPrice(planData.premium_type) }} </div>
|
{{ getPremiumPrice(planData.premium_type) }} </div>
|
||||||
<div class="poppins text-lyx-lightmode-text-dark dark:text-text-sub mt-2"> per month </div>
|
<div class="poppins text-lyx-lightmode-text-dark dark:text-text-sub mt-2"> per month
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -266,6 +267,10 @@ const { showDrawer } = useDrawer();
|
|||||||
</CardTitled>
|
</CardTitled>
|
||||||
</template>
|
</template>
|
||||||
</SettingsTemplate>
|
</SettingsTemplate>
|
||||||
|
|
||||||
|
<div v-if="isGuest" class="text-lyx-text-darker flex w-full h-full justify-center mt-20">
|
||||||
|
Guests cannot view billing
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ const entries: SettingsTemplateEntry[] = [
|
|||||||
User should have been registered to Litlyx
|
User should have been registered to Litlyx
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isGuest" class="text-lyx-text-darker"> Guests cannot add members</div>
|
||||||
</template>
|
</template>
|
||||||
<template #members>
|
<template #members>
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
project_id,
|
project_id,
|
||||||
_id: '___allTime' as any,
|
_id: '___allTime' as any,
|
||||||
name: 'All Time',
|
name: 'All Time',
|
||||||
from: fns.addMinutes(fns.startOfMonth(new Date(project_created_at.toString())), -new Date().getTimezoneOffset()),
|
from: fns.addMinutes(fns.startOfMonth(new Date(project_created_at.toString())), 0),
|
||||||
to: new Date(Date.now()),
|
to: new Date(Date.now()),
|
||||||
color: '#9362FF',
|
color: '#9362FF',
|
||||||
default: true
|
default: true
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ type RefOrPrimitive<T> = T | Ref<T> | ComputedRef<T>
|
|||||||
|
|
||||||
export type CustomOptions = {
|
export type CustomOptions = {
|
||||||
useSnapshotDates?: boolean,
|
useSnapshotDates?: boolean,
|
||||||
|
useActiveDomain?: boolean,
|
||||||
useActivePid?: boolean,
|
useActivePid?: boolean,
|
||||||
useTimeOffset?: boolean,
|
useTimeOffset?: boolean,
|
||||||
slice?: RefOrPrimitive<string>,
|
slice?: RefOrPrimitive<string>,
|
||||||
limit?: RefOrPrimitive<number | string>,
|
limit?: RefOrPrimitive<number | string>,
|
||||||
custom?: Record<string, RefOrPrimitive<string>>
|
custom?: Record<string, RefOrPrimitive<string>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token } = useAccessToken();
|
const { token } = useAccessToken();
|
||||||
const { projectId } = useProject();
|
const { projectId } = useProject();
|
||||||
const { safeSnapshotDates } = useSnapshot()
|
const { safeSnapshotDates } = useSnapshot()
|
||||||
|
const { domain } = useDomain();
|
||||||
|
|
||||||
function getValueFromRefOrPrimitive<T>(data?: T | Ref<T> | ComputedRef<T>) {
|
function getValueFromRefOrPrimitive<T>(data?: T | Ref<T> | ComputedRef<T>) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@@ -24,6 +26,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
|
|||||||
const useSnapshotDates = customOptions?.useSnapshotDates || true;
|
const useSnapshotDates = customOptions?.useSnapshotDates || true;
|
||||||
const useActivePid = customOptions?.useActivePid || true;
|
const useActivePid = customOptions?.useActivePid || true;
|
||||||
const useTimeOffset = customOptions?.useTimeOffset || true;
|
const useTimeOffset = customOptions?.useTimeOffset || true;
|
||||||
|
const useActiveDomain = customOptions?.useActiveDomain || true;
|
||||||
|
|
||||||
const headers = computed<Record<string, string>>(() => {
|
const headers = computed<Record<string, string>>(() => {
|
||||||
// console.trace('Computed recalculated');
|
// console.trace('Computed recalculated');
|
||||||
@@ -41,6 +44,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
|
|||||||
'x-time-offset': useTimeOffset ? (new Date().getTimezoneOffset().toString()) : '',
|
'x-time-offset': useTimeOffset ? (new Date().getTimezoneOffset().toString()) : '',
|
||||||
'x-slice': getValueFromRefOrPrimitive(customOptions?.slice) ?? '',
|
'x-slice': getValueFromRefOrPrimitive(customOptions?.slice) ?? '',
|
||||||
'x-limit': getValueFromRefOrPrimitive(customOptions?.limit)?.toString() ?? '',
|
'x-limit': getValueFromRefOrPrimitive(customOptions?.limit)?.toString() ?? '',
|
||||||
|
'x-domain': useActiveDomain ? (domain.value ?? '') : '',
|
||||||
...parsedCustom
|
...parsedCustom
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
47
dashboard/composables/useDomain.ts
Normal file
47
dashboard/composables/useDomain.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const { token } = useAccessToken();
|
||||||
|
const { projectId } = useProject();
|
||||||
|
|
||||||
|
const domainsRequest = useFetch<{ _id: string, visits: number }[]>('/api/domains/list', {
|
||||||
|
headers: computed(() => {
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${token.value}`,
|
||||||
|
'x-pid': projectId.value || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
function refreshDomains() {
|
||||||
|
domainsRequest.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshingDomains = computed(() => domainsRequest.pending.value);
|
||||||
|
|
||||||
|
const domainList = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
_id: 'ALL DOMAINS', visits: domainsRequest.data.value?.reduce((a, e) => a + e.visits, 0)
|
||||||
|
},
|
||||||
|
...(domainsRequest.data.value?.sort((a, b) => b.visits - a.visits) || [])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const activeDomain = ref<string>();
|
||||||
|
|
||||||
|
const domain = computed(() => {
|
||||||
|
if (activeDomain.value) return activeDomain.value;
|
||||||
|
if (!domainList.value) return;
|
||||||
|
if (domainList.value.length == 0) return;
|
||||||
|
setActiveDomain(domainList.value[0]._id);
|
||||||
|
return domainList.value[0]._id;
|
||||||
|
})
|
||||||
|
|
||||||
|
function setActiveDomain(domain: string) {
|
||||||
|
activeDomain.value = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDomain() {
|
||||||
|
return { domainList, domain, setActiveDomain, refreshDomains, refreshingDomains }
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
apps: [
|
|
||||||
{
|
|
||||||
name: 'Dashboard',
|
|
||||||
port: '3010',
|
|
||||||
exec_mode: 'fork',
|
|
||||||
script: './.output/server/index.mjs',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import type { Section } from '~/components/CVerticalNavigation.vue';
|
import type { Section } from '~/components/layout/VerticalNavigation.vue';
|
||||||
|
|
||||||
import { Lit } from 'litlyx-js';
|
import { Lit } from 'litlyx-js';
|
||||||
import { DialogFeedback } from '#components';
|
import { DialogFeedback } from '#components';
|
||||||
@@ -12,8 +12,6 @@ const modal = useModal();
|
|||||||
|
|
||||||
const selfhosted = useSelfhosted();
|
const selfhosted = useSelfhosted();
|
||||||
|
|
||||||
console.log({ selfhosted })
|
|
||||||
|
|
||||||
const sections: Section[] = [
|
const sections: Section[] = [
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -21,30 +19,32 @@ const sections: Section[] = [
|
|||||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||||
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
|
|
||||||
|
|
||||||
|
// { label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
|
||||||
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||||
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||||
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||||
|
|
||||||
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
{ grow: true, label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||||
{
|
// {
|
||||||
grow: true,
|
// grow: true,
|
||||||
label: 'Leave a Feedback', icon: 'fal fa-message',
|
// label: 'Leave a Feedback', icon: 'fal fa-message',
|
||||||
action() {
|
// action() {
|
||||||
modal.open(DialogFeedback, {});
|
// modal.open(DialogFeedback, {});
|
||||||
},
|
// },
|
||||||
disabled: selfhosted
|
// disabled: selfhosted
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
|
// grow: true,
|
||||||
action() { Lit.event('docs_clicked') },
|
// label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
|
||||||
},
|
// action() { Lit.event('docs_clicked') },
|
||||||
{
|
// },
|
||||||
label: 'Discord support', icon: 'fab fa-discord',
|
// {
|
||||||
to: 'https://discord.gg/9cQykjsmWX',
|
// grow: true,
|
||||||
external: true,
|
// label: 'Discord support', icon: 'fab fa-discord',
|
||||||
},
|
// to: 'https://discord.gg/9cQykjsmWX',
|
||||||
|
// external: true,
|
||||||
|
// },
|
||||||
// {
|
// {
|
||||||
// label: 'Slack support', icon: 'fab fa-slack',
|
// label: 'Slack support', icon: 'fab fa-slack',
|
||||||
// to: '#',
|
// to: '#',
|
||||||
@@ -76,11 +76,11 @@ const { isOpen, close, open } = useMenu();
|
|||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="px-6 py-3 flex items-center justify-center shadow-[0_0_10px_#000000CC] z-[20] rounded-xl mx-2 my-2 lg:hidden">
|
class="px-6 py-3 flex items-center justify-center dark:bg-lyx-background-light z-[20] rounded-xl mx-2 my-2 lg:hidden">
|
||||||
<i @click="open()" class="fas fa-bars text-[1.2rem] absolute left-6"></i>
|
<i @click="open()" class="fas fa-bars text-[1.2rem] absolute left-6"></i>
|
||||||
<div class="nunito font-semibold text-[1.2rem]">
|
<!-- <div class="nunito font-semibold text-[1.2rem]">
|
||||||
Litlyx
|
Litlyx
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
@@ -91,8 +91,8 @@ const { isOpen, close, open } = useMenu();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<CVerticalNavigation :sections="sections">
|
<LayoutVerticalNavigation :sections="sections">
|
||||||
</CVerticalNavigation>
|
</LayoutVerticalNavigation>
|
||||||
|
|
||||||
|
|
||||||
<div class="overflow-hidden w-full bg-lyx-lightmode-background dark:bg-lyx-background relative h-full">
|
<div class="overflow-hidden w-full bg-lyx-lightmode-background dark:bg-lyx-background relative h-full">
|
||||||
@@ -107,7 +107,12 @@ const { isOpen, close, open } = useMenu();
|
|||||||
<DashboardDialogBarCard @click.stop="null" class="z-[36]"></DashboardDialogBarCard>
|
<DashboardDialogBarCard @click.stop="null" class="z-[36]"></DashboardDialogBarCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot></slot>
|
<LayoutTopNavigation class="flex"></LayoutTopNavigation>
|
||||||
|
|
||||||
|
<div class="h-full pb-[3rem]">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,15 +26,16 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
pages: true,
|
pages: true,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
css: ['~/assets/scss/main.scss'],
|
css: [
|
||||||
|
'~/assets/main.css',
|
||||||
|
'~/assets/scss/main.scss',
|
||||||
|
],
|
||||||
alias: {
|
alias: {
|
||||||
'@schema': fileURLToPath(new URL('../shared/schema', import.meta.url)),
|
'@schema': fileURLToPath(new URL('./shared/schema', import.meta.url)),
|
||||||
'@services': fileURLToPath(new URL('../shared/services', import.meta.url)),
|
'@services': fileURLToPath(new URL('./shared/services', import.meta.url)),
|
||||||
'@data': fileURLToPath(new URL('../shared/data', import.meta.url)),
|
'@data': fileURLToPath(new URL('./shared/data', import.meta.url)),
|
||||||
'@functions': fileURLToPath(new URL('../shared/functions', import.meta.url)),
|
'@functions': fileURLToPath(new URL('./shared/functions', import.meta.url)),
|
||||||
},
|
},
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
MONGO_CONNECTION_STRING: process.env.MONGO_CONNECTION_STRING,
|
MONGO_CONNECTION_STRING: process.env.MONGO_CONNECTION_STRING,
|
||||||
REDIS_URL: process.env.REDIS_URL,
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
@@ -43,8 +44,7 @@ export default defineNuxtConfig({
|
|||||||
AI_ORG: process.env.AI_ORG,
|
AI_ORG: process.env.AI_ORG,
|
||||||
AI_PROJECT: process.env.AI_PROJECT,
|
AI_PROJECT: process.env.AI_PROJECT,
|
||||||
AI_KEY: process.env.AI_KEY,
|
AI_KEY: process.env.AI_KEY,
|
||||||
EMAIL_SERVICE: process.env.EMAIL_SERVICE,
|
EMAIL_SECRET: process.env.EMAIL_SECRET,
|
||||||
BREVO_API_KEY: process.env.BREVO_API_KEY,
|
|
||||||
AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET,
|
AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET,
|
||||||
GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID,
|
GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID,
|
||||||
GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET,
|
GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET,
|
||||||
@@ -56,6 +56,7 @@ export default defineNuxtConfig({
|
|||||||
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
|
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
|
||||||
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
|
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
|
||||||
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
|
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
|
||||||
|
MODE: process.env.MODE || 'NONE',
|
||||||
SELFHOSTED: process.env.SELFHOSTED || 'FALSE',
|
SELFHOSTED: process.env.SELFHOSTED || 'FALSE',
|
||||||
public: {
|
public: {
|
||||||
AUTH_MODE: process.env.AUTH_MODE,
|
AUTH_MODE: process.env.AUTH_MODE,
|
||||||
|
|||||||
@@ -3,32 +3,39 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "npm run workspace:shared && nuxt build --dotenv .env.testmode",
|
||||||
"dev": "nuxt dev",
|
"build:prod": "npm run workspace:shared && nuxt build --dotenv .env.prod",
|
||||||
|
"dev": "npm run workspace:shared && nuxt dev --dotenv .env.testmode",
|
||||||
|
"dev:prod": "npm run workspace:shared && nuxi dev --dotenv .env.prod",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"docker-build": "docker build -t litlyx-dashboard -f Dockerfile ../",
|
"docker-build": "docker build -t litlyx-dashboard -f Dockerfile ../",
|
||||||
"docker-inspect": "docker run -it litlyx-dashboard sh",
|
"docker-inspect": "docker run -it litlyx-dashboard sh",
|
||||||
"docker-run": "docker run -p 3000:3000 litlyx-dashboard"
|
"docker-run": "docker run -p 3000:3000 litlyx-dashboard",
|
||||||
|
"workspace:shared": "ts-node ../scripts/dashboard/shared.ts",
|
||||||
|
"workspace:deploy": "ts-node ../scripts/dashboard/deploy.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||||
"chart.js": "^3.9.1",
|
"chart.js": "^3.9.1",
|
||||||
"chartjs-chart-funnel": "^4.2.1",
|
"chartjs-chart-funnel": "^4.2.1",
|
||||||
"chartjs-plugin-annotation": "^2.2.1",
|
"chartjs-plugin-annotation": "^2.2.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"google-auth-library": "^9.10.0",
|
"google-auth-library": "^9.10.0",
|
||||||
"googleapis": "^144.0.0",
|
"googleapis": "^144.0.0",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"litlyx-js": "^1.0.3",
|
"litlyx-js": "^1.0.3",
|
||||||
|
"mongoose": "^8.9.5",
|
||||||
"nuxt": "^3.11.2",
|
"nuxt": "^3.11.2",
|
||||||
"nuxt-vue3-google-signin": "^0.0.11",
|
"nuxt-vue3-google-signin": "^0.0.11",
|
||||||
"openai": "^4.61.0",
|
"openai": "^4.61.0",
|
||||||
"pdfkit": "^0.15.0",
|
"pdfkit": "^0.15.0",
|
||||||
"primevue": "^3.52.0",
|
"primevue": "^3.52.0",
|
||||||
"sass": "^1.81.0",
|
"redis": "^4.7.0",
|
||||||
|
"sass": "^1.83.4",
|
||||||
"stripe": "^17.3.1",
|
"stripe": "^17.3.1",
|
||||||
"v-calendar": "^3.1.2",
|
"v-calendar": "^3.1.2",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
@@ -49,4 +56,4 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"tailwindcss": "^3.4.3"
|
"tailwindcss": "^3.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,24 +398,29 @@ async function clearAllChats() {
|
|||||||
<div :class="{ '!text-green-500': debugModeAi }" class="cursor-pointer text-red-500 w-fit"
|
<div :class="{ '!text-green-500': debugModeAi }" class="cursor-pointer text-red-500 w-fit"
|
||||||
v-if="userRoles.isAdmin.value" @click="debugModeAi = !debugModeAi"> Debug mode </div>
|
v-if="userRoles.isAdmin.value" @click="debugModeAi = !debugModeAi"> Debug mode </div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center pt-3">
|
<div class="flex pt-3 px-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="bg-accent w-5 h-5 rounded-full animate-pulse">
|
<!-- <div class="bg-accent w-4 h-4 rounded-full animate-pulse">
|
||||||
</div>
|
</div> -->
|
||||||
<div class="manrope font-semibold text-lyx-lightmode-text dark:text-text-dirty"> {{
|
<div class="manrope font-semibold text-lyx-lightmode-text dark:text-text-dirty">
|
||||||
chatsRemaining }} remaining requests
|
{{ chatsRemaining }} messages left
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LyxUiButton v-if="!selfhosted" type="primary" class="text-[.9rem] text-center " @click="showDrawer('PRICING')">
|
<div class="grow"></div>
|
||||||
|
<LyxUiButton v-if="!selfhosted" type="primary" class="text-[.9rem] text-center "
|
||||||
|
@click="showDrawer('PRICING')">
|
||||||
Upgrade
|
Upgrade
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="dark:bg-lyx-widget-light bg-lyx-lightmode-widget-light h-[1px]"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 px-4 mt-4">
|
||||||
<div class="poppins font-semibold text-[1.1rem]"> History </div>
|
<div class="poppins font-semibold text-[1.1rem]"> History </div>
|
||||||
|
<div class="grow"></div>
|
||||||
<LyxUiButton v-if="chatsList && chatsList.length > 0" @click="clearAllChats()" type="secondary"
|
<LyxUiButton v-if="chatsList && chatsList.length > 0" @click="clearAllChats()" type="secondary"
|
||||||
class="text-center text-[.8rem]">
|
class="text-center text-[.8rem]">
|
||||||
Clear all
|
Clear all chats
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const canDownload = computed(() => {
|
|||||||
const metricsInfo = ref<number>(0);
|
const metricsInfo = ref<number>(0);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'website', label: 'Website', sortable: true },
|
{ key: 'website', label: 'Domain', sortable: true },
|
||||||
{ key: 'page', label: 'Page', sortable: true },
|
{ key: 'page', label: 'Page', sortable: true },
|
||||||
{ key: 'referrer', label: 'Referrer', sortable: true },
|
{ key: 'referrer', label: 'Referrer', sortable: true },
|
||||||
{ key: 'browser', label: 'Browser', sortable: true },
|
{ key: 'browser', label: 'Browser', sortable: true },
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import EventsFunnelChart from '~/components/events/EventsFunnelChart.vue';
|
import EventsFunnelChart from '~/components/events/EventsFunnelChart.vue';
|
||||||
|
import DateService, { type Slice } from '@services/DateService';
|
||||||
|
|
||||||
definePageMeta({ layout: 'dashboard' });
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
const selectLabelsEvents = [
|
|
||||||
|
const { snapshotDuration } = useSnapshot();
|
||||||
|
|
||||||
|
const selectedLabelIndex = ref<number>(1);
|
||||||
|
|
||||||
|
const selectLabels: { label: string, value: Slice }[] = [
|
||||||
|
{ label: 'Hour', value: 'hour' },
|
||||||
{ label: 'Day', value: 'day' },
|
{ label: 'Day', value: 'day' },
|
||||||
{ label: 'Month', value: 'month' },
|
{ label: 'Month', value: 'month' },
|
||||||
];
|
];
|
||||||
const eventsStackedSelectIndex = ref<number>(0);
|
|
||||||
|
|
||||||
const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeaders({ custom: { 'x-schema': 'events' } }), lazy: true });
|
const selectLabelsAvailable = computed<{ label: string, value: Slice, disabled: boolean }[]>(() => {
|
||||||
|
return selectLabels.map(e => {
|
||||||
|
return { ...e, disabled: !DateService.canUseSliceFromDays(snapshotDuration.value, e.value)[0] }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventsData = await useFetch(`/api/data/count`, {
|
||||||
|
headers: useComputedHeaders({ custom: { 'x-schema': 'events' } }),
|
||||||
|
lazy: true
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -24,9 +38,6 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
|||||||
<div>
|
<div>
|
||||||
Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
|
Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="(eventsData.data.value?.[0]?.count || 0) === 0">
|
|
||||||
Waiting for your first event...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<LyxUiButton type="secondary" target="_blank" to="https://docs.litlyx.com/custom-events">
|
<LyxUiButton type="secondary" target="_blank" to="https://docs.litlyx.com/custom-events">
|
||||||
@@ -45,12 +56,14 @@ const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeade
|
|||||||
<CardTitled :key="refreshKey" class="p-4 xl:flex-[4] w-full h-full" title="Events"
|
<CardTitled :key="refreshKey" class="p-4 xl:flex-[4] w-full h-full" title="Events"
|
||||||
sub="Events stacked bar chart.">
|
sub="Events stacked bar chart.">
|
||||||
<template #header>
|
<template #header>
|
||||||
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
|
|
||||||
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
|
<SelectButton class="w-fit" @changeIndex="selectedLabelIndex = $event"
|
||||||
|
:currentIndex="selectedLabelIndex" :options="selectLabelsAvailable">
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)">
|
<EventsStackedBarChart :slice="(selectLabelsAvailable[selectedLabelIndex].value as any)">
|
||||||
</EventsStackedBarChart>
|
</EventsStackedBarChart>
|
||||||
</div>
|
</div>
|
||||||
</CardTitled>
|
</CardTitled>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ onMounted(async () => {
|
|||||||
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
|
||||||
const loggedUser = useLoggedUser();
|
const loggedUser = useLoggedUser();
|
||||||
loggedUser.user = user;
|
loggedUser.user = user;
|
||||||
// setTimeout(() => { location.reload(); }, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (justLogged.value) { setTimeout(() => { location.href = '/' }, 500) }
|
if (justLogged.value) { setTimeout(() => { location.href = '/' }, 500) }
|
||||||
@@ -41,31 +40,33 @@ const selfhosted = useSelfhosted();
|
|||||||
|
|
||||||
|
|
||||||
<div v-if="showDashboard">
|
<div v-if="showDashboard">
|
||||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||||
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
||||||
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
|
<!-- <BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
|
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
|
||||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
|
||||||
|
<div class="flex w-full justify-center mt-6 px-6">
|
||||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
|
<BarCardPages :key="refreshKey"></BarCardPages>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
<div class="flex w-full justify-center mt-6 px-6">
|
||||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -75,8 +76,9 @@ const selfhosted = useSelfhosted();
|
|||||||
<BarCardDevices :key="refreshKey"></BarCardDevices>
|
<BarCardDevices :key="refreshKey"></BarCardDevices>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
<div class="flex w-full justify-center mt-6 px-6">
|
||||||
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
<div class="flex w-full gap-6 flex-col xl:flex-row">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -86,7 +88,7 @@ const selfhosted = useSelfhosted();
|
|||||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const selectLabelsEvents = [
|
|||||||
Litlyx open metrics
|
Litlyx open metrics
|
||||||
</div>
|
</div>
|
||||||
<div v-if="project" class="flex gap-2 items-center text-text/90">
|
<div v-if="project" class="flex gap-2 items-center text-text/90">
|
||||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
<div class="animate-pulse w-[.8rem] h-[.8rem] bg-green-400 rounded-full"> </div>
|
||||||
<div> {{ onlineUsers }} Online users</div>
|
<div> {{ onlineUsers }} Online users</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async function createProject() {
|
|||||||
<div class="flex flex-col items-center justify-center pt-[12rem] gap-12 relative z-[10]">
|
<div class="flex flex-col items-center justify-center pt-[12rem] gap-12 relative z-[10]">
|
||||||
|
|
||||||
<div class="text-[3rem] font-semibold text-center text-lyx-lightmode-text dark:text-lyx-text">
|
<div class="text-[3rem] font-semibold text-center text-lyx-lightmode-text dark:text-lyx-text">
|
||||||
Create your {{ isFirstProject ? 'first' : '' }} project
|
Create {{ isFirstProject ? '' : 'a new' }} {{ isFirstProject ? 'your first' : '' }} project
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isFirstProject" class="text-[1.5rem]">
|
<div v-if="isFirstProject" class="text-[1.5rem]">
|
||||||
|
|||||||
@@ -6,18 +6,6 @@ const reportList = useFetch(`/api/security/list`, { headers: useComputedHeaders(
|
|||||||
|
|
||||||
const { createAlert } = useAlert();
|
const { createAlert } = useAlert();
|
||||||
|
|
||||||
function showAnomalyInfoAlert() {
|
|
||||||
createAlert('AI Anomaly Detector info',
|
|
||||||
`Anomaly detector is running. It helps you detect a spike in visits or events, it could mean an
|
|
||||||
attack or simply higher traffic due to good performance. Additionally, it can detect if someone is
|
|
||||||
stealing parts of your website and hosting a duplicate version—an unfortunately common practice.
|
|
||||||
Litlyx will notify you via email with actionable advices`,
|
|
||||||
'far fa-shield',
|
|
||||||
10000
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const rows = computed(() => reportList.data.value || [])
|
const rows = computed(() => reportList.data.value || [])
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -33,14 +21,14 @@ const columns = [
|
|||||||
|
|
||||||
<div class="home w-full h-full px-10 pt-6 overflow-y-auto">
|
<div class="home w-full h-full px-10 pt-6 overflow-y-auto">
|
||||||
|
|
||||||
<div class="flex gap-2 items-center text-lyx-lightmode-text dark:text-text/90 justify-end">
|
<!-- <div class="flex gap-2 items-center text-lyx-lightmode-text dark:text-text/90 justify-end">
|
||||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||||
<div class="poppins font-regular text-[1rem]"> AI Anomaly Detector </div>
|
<div class="poppins font-regular text-[1rem]"> AI Anomaly Detector </div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
|
||||||
@click="showAnomalyInfoAlert"></i>
|
@click="showAnomalyInfoAlert"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="pb-[10rem]">
|
<div class="pb-[10rem]">
|
||||||
<UTable :rows="rows" :columns="columns">
|
<UTable :rows="rows" :columns="columns">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const items = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars">
|
<div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars !pb-[10rem]">
|
||||||
|
|
||||||
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>
|
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>
|
||||||
|
|
||||||
|
|||||||
2325
dashboard/pnpm-lock.yaml
generated
2325
dashboard/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,12 +1,11 @@
|
|||||||
import { AuthContext } from "./middleware/01-authorization";
|
import { AuthContext } from "./middleware/01-authorization";
|
||||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||||
import { LITLYX_PROJECT_ID } from '@data/LITLYX'
|
|
||||||
import { hasAccessToProject } from "./utils/hasAccessToProject";
|
import { hasAccessToProject } from "./utils/hasAccessToProject";
|
||||||
|
|
||||||
export async function getUserProjectFromId(project_id: string, user: AuthContext | undefined, allowGuest: boolean = true) {
|
export async function getUserProjectFromId(project_id: string, user: AuthContext | undefined, allowGuest: boolean = true) {
|
||||||
if (!project_id) return;
|
if (!project_id) return;
|
||||||
|
|
||||||
if (project_id === LITLYX_PROJECT_ID) {
|
if (project_id === "6643cd08a1854e3b81722ab5") {
|
||||||
return await ProjectModel.findOne({ _id: project_id });
|
return await ProjectModel.findOne({ _id: project_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ const getVisitsCountsTool: AIPlugin_TTool<'getVisitsCount'> = {
|
|||||||
from: { type: 'string', description: 'ISO string of start date' },
|
from: { type: 'string', description: 'ISO string of start date' },
|
||||||
to: { type: 'string', description: 'ISO string of end date' },
|
to: { type: 'string', description: 'ISO string of end date' },
|
||||||
website: { type: 'string', description: 'The website of the visits' },
|
website: { type: 'string', description: 'The website of the visits' },
|
||||||
page: { type: 'string', description: 'The page of the visit' }
|
page: { type: 'string', description: 'The page of the visit' },
|
||||||
|
domain: { type: 'string', description: 'Used only to filter a specific domain' }
|
||||||
},
|
},
|
||||||
required: ['from', 'to']
|
required: ['from', 'to']
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,7 @@ const getVisitsTimelineTool: AIPlugin_TTool<'getVisitsTimeline'> = {
|
|||||||
to: { type: 'string', description: 'ISO string of end date' },
|
to: { type: 'string', description: 'ISO string of end date' },
|
||||||
website: { type: 'string', description: 'The website of the visits' },
|
website: { type: 'string', description: 'The website of the visits' },
|
||||||
page: { type: 'string', description: 'The page of the visit' },
|
page: { type: 'string', description: 'The page of the visit' },
|
||||||
|
domain: { type: 'string', description: 'Used only to filter a specific domain' }
|
||||||
},
|
},
|
||||||
required: ['from', 'to']
|
required: ['from', 'to']
|
||||||
}
|
}
|
||||||
@@ -45,14 +47,15 @@ export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']>
|
|||||||
|
|
||||||
super({
|
super({
|
||||||
'getVisitsCount': {
|
'getVisitsCount': {
|
||||||
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string }) => {
|
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string, domain?: string }) => {
|
||||||
|
|
||||||
const query: any = {
|
const query: any = {
|
||||||
project_id: data.project_id,
|
project_id: data.project_id,
|
||||||
created_at: {
|
created_at: {
|
||||||
$gt: new Date(data.from),
|
$gt: new Date(data.from),
|
||||||
$lt: new Date(data.to),
|
$lt: new Date(data.to),
|
||||||
}
|
},
|
||||||
|
website: data.domain || { $ne: '_NODOMAIN_' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.website) query.website = data.website;
|
if (data.website) query.website = data.website;
|
||||||
@@ -67,7 +70,7 @@ export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']>
|
|||||||
tool: getVisitsCountsTool
|
tool: getVisitsCountsTool
|
||||||
},
|
},
|
||||||
'getVisitsTimeline': {
|
'getVisitsTimeline': {
|
||||||
handler: async (data: { project_id: string, from: string, to: string, time_offset: number, website?: string, page?: string }) => {
|
handler: async (data: { project_id: string, from: string, to: string, time_offset: number, website?: string, page?: string, domain?: string }) => {
|
||||||
|
|
||||||
const timelineData = await executeTimelineAggregation({
|
const timelineData = await executeTimelineAggregation({
|
||||||
projectId: new Types.ObjectId(data.project_id),
|
projectId: new Types.ObjectId(data.project_id),
|
||||||
@@ -75,7 +78,8 @@ export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']>
|
|||||||
from: data.from,
|
from: data.from,
|
||||||
to: data.to,
|
to: data.to,
|
||||||
slice: 'day',
|
slice: 'day',
|
||||||
timeOffset: data.time_offset
|
timeOffset: data.time_offset,
|
||||||
|
domain: data.domain || { $ne: '_NODOMAIN_' } as any
|
||||||
});
|
});
|
||||||
return { data: timelineData };
|
return { data: timelineData };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type OpenAI from "openai";
|
|||||||
import { getChartsInMessage } from "~/server/services/AiService";
|
import { getChartsInMessage } from "~/server/services/AiService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const isAdmin = data.user.user.roles.includes('ADMIN');
|
const isAdmin = data.user.user.roles.includes('ADMIN');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function getAiChatRemainings(project_id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid } = data;
|
const { pid } = data;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
|
|||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid } = data;
|
const { pid } = data;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
|
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
|
||||||
import { UserModel } from '@schema/UserSchema';
|
import { UserModel } from '@schema/UserSchema';
|
||||||
import { PasswordModel } from '@schema/PasswordSchema';
|
import { PasswordModel } from '@schema/PasswordSchema';
|
||||||
import EmailService from '@services/EmailService';
|
import { EmailService } from '@services/EmailService';
|
||||||
|
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
@@ -14,9 +15,12 @@ export default defineEventHandler(async event => {
|
|||||||
try {
|
try {
|
||||||
await PasswordModel.create({ email: data.email, password: data.password })
|
await PasswordModel.create({ email: data.email, password: data.password })
|
||||||
await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
|
await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
|
||||||
setImmediate(() => { EmailService.sendWelcomeEmail(data.email); });
|
setImmediate(() => {
|
||||||
|
const emailData = EmailService.getEmailServerInfo('welcome', { target: data.email });
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
|
});
|
||||||
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
|
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
|
||||||
return sendRedirect(event,`https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
|
return sendRedirect(event, `https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return setResponseStatus(event, 400, 'Error creating user');
|
return setResponseStatus(event, 400, 'Error creating user');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { OAuth2Client } from 'google-auth-library';
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
import { createUserJwt } from '~/server/AuthManager';
|
import { createUserJwt } from '~/server/AuthManager';
|
||||||
import { UserModel } from '@schema/UserSchema';
|
import { UserModel } from '@schema/UserSchema';
|
||||||
import EmailService from '@services/EmailService';
|
import { EmailService } from '@services/EmailService';
|
||||||
|
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||||
|
|
||||||
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
|
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
|
||||||
|
|
||||||
@@ -60,7 +61,9 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
console.log('SENDING WELCOME EMAIL TO', payload.email);
|
console.log('SENDING WELCOME EMAIL TO', payload.email);
|
||||||
if (payload.email) EmailService.sendWelcomeEmail(payload.email);
|
if (!payload.email) return;
|
||||||
|
const emailData = EmailService.getEmailServerInfo('welcome', { target: payload.email });
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) }
|
return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) }
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import { createRegisterJwt, createUserJwt } from '~/server/AuthManager';
|
import { createRegisterJwt } from '~/server/AuthManager';
|
||||||
import { UserModel } from '@schema/UserSchema';
|
import { UserModel } from '@schema/UserSchema';
|
||||||
import { RegisterModel } from '@schema/RegisterSchema';
|
import { RegisterModel } from '@schema/RegisterSchema';
|
||||||
import EmailService from '@services/EmailService';
|
import { EmailService } from '@services/EmailService';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||||
|
|
||||||
function canRegister(email: string, password: string) {
|
function canRegister(email: string, password: string) {
|
||||||
if (email.length == 0) return false;
|
if (email.length == 0) return false;
|
||||||
@@ -34,7 +35,8 @@ export default defineEventHandler(async event => {
|
|||||||
await RegisterModel.create({ email, password: hashedPassword });
|
await RegisterModel.create({ email, password: hashedPassword });
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
EmailService.sendConfirmEmail(email, `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}`);
|
const emailData = EmailService.getEmailServerInfo('confirm', { target: email, link: `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}` });
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `browsers:${pid}:${limit}:${from}:${to}`;
|
const cacheKey = `browsers:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
website: domain
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$browser", count: { $sum: 1, } } },
|
{ $group: { _id: "$browser", count: { $sum: 1, } } },
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
|
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: true });
|
const data = await getRequestData(event, ['GUEST', 'DOMAIN', 'RANGE', 'SCHEMA']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { schemaName, pid, from, to, model, project_id } = data;
|
const { schemaName, pid, from, to, model, project_id, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `count:${schemaName}:${pid}:${from}:${to}`;
|
const cacheKey = `count:${schemaName}:${pid}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 20;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
|
|
||||||
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
website: domain
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ $count: 'count' },
|
{ $count: 'count' },
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `countries:${pid}:${limit}:${from}:${to}`;
|
const cacheKey = `countries:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
website: domain
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$country", count: { $sum: 1, } } },
|
{ $group: { _id: "$country", count: { $sum: 1, } } },
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `devices:${pid}:${limit}:${from}:${to}`;
|
const cacheKey = `devices:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
website: domain
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$device", count: { $sum: 1, } } },
|
{ $group: { _id: "$device", count: { $sum: 1, } } },
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
|
|
||||||
import { EventModel } from "@schema/metrics/EventSchema";
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, ['GUEST', 'DOMAIN', 'RANGE']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `events:${pid}:${limit}:${from}:${to}`;
|
const cacheKey = `events:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 20;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
|
|
||||||
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
website: domain
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$name", count: { $sum: 1, } } },
|
{ $group: { _id: "$name", count: { $sum: 1, } } },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id, from, to } = data;
|
const { project_id, from, to } = data;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { PipelineStage } from "mongoose";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SessionModel } from "@schema/metrics/SessionSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `oss:${pid}:${limit}:${from}:${to}`;
|
const cacheKey = `oss:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
website: domain
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$os", count: { $sum: 1, } } },
|
{ $group: { _id: "$os", count: { $sum: 1, } } },
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `websites:${pid}:${limit}:${from}:${to}`;
|
const cacheKey = `pages:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
@@ -19,10 +18,11 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
}
|
website: domain
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$website", count: { $sum: 1, } } },
|
{ $group: { _id: "$page", count: { $sum: 1, } } },
|
||||||
{ $sort: { count: -1 } },
|
{ $sort: { count: -1 } },
|
||||||
{ $limit: limit }
|
{ $limit: limit }
|
||||||
]);
|
]);
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, ['OFFSET', 'RANGE', 'GUEST', 'DOMAIN']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `referrers:${pid}:${limit}:${from}:${to}`;
|
const cacheKey = `referrers:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
|
|||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
project_id,
|
project_id,
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
website: domain,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$referrer", count: { $sum: 1, } } },
|
{ $group: { _id: "$referrer", count: { $sum: 1, } } },
|
||||||
@@ -27,6 +27,7 @@ export default defineEventHandler(async event => {
|
|||||||
{ $limit: limit }
|
{ $limit: limit }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
return result as { _id: string, count: number }[];
|
return result as { _id: string, count: number }[];
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
|
||||||
import { Redis } from "~/server/services/CacheService";
|
|
||||||
import { getRequestData } from "~/server/utils/getRequestData";
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit } = data;
|
|
||||||
|
|
||||||
const websiteName = getHeader(event, 'x-website-name');
|
|
||||||
|
|
||||||
const cacheKey = `websites_pages:${websiteName}:${pid}:${limit}:${from}:${to}`;
|
|
||||||
const cacheExp = 60;
|
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
|
||||||
|
|
||||||
const result = await VisitModel.aggregate([
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
project_id,
|
|
||||||
created_at: { $gte: new Date(from), $lte: new Date(to) }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ $match: { website: websiteName, }, },
|
|
||||||
{ $group: { _id: "$page", count: { $sum: 1, } } },
|
|
||||||
{ $sort: { count: -1 } },
|
|
||||||
{ $limit: limit }
|
|
||||||
]);
|
|
||||||
|
|
||||||
return result as { _id: string, count: number }[];
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
18
dashboard/server/api/domains/list.ts
Normal file
18
dashboard/server/api/domains/list.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, ['GUEST']);
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project_id } = data;
|
||||||
|
|
||||||
|
const result = await VisitModel.aggregate([
|
||||||
|
{ $match: { project_id, } },
|
||||||
|
{ $group: { _id: "$website", visits: { $sum: 1 } } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return result as { _id: string, visits: number }[];
|
||||||
|
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { FeedbackModel } from '@schema/FeedbackSchema';
|
import { FeedbackModel } from '@schema/FeedbackSchema';
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { text } = await readBody(event);
|
const { text } = await readBody(event);
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
|
|
||||||
import { createUserJwt } from '~/server/AuthManager';
|
|
||||||
import { UserModel } from '@schema/UserSchema';
|
|
||||||
import EmailService from '@services/EmailService';
|
|
||||||
|
|
||||||
const config = useRuntimeConfig();
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
|
||||||
|
|
||||||
const { code } = getQuery(event);
|
|
||||||
console.log('CODE', code);
|
|
||||||
|
|
||||||
const redirect_uri = 'http://127.0.0.1:3000'
|
|
||||||
|
|
||||||
const res = await fetch(`https://github.com/login/oauth/access_token?client_id=${config.GITHUB_AUTH_CLIENT_ID}&client_secret=${config.GITHUB_AUTH_CLIENT_SECRET}&code=${code}&redirect_url=${redirect_uri}`, {
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Accept-Encoding": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
const access_token = data.access_token;
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
return sendRedirect(event,`http://127.0.0.1:3000/login?github_access_token=${access_token}`)
|
|
||||||
|
|
||||||
|
|
||||||
// const origin = event.headers.get('origin');
|
|
||||||
|
|
||||||
// const tokenResponse = await client.getToken({
|
|
||||||
// code: body.code,
|
|
||||||
// redirect_uri: origin || ''
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const tokens = tokenResponse.tokens;
|
|
||||||
|
|
||||||
// const ticket = await client.verifyIdToken({
|
|
||||||
// idToken: tokens.id_token || '',
|
|
||||||
// audience: GOOGLE_AUTH_CLIENT_ID,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const payload = ticket.getPayload();
|
|
||||||
// if (!payload) return { error: true, access_token: '' };
|
|
||||||
|
|
||||||
|
|
||||||
// const user = await UserModel.findOne({ email: payload.email });
|
|
||||||
|
|
||||||
// if (user) return { error: false, access_token: createUserJwt({ email: user.email, name: user.name }) }
|
|
||||||
|
|
||||||
|
|
||||||
// const newUser = new UserModel({
|
|
||||||
// email: payload.email,
|
|
||||||
// given_name: payload.given_name,
|
|
||||||
// name: payload.name,
|
|
||||||
// locale: payload.locale,
|
|
||||||
// picture: payload.picture,
|
|
||||||
// created_at: Date.now()
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const savedUser = await newUser.save();
|
|
||||||
|
|
||||||
// setImmediate(() => {
|
|
||||||
// console.log('SENDING WELCOME EMAIL TO', payload.email);
|
|
||||||
// if (payload.email) EmailService.sendWelcomeEmail(payload.email);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) }
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -18,7 +18,7 @@ export default defineEventHandler(async event => {
|
|||||||
if (body.name.length < 3) return setResponseStatus(event, 400, 'name too short');
|
if (body.name.length < 3) return setResponseStatus(event, 400, 'name too short');
|
||||||
if (body.name.length > 32) return setResponseStatus(event, 400, 'name too long');
|
if (body.name.length > 32) return setResponseStatus(event, 400, 'name too long');
|
||||||
|
|
||||||
const data = await getRequestData(event, { allowGuests: false, allowLitlyx: false, });
|
const data = await getRequestDataOld(event, { allowGuests: false, allowLitlyx: false, });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const data = await getRequestData(event, { allowGuests: false, allowLitlyx: false, });
|
const data = await getRequestDataOld(event, { allowGuests: false, allowLitlyx: false, });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function cryptApiKeyName(apiSettings: TApiSettings): TApiSettings {
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { allowGuests: false, allowLitlyx: false, requireRange: false });
|
const data = await getRequestDataOld(event, { allowGuests: false, allowLitlyx: false, requireRange: false });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { OnboardingModel } from '@schema/OnboardingSchema';
|
import { OnboardingModel } from '@schema/OnboardingSchema';
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { job, analytics } = await readBody(event);
|
const { job, analytics } = await readBody(event);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { OnboardingModel } from '@schema/OnboardingSchema';
|
|||||||
const { SELFHOSTED } = useRuntimeConfig();
|
const { SELFHOSTED } = useRuntimeConfig();
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestDataOld(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const exist = await OnboardingModel.exists({ user_id: data.user.id });
|
const exist = await OnboardingModel.exists({ user_id: data.user.id });
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import StripeService from '~/server/services/StripeService';
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project, pid } = data;
|
const { project, pid } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import StripeService from '~/server/services/StripeService';
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project, pid } = data;
|
const { project, pid } = data;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import StripeService from '~/server/services/StripeService';
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
|
const data = await getRequestData(event, []);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project } = data;
|
const { project } = data;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type InvoiceData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
|
const data = await getRequestData(event, []);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project, pid } = data;
|
const { project, pid } = data;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getPlanFromId } from "@data/PREMIUM";
|
import { getPlanFromId, PREMIUM_PLAN } from "@data/PREMIUM";
|
||||||
import { PREMIUM_PLAN } from "../../../../shared/data/PREMIUM";
|
|
||||||
import { canTryAppsumoCode, checkAppsumoCode, useAppsumoCode, useTryAppsumoCode } from "~/server/services/AppsumoService";
|
import { canTryAppsumoCode, checkAppsumoCode, useAppsumoCode, useTryAppsumoCode } from "~/server/services/AppsumoService";
|
||||||
import StripeService from '~/server/services/StripeService';
|
import StripeService from '~/server/services/StripeService';
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ function getPlanToActivate(current_plan_id: number) {
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
const data = await getRequestData(event, []);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project, pid, user } = data;
|
const { project, pid, user } = data;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user