Merge branch 'refactoring'

This commit is contained in:
Emily
2025-02-11 14:51:09 +01:00
216 changed files with 4485 additions and 14880 deletions

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ 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
View File

@@ -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

View File

@@ -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: ''
}
}
]
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 });
} }

View 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);
}
}
}

View File

@@ -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
View 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 });
}
})

View File

@@ -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);
}
} }

View File

@@ -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": [

View File

@@ -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
View 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');

View File

@@ -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");

View File

@@ -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>

View File

@@ -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">

View File

@@ -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"

View 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>

View File

@@ -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."

View File

@@ -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">

View File

@@ -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">

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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();
}); });

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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
} }
}) })

View 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 }
}

View File

@@ -1,10 +0,0 @@
module.exports = {
apps: [
{
name: 'Dashboard',
port: '3010',
exec_mode: 'fork',
script: './.output/server/index.mjs',
}
]
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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",

View File

@@ -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>

View File

@@ -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 },

View File

@@ -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>

View File

@@ -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,9 +40,9 @@ 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>
@@ -51,17 +50,19 @@ const selfhosted = useSelfhosted();
<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>
@@ -77,6 +78,7 @@ const selfhosted = useSelfhosted();
</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">

View File

@@ -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>

View File

@@ -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]">

View File

@@ -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">

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -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 });
} }

View File

@@ -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 };
}, },

View File

@@ -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;

View File

@@ -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');

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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');
} }

View File

@@ -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 }) }

View File

@@ -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 {

View File

@@ -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, } } },

View File

@@ -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' },

View File

@@ -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, } } },

View File

@@ -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, } } },

View File

@@ -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, } } },

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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, } } },

View File

@@ -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 }
]); ]);

View File

@@ -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 }[];
}); });

View File

@@ -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 }[];
});
});

View 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 }[];
});

View File

@@ -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);

View File

@@ -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 }) }
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 });

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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