refactoring dashboard

This commit is contained in:
Emily
2025-01-23 17:34:43 +01:00
parent afeaac1b0d
commit e4bdf7e4c3
112 changed files with 2345 additions and 12532 deletions

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 './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-family: "Geist";
src: url("../fonts/GeistVF.ttf");

View File

@@ -121,7 +121,7 @@ function openExternalLink(link: string) {
<i v-else :class="iconProvider(element)?.[1]"></i>
</div>
<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 }}
</span>
</div>

View File

@@ -57,7 +57,7 @@ async function showMore() {
<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 || []"
desc="The browsers most used to search your website." :dataIcons="true" :iconProvider="iconProvider"
:loading="browsersData.pending.value" label="Browsers" sub-label="Browsers">

View File

@@ -49,7 +49,7 @@ async function showMore() {
<template>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
:data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
label="Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"

View File

@@ -0,0 +1,37 @@
<script lang="ts" setup>
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;
}
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @dataReload="pagesData.refresh()" :showLink=true
:data="pagesData.data.value || []" :interactive="false" desc="Most visited pages."
: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>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService';
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
import { useLineChart, LineChart } from 'vue-chart-3';

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import DateService from '@services/DateService';
import type { Slice } from '@services/DateService';
import DateService, { type Slice } from '../../shared/services/DateService';
const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService';
import { type Slice } from '@services/DateService';
const props = defineProps<{ slice: Slice }>();
const slice = computed(() => props.slice);
@@ -10,45 +10,23 @@ const { safeSnapshotDates } = useSnapshot()
function transformResponse(input: { _id: string, name: string, count: number }[]) {
const fixed = fixMetrics({
data: input,
from: input[0]._id,
to: safeSnapshotDates.value.to
},
const fixed = fixMetrics(
{ data: input, from: input[0]._id, to: safeSnapshotDates.value.to },
slice.value,
{ advanced: true, advancedGroupKey: 'name' });
{ advanced: true, advancedGroupKey: 'name' }
);
const parsedDatasets: any[] = [];
const colors = [
"#5655d0",
"#6bbbe3",
"#a6d5cb",
"#fae0b9",
"#f28e8e",
"#e3a7e4",
"#c4a8e1",
"#8cc1d8",
"#f9c2cd",
"#b4e3b2",
"#ffdfba",
"#e9c3b5",
"#d5b8d6",
"#add7f6",
"#ffd1dc",
"#ffe7a1",
"#a8e6cf",
"#d4a5a5",
"#f3d6e4",
"#c3aed6"
"#5655d0", "#6bbbe3", "#a6d5cb", "#fae0b9", "#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++) {
const line: any = {
data: [],
color: colors[i] || '#FF0000',
label: fixed.allKeys[i]
};
const line: any = { data: [], color: colors[i] || '#FF0000', label: fixed.allKeys[i] };
parsedDatasets.push(line)
fixed.data.forEach((e: { key: string, value: number }[]) => {
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);
});
}
return {
datasets: parsedDatasets,
labels: fixed.labels
}
return { datasets: parsedDatasets, labels: fixed.labels }
}
const errorData = ref<{ errored: boolean, text: string }>({
@@ -88,7 +61,6 @@ const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
onResponse
});
onMounted(async () => {
eventsStackedData.execute();
});

View File

@@ -1,18 +1,7 @@
<script lang="ts" setup>
;
const { user } = useLoggedUser()
const { domainList, domain, setActiveDomain } = useDomain();
// function isProjectMine(owner?: string) {
// if (!owner) return false;
// if (!user.value) return false;
// if (!user.value.logged) return;
// return user.value.id == owner;
// }
function onChange(e: string) {
setActiveDomain(e);
}
@@ -24,7 +13,7 @@ function onChange(e: string) {
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: 'hover:!bg-lyx-lightmode-widget-light dark:hover:!bg-lyx-widget-lighter cursor-pointer',
base: 'z-[999] 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'
}
}" class="w-full" v-if="domainList" @change="onChange" :value="domain" :options="domainList">

View File

@@ -175,7 +175,8 @@ const { showDrawer } = useDrawer();
<div class="flex items-center gap-1">
<div class="poppins font-semibold text-[2rem]">
{{ 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 class="flex flex-col">

View File

@@ -26,15 +26,16 @@ export default defineNuxtConfig({
pages: true,
ssr: false,
css: ['~/assets/scss/main.scss'],
css: [
'~/assets/main.css',
'~/assets/scss/main.scss',
],
alias: {
'@schema': fileURLToPath(new URL('../shared/schema', import.meta.url)),
'@services': fileURLToPath(new URL('../shared/services', import.meta.url)),
'@data': fileURLToPath(new URL('../shared/data', import.meta.url)),
'@functions': fileURLToPath(new URL('../shared/functions', import.meta.url)),
'@schema': fileURLToPath(new URL('./shared/schema', import.meta.url)),
'@services': fileURLToPath(new URL('./shared/services', import.meta.url)),
'@data': fileURLToPath(new URL('./shared/data', import.meta.url)),
'@functions': fileURLToPath(new URL('./shared/functions', import.meta.url)),
},
runtimeConfig: {
MONGO_CONNECTION_STRING: process.env.MONGO_CONNECTION_STRING,
REDIS_URL: process.env.REDIS_URL,

View File

@@ -3,32 +3,36 @@
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"build": "npm run workspace:shared && nuxt build",
"dev": "npm run workspace:shared && nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"test": "vitest",
"docker-build": "docker build -t litlyx-dashboard -f Dockerfile ../",
"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": "node ../scripts/dashboard/shared.js"
},
"dependencies": {
"@nuxtjs/tailwindcss": "^6.12.0",
"chart.js": "^3.9.1",
"chartjs-chart-funnel": "^4.2.1",
"chartjs-plugin-annotation": "^2.2.1",
"dayjs": "^1.11.13",
"google-auth-library": "^9.10.0",
"googleapis": "^144.0.0",
"highlight.js": "^11.10.0",
"jsonwebtoken": "^9.0.2",
"litlyx-js": "^1.0.3",
"mongoose": "^8.9.5",
"nuxt": "^3.11.2",
"nuxt-vue3-google-signin": "^0.0.11",
"openai": "^4.61.0",
"pdfkit": "^0.15.0",
"primevue": "^3.52.0",
"sass": "^1.81.0",
"redis": "^4.7.0",
"sass": "^1.83.4",
"stripe": "^17.3.1",
"v-calendar": "^3.1.2",
"vue": "^3.4.21",
@@ -49,4 +53,4 @@
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3"
}
}
}

View File

@@ -18,7 +18,6 @@ onMounted(async () => {
const user = await $fetch<any>('/api/user/me', { headers: { 'Authorization': 'Bearer ' + token.value } })
const loggedUser = useLoggedUser();
loggedUser.user = user;
// setTimeout(() => { location.reload(); }, 100);
}
if (justLogged.value) { setTimeout(() => { location.href = '/' }, 500) }
@@ -41,7 +40,7 @@ const selfhosted = useSelfhosted();
<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>
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
</div>
@@ -50,23 +49,24 @@ const selfhosted = useSelfhosted();
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
</div>
-->
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
<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-1">
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
</div>
<div class="flex-1">
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
<BarCardPages :key="refreshKey"></BarCardPages>
</div>
</div>
</div>
<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-1">
@@ -76,7 +76,7 @@ const selfhosted = useSelfhosted();
<BarCardDevices :key="refreshKey"></BarCardDevices>
</div>
</div>
</div>
</div>
<div class="flex w-full justify-center mt-6 px-6">
@@ -88,7 +88,7 @@ const selfhosted = useSelfhosted();
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
</div>
</div>
</div>
</div> -->
</div>

2325
dashboard/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
import { AuthContext } from "./middleware/01-authorization";
import { ProjectModel } from "@schema/project/ProjectSchema";
import { LITLYX_PROJECT_ID } from '@data/LITLYX'
import { hasAccessToProject } from "./utils/hasAccessToProject";
export async function getUserProjectFromId(project_id: string, user: AuthContext | undefined, allowGuest: boolean = true) {
if (!project_id) return;
if (project_id === LITLYX_PROJECT_ID) {
if (project_id === "6643cd08a1854e3b81722ab5") {
return await ProjectModel.findOne({ _id: project_id });
}

View File

@@ -2,7 +2,7 @@
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import { PasswordModel } from '@schema/PasswordSchema';
import EmailService from '@services/EmailService';
// import EmailService from '@services/EmailService';
export default defineEventHandler(async event => {
@@ -14,7 +14,7 @@ export default defineEventHandler(async event => {
try {
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() });
setImmediate(() => { EmailService.sendWelcomeEmail(data.email); });
// setImmediate(() => { EmailService.sendWelcomeEmail(data.email); });
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
return sendRedirect(event,`https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
} catch (ex) {

View File

@@ -2,7 +2,7 @@
import { OAuth2Client } from 'google-auth-library';
import { createUserJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import EmailService from '@services/EmailService';
// import EmailService from '@services/EmailService';
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
@@ -58,10 +58,10 @@ export default defineEventHandler(async event => {
const savedUser = await newUser.save();
setImmediate(() => {
console.log('SENDING WELCOME EMAIL TO', payload.email);
if (payload.email) EmailService.sendWelcomeEmail(payload.email);
});
// 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

@@ -2,7 +2,7 @@
import { createRegisterJwt, createUserJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import { RegisterModel } from '@schema/RegisterSchema';
import EmailService from '@services/EmailService';
// import EmailService from '@services/EmailService';
import crypto from 'crypto';
function canRegister(email: string, password: string) {
@@ -33,9 +33,9 @@ export default defineEventHandler(async event => {
await RegisterModel.create({ email, password: hashedPassword });
setImmediate(() => {
EmailService.sendConfirmEmail(email, `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}`);
});
// setImmediate(() => {
// EmailService.sendConfirmEmail(email, `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}`);
// });
return {
error: false,

View File

@@ -1,16 +1,15 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestDataOld } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false });
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
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;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
{
$match: {
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, } } },

View File

@@ -1,16 +1,15 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestDataOld } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false });
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
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;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
@@ -19,7 +18,8 @@ export default defineEventHandler(async event => {
{
$match: {
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, } } },

View File

@@ -1,16 +1,15 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestDataOld } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false });
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
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;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
@@ -19,10 +18,11 @@ export default defineEventHandler(async event => {
{
$match: {
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 } },
{ $limit: limit }
]);

View File

@@ -1,37 +0,0 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestDataOld } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(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

@@ -1,5 +1,4 @@
import { getPlanFromId } from "@data/PREMIUM";
import { PREMIUM_PLAN } from "../../../../shared/data/PREMIUM";
import { getPlanFromId, PREMIUM_PLAN } from "@data/PREMIUM";
import { canTryAppsumoCode, checkAppsumoCode, useAppsumoCode, useTryAppsumoCode } from "~/server/services/AppsumoService";
import StripeService from '~/server/services/StripeService';

View File

@@ -4,7 +4,7 @@ import type Event from 'stripe';
import { ProjectModel } from '@schema/project/ProjectSchema';
import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM';
import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
import EmailService from '@services/EmailService'
// import EmailService from '@services/EmailService'
import { UserModel } from '@schema/UserSchema';
@@ -93,9 +93,9 @@ async function onPaymentOnetimeSuccess(event: Event.PaymentIntentSucceededEvent)
const user = await UserModel.findOne({ _id: project.owner });
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
setTimeout(() => {
EmailService.sendPurchaseEmail(user.email, project.name);
}, 1);
// setTimeout(() => {
// EmailService.sendPurchaseEmail(user.email, project.name);
// }, 1);
return { ok: true };
}
@@ -140,10 +140,10 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
const user = await UserModel.findOne({ _id: project.owner });
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
setTimeout(() => {
if (PLAN.ID == 0) return;
if (isNewSubscription) EmailService.sendPurchaseEmail(user.email, project.name);
}, 1);
// setTimeout(() => {
// if (PLAN.ID == 0) return;
// if (isNewSubscription) EmailService.sendPurchaseEmail(user.email, project.name);
// }, 1);
return { ok: true };

View File

@@ -1,22 +1,22 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis } from "~/server/services/CacheService";
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
import { executeTimelineAggregation } from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false, requireSlice: true });
const data = await getRequestData(event, ['SLICE', 'GUEST', 'DOMAIN', 'RANGE', 'OFFSET']);
if (!data) return;
const { pid, from, to, slice, project_id, timeOffset } = data;
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
const cacheKey = `timeline:events:${pid}:${slice}:${from}:${to}`;
const cacheKey = `timeline:events:${pid}:${slice}:${from}:${to}:${domain}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project_id,
model: EventModel,
from, to, slice, timeOffset
from, to, slice, timeOffset, domain, debug: true
});
return timelineData;
});

View File

@@ -1,22 +1,22 @@
import { SessionModel } from "@schema/metrics/SessionSchema";
import { Redis } from "~/server/services/CacheService";
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
import { executeTimelineAggregation } from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false, requireSlice: true });
const data = await getRequestData(event, ['SLICE', 'GUEST', 'DOMAIN', 'RANGE', 'OFFSET']);
if (!data) return;
const { pid, from, to, slice, project_id, timeOffset } = data;
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
const cacheKey = `timeline:sessions:${pid}:${slice}:${from}:${to}`;
const cacheKey = `timeline:sessions:${pid}:${slice}:${from}:${to}:${domain}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project_id,
model: SessionModel,
from, to, slice, timeOffset
from, to, slice, timeOffset, domain
});
return timelineData;
});

View File

@@ -1,22 +1,22 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
import { executeTimelineAggregation } from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false, requireSlice: true });
const data = await getRequestData(event, ['SLICE', 'GUEST', 'DOMAIN', 'RANGE', 'OFFSET']);
if (!data) return;
const { pid, from, to, slice, project_id, timeOffset } = data;
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}`;
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}:${domain}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project_id,
model: VisitModel,
from, to, slice, timeOffset
from, to, slice, timeOffset, domain
});
return timelineData;
});

View File

@@ -1,7 +1,7 @@
import crypto from 'crypto';
import { PasswordModel } from '@schema/PasswordSchema';
import EmailService from '@services/EmailService'
// import EmailService from '@services/EmailService'
export default defineEventHandler(async event => {
@@ -19,7 +19,7 @@ export default defineEventHandler(async event => {
target.password = hashedPassword;
await target.save();
await EmailService.sendResetPasswordEmail(email, newPass);
// await EmailService.sendResetPasswordEmail(email, newPass);
return { error: false, message: 'Password changed' }

View File

@@ -1,6 +1,6 @@
import mongoose from "mongoose";
import { Redis } from "~/server/services/CacheService";
import EmailService from '@services/EmailService';
// import EmailService from '@services/EmailService';
import StripeService from '~/server/services/StripeService';
import { logger } from "./Logger";
@@ -14,10 +14,10 @@ export default async () => {
logger.info('[SERVER] Initializing');
if (config.EMAIL_SERVICE) {
EmailService.init(config.BREVO_API_KEY);
logger.info('[EMAIL] Initialized');
}
// if (config.EMAIL_SERVICE) {
// EmailService.init(config.BREVO_API_KEY);
// logger.info('[EMAIL] Initialized');
// }
if (config.STRIPE_SECRET) {

View File

@@ -11,14 +11,16 @@ export type TimelineAggregationOptions = {
to: string | number,
slice: Slice,
timeOffset?: number,
debug?: boolean
debug?: boolean,
domain?: string
}
export type AdvancedTimelineAggregationOptions = TimelineAggregationOptions & {
customMatch?: Record<string, any>,
customGroup?: Record<string, any>,
customProjection?: Record<string, any>,
customIdGroup?: Record<string, any>
customIdGroup?: Record<string, any>,
customAfterMatch?: Record<string, any>
}
export async function executeAdvancedTimelineAggregation<T = {}>(options: AdvancedTimelineAggregationOptions) {
@@ -36,6 +38,9 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
const timeOffset = options.timeOffset || 0;
const domainMatch: any = {}
if (options.domain) domainMatch.website = options.domain
const aggregation = [
{
$match: {
@@ -44,6 +49,7 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
$gte: new Date(options.from),
$lte: new Date(options.to)
},
...domainMatch,
...options.customMatch
}
},
@@ -94,7 +100,11 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
...options.customProjection
}
}
] as any;
] as any[];
if (options.customAfterMatch) aggregation.splice(1, 0, options.customAfterMatch);
if (options.debug === true) {
console.log('---------- AGGREAGATION ----------')

View File

@@ -1,7 +1,6 @@
import type { AuthContext } from "../middleware/01-authorization";
import type { EventHandlerRequest, H3Event } from 'h3'
import { allowedModels, TModelName } from "../services/DataService";
import { LITLYX_PROJECT_ID } from "@data/LITLYX";
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { Model, Types } from "mongoose";
import { TeamMemberModel } from "@schema/TeamMemberSchema";
@@ -171,7 +170,7 @@ export async function getRequestDataOld(event: H3Event<EventHandlerRequest>, opt
if (!project) return setResponseStatus(event, 400, 'project not found');
if (pid !== LITLYX_PROJECT_ID) {
if (pid !== "6643cd08a1854e3b81722ab5") {
const [hasAccess, role] = await hasAccessToProject(user.id, project);
if (!hasAccess) return setResponseStatus(event, 400, 'no access to project');
if (role === 'GUEST' && !allowGuests) return setResponseStatus(event, 403, 'only owner can access this');

View File

@@ -1,5 +1,3 @@
import { CustomPremiumPriceModel } from "../schema/CustomPremiumPriceSchema";
export type PREMIUM_TAG = typeof PREMIUM_TAGS[number];
export const PREMIUM_TAGS = [
@@ -153,25 +151,6 @@ export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
},
}
try {
CustomPremiumPriceModel.find({}).then(custom_prices => {
for (const custom_price of custom_prices) {
PREMIUM_PLAN[custom_price.tag] = {
ID: custom_price.price_id,
COUNT_LIMIT: custom_price.count_limit,
AI_MESSAGE_LIMIT: custom_price.ai_message_limit,
PRICE: custom_price.price,
PRICE_TEST: custom_price.price_test || ''
}
}
});
} catch (ex) {
}
export function getPlanFromTag(tag: PREMIUM_TAG) {
return PREMIUM_PLAN[tag];
}

View File

@@ -1,4 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -1,77 +1,10 @@
import type { MetricsTimeline } from "~/server/api/metrics/[project_id]/timeline/generic";
// Calcola date snapshot
// 1- Frontend
// 2- Backend
// 3- Data singola
// 4- Aggregazione
// ISO
// UTC UTENTE
// Utility - per date snapshot
// getStartDay: data => 00.00 della data
// getEndDay: data => 23.59 della data
// getStartWeek: data => 00.00 del primo giorno
// getEndWeek: data => 23.59 dell ultimo giorno
// getStartMonth: data => 00.00 del primo giorno del mese
// getEndMonth: data => 23.59 dell ulrimo giorno del mese
// Snapshot -> Current Week -> 11/11-00:00 - 17/11-23:59
// Converti UTC UTENTE -> ISO
// Backend -> Prendi dati da ISO_A a ISO_B
// Funzioni da creare
// Converte utc -> Iso
// UTC TO ISO
// UTC TO ISO Day
// UTC TO ISO Month
// UTC_IS_NEXT_DAY
// True se il giorno passa a quello successivo
// UTC_IS_PREV_DAY
// True se il giorno passa a quello precedente
export const slicesData = {
hour: {
fromOffset: 1000 * 60 * 60 * 24
},
day: {
fromOffset: 1000 * 60 * 60 * 24 * 7
},
month: {
fromOffset: 1000 * 60 * 60 * 24 * 30 * 12
},
year: {
fromOffset: 1000 * 60 * 60 * 24 * 30 * 12 * 10
}
}
export type SliceName = keyof typeof slicesData;
import { type Slice } from '../shared/services/DateService';
export const hoursOffset = -(new Date().getTimezoneOffset() / 60);
function matchDateWithSlice(a: Date, b: Date, slice: SliceName): boolean {
function matchDateWithSlice(a: Date, b: Date, slice: Slice): boolean {
if (a.getFullYear() != b.getFullYear()) return false;
if (a.getMonth() != b.getMonth()) return false;
if (slice === 'month') return true;
@@ -87,7 +20,7 @@ type FixMetricsOptions = {
advancedGroupKey?: string,
timeLabels?: boolean
}
export function fixMetrics(result: { data: MetricsTimeline[], from: string, to: string }, slice: SliceName, _options?: FixMetricsOptions) {
export function fixMetrics(result: { data: MetricsTimeline[], from: string, to: string }, slice: Slice, _options?: FixMetricsOptions) {
const options = {
advanced: false,

View File

@@ -3,15 +3,10 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"scripts": {
"dashboard:clear-logs": "node scripts/dashboard/clear-logs.js"
},
"keywords": [],
"author": "Emily",
"license": "MIT",
"dependencies": {
"@getbrevo/brevo": "^2.2.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"mongoose": "^8.3.2",
"redis": "^4.7.0"
}
}
"license": "MIT"
}

11066
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
packages:
- "dashboard"
- "producer"
- "consumer"
- "security"
- "shared"

View File

@@ -0,0 +1,17 @@
const path = require('path');
const fs = require('fs');
const dashboardPath = path.join(__dirname, '../../dashboard');
const logNames = [
"winston-debug.ndjson",
"winston-exceptions.ndjson",
"winston-logs.ndjson",
"winston-rejections.ndjson",
]
for (const logName of logNames) {
const logFullPath = path.join(dashboardPath, logName);
fs.rmSync(logFullPath);
}

View File

@@ -0,0 +1,63 @@
const path = require('path');
const fs = require('fs');
const dashboardPath = path.join(__dirname, '../../dashboard');
const sharedPath = path.join(__dirname, '../../shared_global');
// TODO: Email service as external repo
// ---------------- Services ----------------
const dashServicesPath = path.join(dashboardPath, 'shared/services');
const sharedServicesPath = path.join(sharedPath, 'services');
if (fs.existsSync(dashServicesPath)) {
fs.rmSync(dashServicesPath, { force: true, recursive: true });
fs.mkdirSync(dashServicesPath);
}
// DateService
const dashDateServicePath = path.join(dashServicesPath, 'DateService.ts');
const sharedDateServicePath = path.join(sharedServicesPath, 'DateService.ts');
fs.cpSync(sharedDateServicePath, dashDateServicePath);
// ---------------- Data ----------------
const dashDataPath = path.join(dashboardPath, 'shared/data');
const sharedDataPath = path.join(sharedPath, 'data');
if (fs.existsSync(dashDataPath)) {
fs.rmSync(dashDataPath, { force: true, recursive: true });
fs.mkdirSync(dashDataPath);
}
// Premium
const dashPremiumDataPath = path.join(dashDataPath, 'PREMIUM.ts');
const sharedPremiumDataPath = path.join(sharedDataPath, 'PREMIUM.ts');
fs.cpSync(sharedPremiumDataPath, dashPremiumDataPath);
// Admins
const dashAdminsDataPath = path.join(dashDataPath, 'ADMINS.ts');
const sharedAdminsDataPath = path.join(sharedDataPath, 'ADMINS.ts');
fs.cpSync(sharedAdminsDataPath, dashAdminsDataPath);
// BrokerLimits
const dashBrokerLimitsDataPath = path.join(dashDataPath, 'broker/Limits.ts');
const sharedBrokerLimitsDataPath = path.join(sharedDataPath, 'broker/Limits.ts');
fs.cpSync(sharedBrokerLimitsDataPath, dashBrokerLimitsDataPath);
// ---------------- Schema ----------------
const dashSchemaPath = path.join(dashboardPath, 'shared/schema');
const sharedSchemaPath = path.join(sharedPath, 'schema');
if (fs.existsSync(dashSchemaPath)) {
fs.rmSync(dashSchemaPath, { force: true, recursive: true });
fs.mkdirSync(dashSchemaPath);
}
fs.cpSync(sharedSchemaPath, dashSchemaPath, { recursive: true });

View File

@@ -1,21 +0,0 @@
import { model, Schema } from 'mongoose';
export type TCustomPremiumPrice = {
tag: string,
price_id: number,
count_limit: number,
ai_message_limit: number,
price: string,
price_test?: string
}
const CustomPremiumPriceSchema = new Schema<TCustomPremiumPrice>({
tag: { type: String, required: true },
price_id: { type: Number, required: true },
count_limit: { type: Number, required: true },
ai_message_limit: { type: Number, required: true },
price: { type: String, required: true },
price_test: { type: String },
})
export const CustomPremiumPriceModel = model<TCustomPremiumPrice>('custom_premium_prices', CustomPremiumPriceSchema);

View File

@@ -0,0 +1,5 @@
export const ADMIN_EMAILS = [
'laura.emily.lovi@gmail.com',
'mangaiomaster@gmail.com',
'helplitlyx@gmail.com'
]

View File

@@ -0,0 +1,175 @@
export type PREMIUM_TAG = typeof PREMIUM_TAGS[number];
export const PREMIUM_TAGS = [
'FREE',
'PLAN_1',
'PLAN_2',
'CUSTOM_1',
'INCUBATION',
'ACCELERATION',
'GROWTH',
'EXPANSION',
'SCALING',
'UNICORN',
'LIFETIME_GROWTH_ONETIME',
'GROWTH_DUMMY',
'APPSUMO_INCUBATION',
'APPSUMO_ACCELERATION',
'APPSUMO_GROWTH',
] as const;
export type PREMIUM_DATA = {
COUNT_LIMIT: number,
AI_MESSAGE_LIMIT: number,
PRICE: string,
PRICE_TEST: string,
ID: number,
COST: number
}
export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
FREE: {
ID: 0,
COUNT_LIMIT: 5_000,
AI_MESSAGE_LIMIT: 10,
PRICE: 'price_1POKCMB2lPUiVs9VLe3QjIHl',
PRICE_TEST: 'price_1PNbHYB2lPUiVs9VZP32xglF',
COST: 0
},
PLAN_1: {
ID: 1,
COUNT_LIMIT: 150_000,
AI_MESSAGE_LIMIT: 100,
PRICE: 'price_1POKCOB2lPUiVs9VC13s2rQw',
PRICE_TEST: 'price_1PNZjVB2lPUiVs9VrsTbJL04',
COST: 0
},
PLAN_2: {
ID: 2,
COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 5_000,
PRICE: 'price_1POKCKB2lPUiVs9Vol8XOmhW',
PRICE_TEST: 'price_1POK34B2lPUiVs9VIROb0IIV',
COST: 0
},
CUSTOM_1: {
ID: 1001,
COUNT_LIMIT: 10_000_000,
AI_MESSAGE_LIMIT: 100_000,
PRICE: 'price_1POKZyB2lPUiVs9VMAY6jXTV',
PRICE_TEST: '',
COST: 0
},
INCUBATION: {
ID: 101,
COUNT_LIMIT: 50_000,
AI_MESSAGE_LIMIT: 30,
PRICE: 'price_1PdsyzB2lPUiVs9V4J246Jw0',
PRICE_TEST: '',
COST: 499
},
ACCELERATION: {
ID: 102,
COUNT_LIMIT: 150_000,
AI_MESSAGE_LIMIT: 100,
PRICE: 'price_1Pdt5bB2lPUiVs9VhkuCouEt',
PRICE_TEST: '',
COST: 999
},
GROWTH: {
ID: 103,
COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 3_000,
PRICE: 'price_1PdszrB2lPUiVs9VIdkT3thv',
PRICE_TEST: '',
COST: 2999
},
EXPANSION: {
ID: 104,
COUNT_LIMIT: 1_000_000,
AI_MESSAGE_LIMIT: 5_000,
PRICE: 'price_1Pdt0xB2lPUiVs9V0Rdt80Fe',
PRICE_TEST: '',
COST: 5999
},
SCALING: {
ID: 105,
COUNT_LIMIT: 2_500_000,
AI_MESSAGE_LIMIT: 10_000,
PRICE: 'price_1Pdt1UB2lPUiVs9VUmxntSwZ',
PRICE_TEST: '',
COST: 9999
},
UNICORN: {
ID: 106,
COUNT_LIMIT: 5_000_000,
AI_MESSAGE_LIMIT: 20_000,
PRICE: 'price_1Pdt2LB2lPUiVs9VGBFAIG9G',
PRICE_TEST: '',
COST: 14999
},
LIFETIME_GROWTH_ONETIME: {
ID: 2001,
COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 3_000,
PRICE: 'price_1PvewGB2lPUiVs9VLheJC8s1',
PRICE_TEST: 'price_1Pvf7LB2lPUiVs9VMFNyzpim',
COST: 239900
},
GROWTH_DUMMY: {
ID: 5001,
COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 3_000,
PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J',
PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G',
COST: 0
},
APPSUMO_INCUBATION: {
ID: 6001,
COUNT_LIMIT: 50_000,
AI_MESSAGE_LIMIT: 30,
PRICE: 'price_1QIXwbB2lPUiVs9VKSsoksaU',
PRICE_TEST: '',
COST: 0
},
APPSUMO_ACCELERATION: {
ID: 6002,
COUNT_LIMIT: 150_000,
AI_MESSAGE_LIMIT: 100,
PRICE: 'price_1QIXxRB2lPUiVs9VrjaVRoOl',
PRICE_TEST: '',
COST: 0
},
APPSUMO_GROWTH: {
ID: 6003,
COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 3_000,
PRICE: 'price_1QIXy8B2lPUiVs9VQBOUPAoE',
PRICE_TEST: '',
COST: 0
},
}
export function getPlanFromTag(tag: PREMIUM_TAG) {
return PREMIUM_PLAN[tag];
}
export function getPlanFromId(id: number) {
for (const tag of PREMIUM_TAGS) {
const plan = getPlanFromTag(tag);
if (plan.ID === id) return plan;
}
}
export function getPlanFromPrice(price: string, testMode: boolean) {
for (const tag of PREMIUM_TAGS) {
const plan = getPlanFromTag(tag);
if (testMode) {
if (plan.PRICE_TEST === price) return plan;
} else {
if (plan.PRICE === price) return plan;
}
}
}

View File

@@ -0,0 +1,5 @@
// Default: 1.01
// ((events + visits) * VALUE) > limit
export const MAX_LOG_LIMIT_PERCENT = 1.01;

View File

@@ -0,0 +1,20 @@
import { model, Schema, Types } from 'mongoose';
export type TApiSettings = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
apiKey: string,
apiName: string,
usage: number,
created_at: Date
}
const ApiSettingsSchema = new Schema<TApiSettings>({
project_id: { type: Types.ObjectId, index: 1 },
apiKey: { type: String, required: true },
apiName: { type: String, required: true },
usage: { type: Number, default: 0, required: true, },
created_at: { type: Date, default: () => Date.now() },
});
export const ApiSettingsModel = model<TApiSettings>('api_settings', ApiSettingsSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TFeedback = {
user_id: Types.ObjectId,
project_id: Types.ObjectId,
text: string
}
const FeedbackSchema = new Schema<TFeedback>({
user_id: { type: Schema.Types.ObjectId, required: true },
project_id: { type: Schema.Types.ObjectId, required: true },
text: { type: String, required: true },
});
export const FeedbackModel = model<TFeedback>('feedbacks', FeedbackSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TOnboarding = {
user_id: Types.ObjectId,
analytics: string,
job: string
}
const OnboardingSchema = new Schema<TOnboarding>({
user_id: { type: Schema.Types.ObjectId, required: true },
analytics: { type: String, required: false },
job: { type: String, required: false },
});
export const OnboardingModel = model<TOnboarding>('onboardings', OnboardingSchema);

View File

@@ -0,0 +1,14 @@
import { model, Schema, Types } from 'mongoose';
export type TPassword = {
email: string,
password: string,
}
const PasswordSchema = new Schema<TPassword>({
email: { type: String, index: true, unique: true },
password: { type: String },
});
export const PasswordModel = model<TPassword>('passwords', PasswordSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TRegister = {
email: string,
password: string,
created_at: Date
}
const RegisterSchema = new Schema<TRegister>({
email: { type: String },
password: { type: String },
created_at: { type: Date, default: () => Date.now() }
});
export const RegisterModel = model<TRegister>('registers', RegisterSchema);

View File

@@ -0,0 +1,22 @@
import { model, Schema, Types } from 'mongoose';
export type TeamMemberRole = 'ADMIN' | 'GUEST';
export type TTeamMember = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
user_id: Schema.Types.ObjectId,
role: TeamMemberRole,
pending: boolean,
created_at: Date,
}
const TeamMemberSchema = new Schema<TTeamMember>({
project_id: { type: Types.ObjectId, index: true },
user_id: { type: Types.ObjectId, index: true },
role: { type: String, required: true },
pending: { type: Boolean, required: true },
created_at: { type: Date, required: true, default: () => Date.now() },
});
export const TeamMemberModel = model<TTeamMember>('team_members', TeamMemberSchema);

View File

@@ -0,0 +1,38 @@
import { model, Schema, Types } from 'mongoose';
export type TUser = {
email: string,
name: string,
given_name: string,
locale: string,
picture: string,
created_at: Date,
google_tokens?: {
refresh_token?: string;
expiry_date?: number;
access_token?: string;
token_type?: string;
id_token?: string;
scope?: string;
}
}
const UserSchema = new Schema<TUser>({
email: { type: String, unique: true, index: 1 },
name: String,
given_name: String,
locale: String,
picture: String,
google_tokens: {
refresh_token: String,
expiry_date: Number,
access_token: String,
token_type: String,
id_token: String,
scope: String
},
created_at: { type: Date, default: () => Date.now() }
})
export const UserModel = model<TUser>('users', UserSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TUserSettings = {
user_id: Schema.Types.ObjectId,
max_projects: number,
active_project_id: Schema.Types.ObjectId
}
const UserSettingsSchema = new Schema<TUserSettings>({
user_id: { type: Types.ObjectId, unique: true, index: 1 },
max_projects: { type: Number, default: 3 },
active_project_id: Schema.Types.ObjectId,
});
export const UserSettingsModel = model<TUserSettings>('user_settings', UserSettingsSchema);

View File

@@ -0,0 +1,26 @@
import { model, Schema } from 'mongoose';
export type TAiChatSchema = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
messages: any[],
status: string,
completed: boolean,
title: string,
deleted: boolean,
created_at: Date,
updated_at: Date
}
const AiChatSchema = new Schema<TAiChatSchema>({
project_id: { type: Schema.Types.ObjectId, index: 1 },
status: { type: String },
completed: { type: Boolean },
messages: [{ _id: false, type: Schema.Types.Mixed }],
title: { type: String, required: true },
deleted: { type: Boolean, default: false },
created_at: { type: Date, default: () => Date.now() },
updated_at: { type: Date, default: () => Date.now() },
});
export const AiChatModel = model<TAiChatSchema>('ai_chats', AiChatSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TAnomalyDomain = {
project_id: Schema.Types.ObjectId
domain: string,
created_at: Date
}
const AnomalyDomainSchema = new Schema<TAnomalyDomain>({
project_id: { type: Types.ObjectId, required: true },
domain: { type: String, required: true },
created_at: { type: Date, required: true },
})
export const AnomalyDomainModel = model<TAnomalyDomain>('anomaly_domains', AnomalyDomainSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TAnomalyEvents = {
project_id: Schema.Types.ObjectId
eventDate: Date,
created_at: Date
}
const AnomalyEventsSchema = new Schema<TAnomalyEvents>({
project_id: { type: Types.ObjectId, required: true },
eventDate: { type: Date, required: true },
created_at: { type: Date, required: true },
})
export const AnomalyEventsModel = model<TAnomalyEvents>('anomaly_events', AnomalyEventsSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TAnomalyVisit = {
project_id: Schema.Types.ObjectId
visitDate: Date,
created_at: Date
}
const AnomalyVisitSchema = new Schema<TAnomalyVisit>({
project_id: { type: Types.ObjectId, required: true },
visitDate: { type: Date, required: true },
created_at: { type: Date, required: true },
})
export const AnomalyVisitModel = model<TAnomalyVisit>('anomaly_visits', AnomalyVisitSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TAppsumoCode = {
_id: Schema.Types.ObjectId,
code: string,
used_at: Date,
created_at?: Date,
}
const AppsumoCodeSchema = new Schema<TAppsumoCode>({
code: { type: String, index: 1 },
created_at: { type: Date, default: () => Date.now() },
used_at: { type: Date, required: false },
});
export const AppsumoCodeModel = model<TAppsumoCode>('appsumo_codes', AppsumoCodeSchema);

View File

@@ -0,0 +1,15 @@
import { model, Schema, Types } from 'mongoose';
export type TAppsumoCodeTry = {
project_id: Types.ObjectId,
codes: string[],
valid_codes: string[],
}
const AppsumoCodeTrySchema = new Schema<TAppsumoCodeTry>({
project_id: { type: Schema.Types.ObjectId, required: true, unique: true, index: 1 },
codes: [{ type: String }],
valid_codes: [{ type: String }]
});
export const AppsumoCodeTryModel = model<TAppsumoCodeTry>('appsumo_codes_tries', AppsumoCodeTrySchema);

View File

@@ -0,0 +1,18 @@
import { model, Schema, Types } from 'mongoose';
export type TLimitNotify = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
limit1: boolean,
limit2: boolean,
limit3: boolean
}
const LimitNotifySchema = new Schema<TLimitNotify>({
project_id: { type: Types.ObjectId, index: 1 },
limit1: { type: Boolean },
limit2: { type: Boolean },
limit3: { type: Boolean }
});
export const LimitNotifyModel = model<TLimitNotify>('limit_notifies', LimitNotifySchema);

View File

@@ -0,0 +1,22 @@
import { model, Schema, Types } from 'mongoose';
export type TEvent = {
project_id: Schema.Types.ObjectId,
name: string,
metadata: Record<string, string>,
session: string,
flowHash: string,
created_at: Date
}
const EventSchema = new Schema<TEvent>({
project_id: { type: Types.ObjectId, index: 1 },
name: { type: String, required: true, index: 1 },
metadata: Schema.Types.Mixed,
session: { type: String, index: 1 },
flowHash: { type: String },
created_at: { type: Date, default: () => Date.now(), index: true },
})
export const EventModel = model<TEvent>('events', EventSchema);

View File

@@ -0,0 +1,23 @@
import { model, Schema, Types } from 'mongoose';
export type TSession = {
project_id: Schema.Types.ObjectId,
session: string,
flowHash: string,
duration: number,
updated_at: Date,
created_at: Date,
}
const SessionSchema = new Schema<TSession>({
project_id: { type: Types.ObjectId, index: 1 },
session: { type: String, required: true, index: 1 },
flowHash: { type: String },
duration: { type: Number, required: true, default: 0 },
updated_at: { type: Date, default: () => Date.now() },
created_at: { type: Date, default: () => Date.now(), index: true },
})
export const SessionModel = model<TSession>('sessions', SessionSchema);

View File

@@ -0,0 +1,45 @@
import { model, Schema } from 'mongoose';
export type TVisit = {
project_id: Schema.Types.ObjectId,
browser: string,
os: string,
continent: string,
country: string,
session: string,
flowHash: string,
device: string,
website: string,
page: string,
referrer: string,
created_at: Date
}
const VisitSchema = new Schema<TVisit>({
project_id: { type: Schema.Types.ObjectId, index: true },
browser: { type: String, required: true },
os: { type: String, required: true },
continent: { type: String },
country: { type: String },
session: { type: String },
flowHash: { type: String },
device: { type: String },
website: { type: String, required: true, index: true },
page: { type: String, required: true },
referrer: { type: String, required: true },
created_at: { type: Date, default: () => Date.now() },
})
VisitSchema.index({ project_id: 1, created_at: -1 });
export const VisitModel = model<TVisit>('visits', VisitSchema);

View File

@@ -0,0 +1,26 @@
import { model, Schema, Types } from 'mongoose';
export type TProject = {
_id: Schema.Types.ObjectId,
owner: Schema.Types.ObjectId,
name: string,
premium: boolean,
premium_type: number,
customer_id: string,
subscription_id: string,
premium_expire_at: Date,
created_at: Date
}
const ProjectSchema = new Schema<TProject>({
owner: { type: Types.ObjectId, index: 1 },
name: { type: String, required: true },
premium: { type: Boolean, default: false },
premium_type: { type: Number, default: 0 },
customer_id: { type: String, required: true },
subscription_id: { type: String, required: true },
premium_expire_at: { type: Date, required: true },
created_at: { type: Date, default: () => Date.now() },
})
export const ProjectModel = model<TProject>('projects', ProjectSchema);

View File

@@ -0,0 +1,20 @@
import { model, Schema, Types } from 'mongoose';
export type TProjectSnapshot = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
name: string,
from: Date,
to: Date,
color: string
}
const ProjectSnapshotSchema = new Schema<TProjectSnapshot>({
project_id: { type: Types.ObjectId, index: true },
name: { type: String, required: true },
from: { type: Date, required: true },
to: { type: Date, required: true },
color: { type: String, required: true },
});
export const ProjectSnapshotModel = model<TProjectSnapshot>('project_snapshots', ProjectSnapshotSchema);

View File

@@ -0,0 +1,22 @@
import { model, Schema, Types } from 'mongoose';
export type TProjectCount = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
events: number,
visits: number,
sessions: number,
lastRecheck?: Date,
updated_at: Date
}
const ProjectCountSchema = new Schema<TProjectCount>({
project_id: { type: Types.ObjectId, index: true, unique: true },
events: { type: Number, required: true, default: 0 },
visits: { type: Number, required: true, default: 0 },
sessions: { type: Number, required: true, default: 0 },
lastRecheck: { type: Date },
updated_at: { type: Date }
}, { timestamps: { updatedAt: 'updated_at' } });
export const ProjectCountModel = model<TProjectCount>('project_counts', ProjectCountSchema);

View File

@@ -0,0 +1,26 @@
import { model, Schema, Types } from 'mongoose';
export type TProjectLimit = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
events: number,
visits: number,
ai_messages: number,
limit: number,
ai_limit: number,
billing_expire_at: Date,
billing_start_at: Date,
}
const ProjectLimitSchema = new Schema<TProjectLimit>({
project_id: { type: Types.ObjectId, index: true, unique: true },
events: { type: Number, required: true, default: 0 },
visits: { type: Number, required: true, default: 0 },
ai_messages: { type: Number, required: true, default: 0 },
limit: { type: Number, required: true },
ai_limit: { type: Number, required: true },
billing_start_at: { type: Date, required: true },
billing_expire_at: { type: Date, required: true },
});
export const ProjectLimitModel = model<TProjectLimit>('project_limits', ProjectLimitSchema);

View File

@@ -0,0 +1,224 @@
import dayjs from 'dayjs';
import * as fns from 'date-fns';
export type Slice = keyof typeof slicesData;
const slicesData = {
hour: {},
day: {},
week: {},
month: {},
year: {}
}
const startOfFunctions: { [key in Slice]: (date: Date) => Date } = {
hour: fns.startOfHour,
day: fns.startOfDay,
week: fns.startOfWeek,
month: fns.startOfMonth,
year: fns.startOfYear
};
const endOfFunctions: { [key in Slice]: (date: Date) => Date } = {
hour: fns.endOfHour,
day: fns.endOfDay,
week: fns.endOfWeek,
month: fns.endOfMonth,
year: fns.endOfYear
};
class DateService {
public slicesData = slicesData;
getChartLabelFromISO(iso: string, offset: number, slice: Slice) {
const date = new Date(new Date(iso).getTime() + offset * 1000 * 60);
if (slice === 'hour') return fns.format(date, 'HH:mm');
if (slice === 'day') return fns.format(date, 'dd/MM');
if (slice === 'week') return fns.format(date, 'dd/MM');
if (slice === 'month') return fns.format(date, 'MMMM');
if (slice === 'year') return fns.format(date, 'YYYY');
return iso;
}
canUseSlice(from: string | number | Date, to: string | number | Date, slice: Slice) {
const daysDiff = fns.differenceInDays(
new Date(new Date(to).getTime() + 1000),
new Date(from)
);
return this.canUseSliceFromDays(daysDiff, slice);
}
canUseSliceFromDays(days: number, slice: Slice): [false, string] | [true, number] {
// 3 Days
if (slice === 'hour' && (days > 3)) return [false, 'Date gap too big for this slice'];
// 3 Weeks
if (slice === 'day' && (days > 31)) return [false, 'Date gap too big for this slice'];
// 3 Years
if (slice === 'month' && (days > 365 * 3)) return [false, 'Date gap too big for this slice'];
// 2 days
if (slice === 'day' && (days < 2)) return [false, 'Date gap too small for this slice'];
// 2 month
if (slice === 'month' && (days < 31 * 2)) return [false, 'Date gap too small for this slice'];
return [true, days]
}
startOfSlice(date: Date, slice: Slice) {
const fn = startOfFunctions[slice];
if (!fn) throw Error(`startOfFunction of slice ${slice} not found`);
return fn(date);
}
endOfSlice(date: Date, slice: Slice) {
const fn = endOfFunctions[slice];
if (!fn) throw Error(`endOfFunction of slice ${slice} not found`);
return fn(date);
}
getGranularityData(slice: Slice, dateField: string) {
const dateFromParts: Record<string, any> = {};
let granularity;
switch (slice) {
case 'hour':
dateFromParts.hour = { $hour: { date: dateField } }
granularity = granularity || 'hour';
case 'day':
dateFromParts.day = { $dayOfMonth: { date: dateField } }
granularity = granularity || 'day';
case 'month':
dateFromParts.month = { $month: { date: dateField } }
granularity = granularity || 'month';
case 'year':
dateFromParts.year = { $year: { date: dateField } }
granularity = granularity || 'year';
}
return { dateFromParts, granularity }
}
/**
* @deprecated interal to generateDateSlices
*/
prepareDateRange(from: string, to: string, slice: Slice) {
let fromDate = dayjs(from).minute(0).second(0).millisecond(0);
let toDate = dayjs(to).minute(0).second(0).millisecond(0);
switch (slice) {
case 'day':
fromDate = fromDate.hour(0);
toDate = toDate.hour(0);
break;
case 'hour':
break;
}
return {
from: fromDate.toDate(),
to: toDate.toDate()
}
}
/**
* @deprecated interal to generateDateSlices
*/
createBetweenDates(from: string, to: string, slice: Slice) {
let start = dayjs(from);
const end = dayjs(to);
const filledDates: dayjs.Dayjs[] = [];
while (start.isBefore(end) || start.isSame(end)) {
filledDates.push(start);
start = start.add(1, slice);
}
return { dates: filledDates, from, to };
}
/**
* @deprecated use generateDateSlices
*/
fillDates(dates: string[], slice: Slice) {
const allDates: dayjs.Dayjs[] = [];
const firstDate = dayjs(dates.at(0));
const lastDate = dayjs(dates.at(-1));
let currentDate = firstDate.clone();
allDates.push(currentDate);
while (currentDate.isBefore(lastDate, slice)) {
currentDate = currentDate.add(1, slice);
allDates.push(currentDate);
}
return allDates;
}
/**
* @deprecated use mergeDates
*/
mergeFilledDates<T extends Record<string, any>, K extends keyof T>(dates: dayjs.Dayjs[], items: T[], dateField: K, slice: Slice, fillData: Omit<T, K>) {
const result = new Array<T>();
for (const date of dates) {
const item = items.find(e => dayjs(e[dateField]).isSame(date, slice));
result.push(item ?? { ...fillData, [dateField]: date.format() } as T);
}
return result;
}
generateDateSlices(slice: Slice, fromDate: Date, toDate: Date) {
const slices: Date[] = [];
let currentDate = fromDate;
const addFunctions: { [key in Slice]: any } = { hour: fns.addHours, day: fns.addDays, week: fns.addWeeks, month: fns.addMonths, year: fns.addYears };
const addFunction = addFunctions[slice];
if (!addFunction) { throw new Error(`Invalid slice: ${slice}`); }
while (fns.isBefore(currentDate, toDate) || currentDate.getTime() === toDate.getTime()) {
slices.push(currentDate);
currentDate = addFunction(currentDate, 1);
}
return slices;
}
isSameDayUTC(a: Date, b: Date) {
return a.getUTCFullYear() === b.getUTCFullYear() && a.getUTCMonth() === b.getUTCMonth() && a.getUTCDate() === b.getUTCDate();
}
mergeDates(timeline: { _id: string, count: number }[], allDates: Date[], slice: Slice) {
const result: { _id: string, count: number }[] = [];
const isSames: { [key in Slice]: any } = { hour: fns.isSameHour, day: this.isSameDayUTC, week: fns.isSameWeek, month: fns.isSameMonth, year: fns.isSameYear, }
const isSame = isSames[slice];
if (!isSame) {
throw new Error(`Invalid slice: ${slice}`);
}
for (const date of allDates) {
result.push({ _id: date.toISOString(), count: 0 });
for (const element of timeline) {
const elementDate = new Date(element._id);
if (isSame(elementDate, date)) {
const existingEntry = result.find(item => isSame(date, new Date(item._id)));
if (!existingEntry) throw new Error('THIS CANNOT HAPPEN');
existingEntry.count += element.count;
}
}
}
return result;
}
}
const dateServiceInstance = new DateService();
export default dateServiceInstance;

Some files were not shown because too many files have changed in this diff Show More