23 Commits

Author SHA1 Message Date
Emily
1917b74c32 fix admin panel + payment ok page 2025-04-28 18:52:38 +02:00
Emily
b709ad285a fix deployment of producer 2025-04-25 22:46:33 +02:00
Emily
6a157b81c9 fix 2025-04-25 19:20:13 +02:00
Emily
82b0f6aac4 change reports + chage pricing 2025-04-25 18:36:00 +02:00
Emily
407c84f59c fix premium banner 2025-04-25 15:49:57 +02:00
Emily
e8f8600df4 Mode AI tokens 2025-04-25 15:49:27 +02:00
Emily
eb954cac6c update 2025-04-24 17:36:23 +02:00
Emily
a9bbc58ad1 fix payment service + appsumo + ui 2025-04-22 18:42:18 +02:00
Emily
f631c29fb2 fix bugs 2025-04-16 17:13:19 +02:00
Emily
946f9d4d32 fix dashboard + payments 2025-04-13 18:15:43 +02:00
Emily
1d5dad44fa add titles 2025-04-11 18:16:20 +02:00
Emily
1187cafd07 update email templates 2025-04-11 18:16:14 +02:00
Emily
475512711b change wordpress first interaction 2025-04-09 17:40:01 +02:00
Emily
8f7f89e0bd update UI 2025-04-09 17:35:40 +02:00
Emily
4d51676a2e update free prices 2025-04-07 18:47:50 +02:00
Emily
72ceb7971d update pricing data 2025-04-05 16:42:52 +02:00
Emily
10d4a9f1bc fix dashboard premium tables 2025-04-05 16:32:38 +02:00
Emily
70c15238a0 update payment system 2025-04-01 19:46:07 +02:00
Emily
7e093251fa implementing new payment system + rewrite deploy scripts 2025-03-28 16:57:57 +01:00
Emily
7658dbe85c fix members 2025-03-26 16:15:46 +01:00
Emily
1f9ef5d18c add payment service 2025-03-26 15:30:22 +01:00
Emily
94a28b31d3 update shields 2025-03-24 18:54:15 +01:00
Emily
87c9aca5c4 shields update 2025-03-20 16:04:00 +01:00
160 changed files with 5107 additions and 1417 deletions

8
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"github.copilot.enable": {
"*": false,
"plaintext": false,
"markdown": true,
"scminput": false
}
}

View File

@@ -2,77 +2,56 @@ import { ProjectModel } from "./shared/schema/project/ProjectSchema";
import { UserModel } from "./shared/schema/UserSchema";
import { LimitNotifyModel } from "./shared/schema/broker/LimitNotifySchema";
import { EmailService } from './shared/services/EmailService';
import { TProjectLimit } from "./shared/schema/project/ProjectsLimits";
import { EmailServiceHelper } from "./EmailServiceHelper";
import { TUserLimit } from "./shared/schema/UserLimitSchema";
export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
export async function checkLimitsForEmail(projectCounts: TUserLimit) {
const project_id = projectCounts.project_id;
const hasNotifyEntry = await LimitNotifyModel.findOne({ project_id });
const user_id = projectCounts.user_id;
const hasNotifyEntry = await LimitNotifyModel.findOne({ user_id });
if (!hasNotifyEntry) {
await LimitNotifyModel.create({ project_id, limit1: false, limit2: false, limit3: false })
await LimitNotifyModel.create({ user_id, limit1: false, limit2: false, limit3: false })
}
const owner = await UserModel.findById(user_id);
if (!owner) return;
const userName = owner.given_name || owner.name || 'no_name';
if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit)) {
if (hasNotifyEntry.limit3 === true) return;
const project = await ProjectModel.findById(project_id);
if (!project) return;
const owner = await UserModel.findById(project.owner);
if (!owner) return;
setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('limit_max', {
target: owner.email,
projectName: project.name
});
const emailData = EmailService.getEmailServerInfo('limit_max', { target: owner.email });
EmailServiceHelper.sendEmail(emailData);
});
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: true });
await LimitNotifyModel.updateOne({ user_id }, { limit1: true, limit2: true, limit3: true });
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.9)) {
if (hasNotifyEntry.limit2 === true) return;
const project = await ProjectModel.findById(project_id);
if (!project) return;
const owner = await UserModel.findById(project.owner);
if (!owner) return;
setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('limit_90', {
target: owner.email,
projectName: project.name
});
const emailData = EmailService.getEmailServerInfo('limit_90', { target: owner.email });
EmailServiceHelper.sendEmail(emailData);
});
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: false });
await LimitNotifyModel.updateOne({ user_id }, { limit1: true, limit2: true, limit3: false });
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.5)) {
if (hasNotifyEntry.limit1 === true) return;
const project = await ProjectModel.findById(project_id);
if (!project) return;
const owner = await UserModel.findById(project.owner);
if (!owner) return;
setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('limit_50', {
target: owner.email,
projectName: project.name
});
const emailData = EmailService.getEmailServerInfo('limit_50', { target: owner.email });
EmailServiceHelper.sendEmail(emailData);
});
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: false, limit3: false });
await LimitNotifyModel.updateOne({ user_id }, { limit1: true, limit2: false, limit3: false });
}

View File

@@ -1,15 +1,15 @@
import { ProjectLimitModel } from './shared/schema/project/ProjectsLimits';
import { MAX_LOG_LIMIT_PERCENT } from './shared/data/broker/Limits';
import { checkLimitsForEmail } from './EmailController';
import { UserLimitModel } from './shared/schema/UserLimitSchema';
export async function checkLimits(project_id: string) {
const projectLimits = await ProjectLimitModel.findOne({ project_id });
if (!projectLimits) return false;
const TOTAL_COUNT = projectLimits.events + projectLimits.visits;
const COUNT_LIMIT = projectLimits.limit;
export async function checkLimits(user_id: string) {
const userLimits = await UserLimitModel.findOne({ user_id });
if (!userLimits) return false;
const TOTAL_COUNT = userLimits.events + userLimits.visits;
const COUNT_LIMIT = userLimits.limit;
if ((TOTAL_COUNT) > COUNT_LIMIT * MAX_LOG_LIMIT_PERCENT) return false;
await checkLimitsForEmail(projectLimits);
await checkLimitsForEmail(userLimits);
return true;
}

View File

@@ -11,10 +11,9 @@ import { UAParser } from 'ua-parser-js';
import { checkLimits } from './LimitChecker';
import express from 'express';
import { ProjectLimitModel } from './shared/schema/project/ProjectsLimits';
import { ProjectCountModel } from './shared/schema/project/ProjectsCounts';
import { metricsRouter } from './Metrics';
import { UserLimitModel } from './shared/schema/UserLimitSchema';
const app = express();
@@ -27,6 +26,12 @@ main();
const CONSUMER_NAME = `CONSUMER_${process.env.NODE_APP_INSTANCE || 'DEFAULT'}`
async function getProjectOwner(pid: string) {
const ownerData = await ProjectModel.findOne({ _id: pid }, { owner: 1 });
return ownerData.owner;
}
async function main() {
await RedisStreamService.connect();
@@ -51,18 +56,18 @@ async function processStreamEntry(data: Record<string, string>) {
const { pid, sessionHash } = data;
const project = await ProjectModel.exists({ _id: pid });
if (!project) return;
const owner = await getProjectOwner(pid);
if (!owner) return;
const canLog = await checkLimits(pid);
const canLog = await checkLimits(owner.toString());
if (!canLog) return;
if (eventType === 'event') {
await process_event(data, sessionHash);
await process_event(data, sessionHash, owner.toString());
} else if (eventType === 'keep_alive') {
await process_keep_alive(data, sessionHash);
await process_keep_alive(data, sessionHash, owner.toString());
} else if (eventType === 'visit') {
await process_visit(data, sessionHash);
await process_visit(data, sessionHash, owner.toString());
}
} catch (ex: any) {
@@ -75,7 +80,7 @@ async function processStreamEntry(data: Record<string, string>) {
}
async function process_visit(data: Record<string, string>, sessionHash: string) {
async function process_visit(data: Record<string, string>, sessionHash: string, user_id: string) {
const { pid, ip, website, page, referrer, userAgent, flowHash, timestamp } = data;
@@ -105,12 +110,12 @@ async function process_visit(data: Record<string, string>, sessionHash: string)
created_at: new Date(parseInt(timestamp))
}),
ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'visits': 1 } }, { upsert: true }),
ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { 'visits': 1 } })
UserLimitModel.updateOne({ user_id }, { $inc: { 'visits': 1 } })
]);
}
async function process_keep_alive(data: Record<string, string>, sessionHash: string) {
async function process_keep_alive(data: Record<string, string>, sessionHash: string, user_id: string) {
const { pid, instant, flowHash, timestamp, website } = data;
@@ -137,7 +142,7 @@ async function process_keep_alive(data: Record<string, string>, sessionHash: str
}
async function process_event(data: Record<string, string>, sessionHash: string) {
async function process_event(data: Record<string, string>, sessionHash: string, user_id: string) {
const { name, metadata, pid, flowHash, timestamp, website } = data;
@@ -155,7 +160,7 @@ async function process_event(data: Record<string, string>, sessionHash: string)
created_at: new Date(parseInt(timestamp))
}),
ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'events': 1 } }, { upsert: true }),
ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { 'events': 1 } })
UserLimitModel.updateOne({ user_id }, { $inc: { 'events': 1 } })
]);

9
dashboard/app.config.ts Normal file
View File

@@ -0,0 +1,9 @@
export default defineAppConfig({
ui: {
notifications: {
position: 'top-0 bottom-[unset]'
}
}
})

View File

@@ -69,6 +69,7 @@ const { drawerVisible, hideDrawer, drawerClasses } = useDrawer();
<UModals />
<UNotifications />
<LazyOnboarding> </LazyOnboarding>

View File

@@ -111,3 +111,11 @@ body {
* {
font-family: 'Nunito', var(--font-sans);
}
.rotating-thing {
height: 100%;
aspect-ratio: 1 / 1;
opacity: 0.15;
background: radial-gradient(51.24% 31.29% at 50% 50%, rgb(51, 58, 232) 0%, rgba(51, 58, 232, 0) 100%);
animation: 12s linear 0s infinite normal none running spin;
}

View File

@@ -6,6 +6,7 @@ registerChartComponents();
const props = defineProps<{
datasets: any[],
labels: string[],
legendPosition?: "left" | "top" | "right" | "bottom" | "center" | "chartArea"
}>();
const chartOptions = ref<ChartOptions<'bar'>>({
@@ -40,7 +41,7 @@ const chartOptions = ref<ChartOptions<'bar'>>({
plugins: {
legend: {
display: true,
position: 'right',
position: props.legendPosition ?? 'right',
},
title: { display: false },
tooltip: {

View File

@@ -54,7 +54,7 @@ onMounted(() => {
<div class="flex overflow-x-auto hide-scrollbars">
<div class="flex">
<div v-for="(tab, index) of items" @click="onChangeTab(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"
class="px-6 whitespace-nowrap pb-3 poppins font-medium text-lyx-lightmode-text dark:text-lyx-text-darker border-b-[1px] border-lyx-text-darker"
:class="{
'dark:!border-[#FFFFFF] dark:!text-[#FFFFFF] !border-lyx-primary !text-lyx-primary': activeTabIndex === index,
'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

View File

@@ -84,23 +84,8 @@ function reloadPage() {
<div class="flex gap-6 xl:flex-row flex-col">
<div class="h-full w-full">
<CardTitled class="h-full w-full xl:min-w-[400px] xl:h-[35rem]" title="Quick setup tutorial"
sub="Quickly Set Up Litlyx in 30 Seconds!">
<div class="flex items-center justify-center h-full w-full">
<iframe class="w-full h-full min-h-[400px]"
src="https://www.youtube.com/embed/LInFoNLJ-CI?si=a97HVXpXFDgFg2Yp" title="Litlyx"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
</CardTitled>
</div>
<div class="flex flex-col gap-6">
<div class="flex gap-4">
<div class="w-full">
<CardTitled title="Quick Integration"
@@ -133,28 +118,6 @@ function reloadPage() {
</div>
</div>
<div>
<div>
<CardTitled class="w-full h-full" title="Wordpress + Elementor"
sub="Our WordPress plugin is coming soon!.">
<template #header>
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
to="https://docs.litlyx.com">
Visit documentation
</LyxUiButton>
</template>
<div class="flex flex-col items-end">
<div class="justify-center w-full hidden xl:flex gap-3">
<a href="#">
<img class="cursor-pointer" :src="'tech-icons/wpel.png'" alt="Litlyx-Wordpress-Elementor">
</a>
</div>
</div>
</CardTitled>
</div>
</div>
<div>
<div>
<CardTitled class="w-full h-full" title="Modules"
@@ -169,28 +132,36 @@ function reloadPage() {
<div class="flex flex-col items-end">
<div class="justify-center w-full hidden xl:flex gap-3">
<a href="https://docs.litlyx.com/techs/js" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/js.png'" alt="Litlyx-Javascript-Analytics">
<img class="cursor-pointer" :src="'tech-icons/js.png'"
alt="Litlyx-Javascript-Analytics">
</a>
<a href="https://docs.litlyx.com/techs/nuxt" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/nuxt.png'" alt="Litlyx-Nuxt-Analytics">
<img class="cursor-pointer" :src="'tech-icons/nuxt.png'"
alt="Litlyx-Nuxt-Analytics">
</a>
<a href="https://docs.litlyx.com/techs/next" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/next.png'" alt="Litlyx-Next-Analytics">
<img class="cursor-pointer" :src="'tech-icons/next.png'"
alt="Litlyx-Next-Analytics">
</a>
<a href="https://docs.litlyx.com/techs/react" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/react.png'" alt="Litlyx-React-Analytics">
<img class="cursor-pointer" :src="'tech-icons/react.png'"
alt="Litlyx-React-Analytics">
</a>
<a href="https://docs.litlyx.com/techs/vue" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/vue.png'" alt="Litlyx-Vue-Analytics">
<img class="cursor-pointer" :src="'tech-icons/vue.png'"
alt="Litlyx-Vue-Analytics">
</a>
<a href="https://docs.litlyx.com/techs/angular" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/angular.png'" alt="Litlyx-Angular-Analytics">
<img class="cursor-pointer" :src="'tech-icons/angular.png'"
alt="Litlyx-Angular-Analytics">
</a>
<a href="https://docs.litlyx.com/techs/python" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/py.png'" alt="Litlyx-Python-Analytics">
<img class="cursor-pointer" :src="'tech-icons/py.png'"
alt="Litlyx-Python-Analytics">
</a>
<a href="https://docs.litlyx.com/techs/serverless" target="_blank">
<img class="cursor-pointer" :src="'tech-icons/serverless.png'" alt="Litlyx-Serverless-Analytics">
<img class="cursor-pointer" :src="'tech-icons/serverless.png'"
alt="Litlyx-Serverless-Analytics">
</a>
</div>
@@ -198,6 +169,48 @@ function reloadPage() {
</CardTitled>
</div>
</div>
<div class="flex gap-4 w-full">
<CardTitled class="w-full h-full" title="Start with Wordpress."
sub="Setup Litlyx analytics in 30 seconds on Wordpress.">
<!-- <template #header>
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
to="https://docs.litlyx.com/techs/wordpress">
Visit documentation
</LyxUiButton>
</template> -->
<div class="flex flex-col items-end">
<div class="justify-center w-full hidden xl:flex gap-3">
<a href="https://docs.litlyx.com/techs/wordpress">
<img class="cursor-pointer" :src="'tech-icons/wpel.png'"
alt="Litlyx-Wordpress-Elementor">
</a>
</div>
</div>
</CardTitled>
<CardTitled class="w-full h-full" title="Start with Shopify."
sub="Setup Litlyx analytics in 30 seconds on Shopify.">
<!-- <template #header>
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
to="https://docs.litlyx.com/techs/shopify">
Visit documentation
</LyxUiButton>
</template> -->
<div class="flex flex-col items-end">
<div class="justify-center w-full hidden xl:flex gap-3">
<a href="https://docs.litlyx.com/techs/shopify">
<img class="cursor-pointer" :src="'tech-icons/shopify.png'" alt="Litlyx-Shopify">
</a>
</div>
</div>
</CardTitled>
</div>
</div>
</div>

View File

@@ -11,5 +11,5 @@ const widgetStyle = computed(() => {
</script>
<template>
<div :style="widgetStyle" class="bg-lyx-widget-light"></div>
<div :style="widgetStyle" class="dark:bg-lyx-widget-light bg-lyx-lightmode-widget"></div>
</template>

View File

@@ -9,7 +9,23 @@ const avgDuration = computed(() => {
return (backendData.value.durations.durations.reduce((a: any, e: any) => a + parseInt(e[1]), 0) / backendData.value.durations.durations.length);
})
const labels = new Array(650).fill('-');
const labels = computed(() => {
if (!backendData?.value?.durations) return [];
const sizes = new Map<string, number>();
for (const e of backendData.value.durations.durations) {
if (!sizes.has(e[0])) {
sizes.set(e[0], 0);
} else {
const data = sizes.get(e[0]) ?? 0;
sizes.set(e[0], data + 1);
}
}
const max = Array.from(sizes.values()).reduce((a, e) => a > e ? a : e, 0);
return new Array(max).fill('-');
});
const durationsDatasets = computed(() => {
if (!backendData?.value?.durations) return [];
@@ -45,7 +61,7 @@ const durationsDatasets = computed(() => {
<div class="cursor-default flex justify-center w-full">
<div v-if="backendData" class="flex flex-col mt-8 gap-6 px-20 items-center w-full">
<div v-if="backendData && !backendPending" class="flex flex-col mt-8 gap-6 px-20 items-center w-full">
<div class="flex gap-8">
<div> Queue size: {{ backendData.queue?.size || 'ERROR' }} </div>

View File

@@ -1,6 +1,16 @@
<script lang="ts" setup>
const { data: feedbacks, pending: pendingFeedbacks } = useFetch<any[]>(() => `/api/admin/feedbacks`, signHeaders());
const { data: feedbacks, pending: pendingFeedbacks, refresh } = useFetch<any[]>(() => `/api/admin/feedbacks`, signHeaders());
async function deleteFeedback(id: string) {
await $fetch('/api/admin/delete_feedback', {
method: 'DELETE',
headers: useComputedHeaders({ custom: { 'Content-Type': 'application/json' } }).value,
body: JSON.stringify({ id })
});
refresh();
}
</script>
@@ -13,12 +23,20 @@ const { data: feedbacks, pending: pendingFeedbacks } = useFetch<any[]>(() => `/a
<div v-if="feedbacks" class="flex flex-col-reverse gap-4 px-20">
<div class="flex flex-col text-center outline outline-[1px] outline-lyx-widget-lighter p-4 gap-2"
v-for="feedback of feedbacks">
<div class="flex flex-col gap-1">
<div>
<div class="flex flex-col gap-1 items-center">
<div class="text-lyx-text-dark"> {{ feedback.user[0]?.email || 'DELETED USER' }} </div>
<div class="text-lyx-text-dark"> {{ feedback.project[0]?.name || 'DELETED PROJECT' }} </div>
<div class="text-lyx-text-dark flex gap-2 items-center">
<div>{{ feedback.project[0]?.name || 'DELETED PROJECT' }}</div>
<div @click="deleteFeedback(feedback._id.toString())" class="hover:text-red-200"><i
class="fas fa-trash"></i></div>
</div>
</div>
{{ feedback.text }}
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { TAdminProject } from '~/server/api/admin/projects';
import { PREMIUM_PLAN, getPlanFromId } from '@data/PREMIUM'
import { PREMIUM_PLAN, getPlanFromId } from '@data/PLANS'
import { useSelectMenuStyle } from '~/composables/ui/useSelectMenuStyle';
import { sub, format, isSameDay, type Duration, startOfDay, endOfDay } from 'date-fns'
@@ -10,35 +10,17 @@ import { sub, format, isSameDay, type Duration, startOfDay, endOfDay } from 'dat
const page = ref<number>(1);
const ordersList = [
{ label: 'created_at -->', id: '{ "created_at": 1 }' },
{ label: 'created_at <--', id: '{ "created_at": -1 }' },
{ label: 'Older', id: '{ "created_at": 1 }' },
{ label: 'Newer', id: '{ "created_at": -1 }' },
{ label: 'active -->', id: '{ "last_log_at": 1 }' },
{ label: 'active <--', id: '{ "last_log_at": -1 }' },
{ label: 'Less active', id: '{ "last_log_at": 1 }' },
{ label: 'More active', id: '{ "last_log_at": -1 }' },
{ label: 'visits -->', id: '{ "visits": 1 }' },
{ label: 'visits <--', id: '{ "visits": -1 }' },
{ label: 'Less usage', id: '{ "limit_total": 1 }' },
{ label: 'More usage', id: '{ "limit_total": -1 }' },
{ label: 'events -->', id: '{ "events": 1 }' },
{ label: 'events <--', id: '{ "events": -1 }' },
{ label: 'sessions -->', id: '{ "sessions": 1 }' },
{ label: 'sessions <--', id: '{ "sessions": -1 }' },
{ label: 'usage total -->', id: '{ "limit_total": 1 }' },
{ label: 'usage total <--', id: '{ "limit_total": -1 }' },
{ label: 'usage visits -->', id: '{ "limit_visits": 1 }' },
{ label: 'usage visits <--', id: '{ "limit_visits": -1 }' },
{ label: 'usage events -->', id: '{ "limit_events": 1 }' },
{ label: 'usage events <--', id: '{ "limit_events": -1 }' },
{ label: 'usage ai -->', id: '{ "limit_ai_messages": 1 }' },
{ label: 'usage ai <--', id: '{ "limit_ai_messages": -1 }' },
{ label: 'plan -->', id: '{ "premium_type": 1 }' },
{ label: 'plan <--', id: '{ "premium_type": -1 }' },
{ label: 'Smaller plan', id: '{ "premium_type": 1 }' },
{ label: 'Bigger plan', id: '{ "premium_type": -1 }' },
]
@@ -190,7 +172,7 @@ const { uiMenu } = useSelectMenuStyle();
<div
class="cursor-default flex justify-center flex-wrap gap-6 mb-[4rem] mt-4 overflow-auto h-full pt-6 pb-[8rem]">
class="cursor-default flex justify-center flex-wrap gap-6 mb-[4rem] mt-4 overflow-auto h-full pt-6 pb-[20rem]">
<AdminOverviewProjectCard v-if="!pendingProjects" :key="project._id.toString()" :project="project"
class="w-[26rem]" v-for="project of projectsInfo?.projects" />

View File

@@ -137,7 +137,7 @@ const { uiMenu } = useSelectMenuStyle();
<div
class="cursor-default flex justify-center flex-wrap gap-6 mb-[4rem] mt-4 overflow-auto h-full pt-6 pb-[8rem]">
class="cursor-default flex justify-center flex-wrap gap-6 mb-[4rem] mt-4 overflow-auto h-full pt-6 pb-[16rem]">
<AdminUsersUserCard v-if="!pendingUsers" :key="user._id.toString()" :user="user" class="w-[26rem]"
v-for="user of usersInfo?.users" />

View File

@@ -1,48 +0,0 @@
<script lang="ts" setup>
import type { TAdminProject } from '~/server/api/admin/projects';
const props = defineProps<{ pid: string }>();
const { data: projectInfo, refresh, pending } = useFetch<{ domains: { _id: string }[], project: TAdminProject }>(
() => `/api/admin/project_info?pid=${props.pid}`,
signHeaders(),
);
</script>
<template>
<div class="mt-6 h-full flex flex-col gap-10 w-full" v-if="!pending">
<div>
<LyxUiButton type="secondary" @click="refresh"> Refresh </LyxUiButton>
</div>
<div class="flex justify-center gap-10" v-if="projectInfo">
<AdminOverviewProjectCard :project="projectInfo.project" class="w-[30rem] shrink-0" />
<AdminMiniChart class="max-w-[40rem]" :pid="pid"></AdminMiniChart>
</div>
<div v-if="projectInfo" class="flex flex-col">
<div>Domains:</div>
<div class="flex flex-wrap gap-8 mt-8">
<div v-for="domain of projectInfo.domains">
{{ domain._id }}
</div>
</div>
</div>
</div>
<div v-if="pending">
Loading...
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import type { TAdminUserInfo } from '~/server/api/admin/user_info';
const props = defineProps<{ user_id: string }>();
const { data: userInfo, refresh, pending } = useFetch<{ projects: TAdminUserInfo }>(
() => `/api/admin/user_info?user_id=${props.user_id}`,
signHeaders(),
);
</script>
<template>
<div class="mt-6 h-full flex flex-col gap-10 w-full overflow-y-auto pb-[10rem]" v-if="!pending">
<div>
<LyxUiButton type="secondary" @click="refresh"> Refresh </LyxUiButton>
</div>
<div class="flex justify-center gap-10 flex-wrap" v-if="userInfo">
<AdminOverviewProjectCard v-for="project of userInfo.projects" :project="project as any"
class="w-[30rem] shrink-0" />
</div>
</div>
<div v-if="pending">
Loading...
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,20 +1,19 @@
<script lang="ts" setup>
import type { TAdminProject } from '~/server/api/admin/projects';
import { getPlanFromId } from '~/shared/data/PREMIUM';
import { getPlanFromId } from '~/shared/data/PLANS';
import { AdminDialogProjectDetails } from '#components';
import { AdminDialogUserDetails } from '#components';
const { openDialogEx } = useCustomDialog();
function showProjectDetails(pid: string) {
openDialogEx(AdminDialogProjectDetails, {
params: { pid }
function showUserDetails(user_id: string) {
openDialogEx(AdminDialogUserDetails, {
params: { user_id }
})
}
const props = defineProps<{ project: TAdminProject }>();
const props = defineProps<{ project: TAdminProject & { domains?: string[] } }>();
const logBg = computed(() => {
@@ -69,9 +68,9 @@ const usageAiLabel = computed(() => {
</div>
<div class="flex gap-4 justify-center text-[.9rem]">
<UTooltip :text="`PRICE_ID: ${project.premium_type}`">
<UTooltip :text="`PRICE_ID: ${project.premium[0].premium_type}`">
<div class="font-medium text-lyx-text-dark">
{{ getPlanFromId(project.premium_type)?.TAG?.replace('APPSUMO', 'AS') ?? 'ERROR' }}
{{ getPlanFromId(project.premium[0].premium_type)?.TAG?.replace('APPSUMO', 'AS') ?? 'ERROR' }}
</div>
</UTooltip>
<div class="text-lyx-text-darker">
@@ -79,9 +78,11 @@ const usageAiLabel = computed(() => {
</div>
</div>
<div class="flex gap-5 justify-center">
<div @click="showProjectDetails(project._id.toString())" class="font-medium hover:text-lyx-primary cursor-pointer">
{{ project.name }}
<div class="flex flex-col items-center py-1">
<div class="text-center"> {{ project.name }} </div>
<div v-if="project.user" @click="showUserDetails(project.premium[0].user_id.toString())"
class="font-medium hover:text-lyx-primary cursor-pointer text-center">
{{ project.user[0].email }}
</div>
</div>
@@ -128,6 +129,13 @@ const usageAiLabel = computed(() => {
</div>
<LyxUiSeparator class="my-2" />
<div v-if="project.domains" class="flex flex-wrap gap-4">
<div v-for="domain of project.domains" class="hover:text-gray-200 cursor-pointer">
{{ domain }}
</div>
</div>
</div>
</template>

View File

@@ -2,15 +2,15 @@
import type { TAdminProject } from '~/server/api/admin/projects';
import type { TAdminUser } from '~/server/api/admin/users';
import { getPlanFromId } from '~/shared/data/PREMIUM';
import { getPlanFromId } from '~/shared/data/PLANS';
import { AdminDialogProjectDetails } from '#components';
import { AdminDialogUserDetails } from '#components';
const { openDialogEx } = useCustomDialog();
function showProjectDetails(pid: string) {
openDialogEx(AdminDialogProjectDetails, {
params: { pid }
function showUserDetails(user_id: string) {
openDialogEx(AdminDialogUserDetails, {
params: { user_id }
})
}
@@ -31,34 +31,11 @@ const props = defineProps<{ user: TAdminUser }>();
</div>
<div class="flex gap-5 justify-center">
<div class="font-medium">
<div class="font-medium hover:text-blue-400 cursor-pointer" @click="showUserDetails(user._id.toString())">
{{ user.email }}
</div>
</div>
<LyxUiSeparator class="my-2" />
<div class="flex flex-col text-[.9rem]">
<div class="flex gap-2" v-for="project of user.projects">
<div class="text-lyx-text-darker">
{{ new Date(project.created_at).toLocaleDateString('it-IT') }}
</div>
<UTooltip :text="`PRICE_ID: ${project.premium_type}`">
<div class="font-medium text-lyx-text-dark">
{{ getPlanFromId(project.premium_type)?.TAG?.replace('APPSUMO', 'AS') ?? 'ERROR' }}
</div>
</UTooltip>
<div @click="showProjectDetails(project._id.toString())"
class="ml-1 hover:text-lyx-primary cursor-pointer">
{{ project.name }}
</div>
</div>
</div>
</div>
<!-- <div class="poppins outline outline-[1px] outline-lyx-widget-lighter p-3 rounded-md relative">

View File

@@ -7,11 +7,7 @@ function goToUpgrade() {
showDrawer('PRICING');
}
const { project } = useProject()
const isPremium = computed(() => {
return project.value?.premium ?? false;
});
const { isPremium } = useLoggedUser()
</script>

View File

@@ -373,8 +373,9 @@ const legendClasses = ref<string[]>([
</div>
<div class="mt-3 font-normal flex flex-col text-[.9rem] dark:text-lyx-text-dark text-lyx-lightmode-text-dark"
v-if="(currentTooltipData as any).sessions > (currentTooltipData as any).visits">
<div> Unique visitors is greater than visits. </div>
<div> This can indicate bot traffic. </div>
<div> Unique visitors are higher than total visits </div>
<div> which often means bots (automated scripts or crawlers)</div>
<div> are inflating the numbers.</div>
</div>
<!-- <div class="bg-lyx-background-lighter h-[2px] w-full my-2"> </div> -->
</LyxUiCard>

View File

@@ -8,7 +8,7 @@ const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()
const chartSlice = computed(() => {
if (snapshotDuration.value <= 3) return 'hour' as Slice;
if (snapshotDuration.value <= 31 * 3) return 'day' as Slice;
if (snapshotDuration.value <= 31 * 2) return 'day' as Slice;
return 'month' as Slice;
});
@@ -109,6 +109,9 @@ const avgSessionDuration = computed(() => {
seconds += avg * 60;
while (seconds >= 60) { seconds -= 60; minutes += 1; }
while (minutes >= 60) { minutes -= 60; hours += 1; }
if (hours == 0 && minutes == 0 && seconds < 10) return `0m ~10s`
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
});

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel']);
const address = ref<string>('');
const description = ref<string>('');
const { data: currentIP } = useFetch<any>('https://api.ipify.org/?format=json');
const canAddAddress = computed(() => {
return address.value.trim().length > 0;
})
async function addAddress() {
if (!canAddAddress.value) return;
try {
const res = await $fetch('/api/shields/ip/add', {
method: 'POST',
headers: useComputedHeaders({}).value,
body: JSON.stringify({ address: address.value, description: description.value })
});
address.value = '';
emit('success');
} catch (ex: any) {
alert(ex.message);
emit('cancel');
}
}
</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-semibold text-[1.1rem]"> Add IP to Block List </div>
<div class="flex flex-col gap-2 dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
<div> Your current IP address is: {{ currentIP?.ip || '...' }} </div>
<div> Copy and Paste your IP address in the box below or enter a custom address </div>
</div>
<div class="flex flex-col gap-2">
<div class="font-medium"> IP Address </div>
<LyxUiInput class="px-2 py-1" v-model="address" placeholder="127.0.0.1"></LyxUiInput>
</div>
<div class="flex flex-col gap-2">
<div class="font-medium"> Description (optional) </div>
<LyxUiInput class="px-2 py-1" v-model="description" placeholder="e.g. localhost or office">
</LyxUiInput>
</div>
<div class="flex flex-col gap-2 dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
<div> Once added, we will start rejecting traffic from this IP within a few minutes.</div>
</div>
<div class="flex">
<LyxUiButton class="w-full text-center" :disabled="!canAddAddress" @click="addAddress()"
type="primary">
Add IP Address
</LyxUiButton>
</div>
</div>
</div>
</UModal>
</template>

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel']);
const domain = ref<string>('');
const canAddDomain = computed(() => {
return domain.value.trim().length > 0;
})
async function addDomain() {
if (!canAddDomain.value) return;
try {
const res = await $fetch('/api/shields/domains/add', {
method: 'POST',
headers: useComputedHeaders({}).value,
body: JSON.stringify({ domain: domain.value })
});
domain.value = '';
emit('success');
} catch (ex: any) {
alert(ex.message);
emit('cancel');
}
}
</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-semibold text-[1.1rem]"> Add Domain to Allow List </div>
<LyxUiInput class="px-2 py-1" v-model="domain"></LyxUiInput>
<div class="flex flex-col gap-2 dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
<div>
<div> You can use a wildcard (*) to match multiple hostnames. </div>
<div> For example, *.domain.com will only record traffic on the main domain and all the
subdomains.
</div>
</div>
<div> NB: Once added, we will start allowing traffic only from matching hostnames within a few
minutes.</div>
</div>
<div class="flex">
<LyxUiButton class="w-full text-center" :disabled="!canAddDomain" @click="addDomain()" type="primary">
Add domain
</LyxUiButton>
</div>
</div>
</div>
</UModal>
</template>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel']);
const props = defineProps<{ address: string }>();
async function deleteAddress() {
if (!props.address) return;
try {
const res = await $fetch('/api/shields/ip/delete', {
method: 'DELETE',
headers: useComputedHeaders({}).value,
body: JSON.stringify({ address: props.address })
});
emit('success');
} catch (ex: any) {
alert(ex.message);
emit('cancel');
}
}
</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-semibold text-[1.1rem]"> IP Address delete </div>
<div> Are you sure to delete the blacklisted IP Address
<span class="font-semibold">{{ props.address }}</span>
</div>
<div class="flex justify-end gap-2">
<LyxUiButton type="secondary" @click="emit('cancel')">
Cancel
</LyxUiButton>
<LyxUiButton @click="deleteAddress()" type="danger">
Delete
</LyxUiButton>
</div>
</div>
</div>
</UModal>
</template>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel']);
const props = defineProps<{ domain: string }>();
async function deleteDomain() {
if (!props.domain) return;
try {
const res = await $fetch('/api/shields/domains/delete', {
method: 'DELETE',
headers: useComputedHeaders({}).value,
body: JSON.stringify({ domain: props.domain })
});
emit('success');
} catch (ex: any) {
alert(ex.message);
emit('cancel');
}
}
</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-semibold text-[1.1rem]"> Domain delete </div>
<div> Are you sure to delete the whitelisted domain
<span class="font-semibold">{{ props.domain }}</span>
</div>
<div class="flex justify-end gap-2">
<LyxUiButton type="secondary" @click="emit('cancel')">
Cancel
</LyxUiButton>
<LyxUiButton @click="deleteDomain()" type="danger">
Delete
</LyxUiButton>
</div>
</div>
</div>
</UModal>
</template>

View File

@@ -196,7 +196,7 @@ function getPricingsData() {
<div class="flex justify-between items-center mt-10 flex-col xl:flex-row">
<div class="flex flex-col gap-2">
<div class="poppins text-[1.1rem] text-lyx-lightmode-text dark:text-yellow-400 mb-2">
*Plan upgrades are applicable exclusively to this project(workspace).
*Plan upgrades are applied to account level.
</div>
<div class="poppins text-[2rem] font-semibold">
Do you need help ?

View File

@@ -72,7 +72,7 @@ const canSearch = computed(() => {
<template>
<CardTitled title="Event metadata analyzer" sub="Filter events metadata fields to analyze them" class="w-full p-4">
<CardTitled title="Analyze event metadata" sub="Filter events metadata fields to analyze them" class="w-full p-4">
<div class="">

View File

@@ -74,7 +74,7 @@ onMounted(async () => {
</div>
<AdvancedStackedBarChart v-if="!eventsStackedData.pending.value && !errorData.errored"
:datasets="eventsStackedData.data.value?.datasets || []"
:labels="eventsStackedData.data.value?.labels || []">
:labels="eventsStackedData.data.value?.labels || []" legendPosition="bottom">
</AdvancedStackedBarChart>
<div v-if="errorData.errored" class="flex items-center justify-center py-8 h-full">
{{ errorData.text }}

View File

@@ -52,15 +52,6 @@ const { safeSnapshotDates } = useSnapshot();
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>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
import { DialogConfirmLogout } from '#components';
const router = useRouter();
const { user, isAdmin, setLoggedUser } = useLoggedUser();
const { setToken } = useAccessToken();
const selfhosted = useSelfhosted();
const modal = useModal();
function onLogout() {
modal.open(DialogConfirmLogout, {
onSuccess() {
modal.close();
console.log('LOGOUT');
setToken('');
setLoggedUser(undefined);
router.push('/login');
},
onCancel() {
modal.close();
}
})
}
const colorMode = useColorMode()
const isDark = computed({
get() {
return colorMode.value === 'dark'
},
set() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
const items = computed(() => {
const slots: any = [];
if (selfhosted === true) {
slots.push([
{
label: 'Account',
icon: 'far fa-user',
to: '/account'
},
]);
} else {
slots.push([
{
label: 'Account',
icon: 'far fa-user',
to: '/account'
},
{
label: 'Billing',
icon: 'far fa-wallet',
to: '/billing'
},
]);
}
slots.push([
{
label: 'Switch theme',
icon: isDark.value ? 'far fa-sun' : 'far fa-moon',
click: () => {
isDark.value = !isDark.value
}
}
]);
if (isAdmin.value === true) {
slots.push([
{
label: 'Admin',
icon: 'far fa-cat',
to: '/admin'
},
{
label: 'Logout',
icon: 'far fa-arrow-right-from-bracket scale-x-[-100%]',
click: () => onLogout()
}
])
} else {
slots.push([
{
label: 'Logout',
icon: 'far fa-arrow-right-from-bracket scale-x-[-100%]',
click: () => onLogout()
}
])
}
return slots;
});
</script>
<template>
<UDropdown :popper="{ placement: 'top' }" class="w-full" :items="items" :ui="{
width: 'w-[14rem]',
strategy: 'override',
background: 'dark:bg-lyx-background bg-lyx-lightmode-background',
ring: 'ring-1 dark:ring-lyx-widget-lighter ring-lyx-lightmode-widget',
item: {
active: 'dark:text-lyx-text text-lyx-lightmode-text dark:bg-lyx-background-lighter bg-lyx-lightmode-background-light'
}
}">
<div class="dark:hover:bg-lyx-widget-light hover:bg-lyx-lightmode-widget cursor-pointer px-1 py-1 rounded-lg text-lyx-lightmode-text-dark dark:text-lyx-text-dark flex items-center gap-2 w-full"
v-if="user && user.logged && user.user">
<div class="flex">
<div
class="flex shrink-0 items-center justify-center h-6 w-6 rounded-full border-solid border-[2px] border-lyx-lightmode-widget dark:border-lyx-widget-lighter">
<div class="text-[.8rem] font-medium translate-y-[1px]">
{{ user.user.name.substring(0, 1).toUpperCase() }}
</div>
</div>
</div>
<div class="text-[.9rem] font-medium poppins overflow-hidden text-ellipsis grow">
{{ user.user.email }}
</div>
<div class="w-[2rem] text-right pr-1">
<i class="fas fa-ellipsis-h brightness-75"></i>
</div>
</div>
<template #item="e">
<div class="flex items-center gap-3">
<i class="text-lyx-text-darker" :class="e.item.icon"></i>
<div>{{ e.item.label }}</div>
</div>
</template>
</UDropdown>
</template>

View File

@@ -32,7 +32,7 @@ const { data: pendingInvites, refresh: refreshInvites } = useFetch('/api/project
headers: useComputedHeaders({})
});
const { userRoles, setLoggedUser } = useLoggedUser();
const { userRoles, isPremium } = useLoggedUser();
const { projectList } = useProject();
const debugMode = process.dev;
@@ -73,48 +73,14 @@ async function deleteSnapshot(close: () => any) {
close();
}
async function generatePDF() {
try {
const res = await $fetch<Blob>('/api/project/generate_pdf', {
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'x-snapshot-name': snapshot.value.name } }).value,
responseType: 'blob'
});
const url = URL.createObjectURL(res);
const a = document.createElement('a');
a.href = url;
a.download = `Report.pdf`;
a.click();
URL.revokeObjectURL(url);
} catch (ex: any) {
alert(ex.message);
}
}
const { setToken } = useAccessToken();
const router = useRouter();
const { actions } = useProject();
const { showDrawer } = useDrawer();
const modal = useModal();
function onLogout() {
modal.open(DialogConfirmLogout, {
onSuccess() {
modal.close();
console.log('LOGOUT');
setToken('');
setLoggedUser(undefined);
router.push('/login');
},
onCancel() {
modal.close();
}
})
}
const { data: maxProjects } = useFetch("/api/user/max_projects", {
headers: computed(() => {
return {
@@ -149,19 +115,12 @@ function openPendingInvites() {
</script>
<template>
<div class="CVerticalNavigation border-solid border-[#D9D9E0] dark:border-[#202020] border-r-[1px] h-full w-[20rem] bg-lyx-lightmode-background dark:bg-lyx-background flex shadow-[1px_0_10px_#000000]"
<div class="CVerticalNavigation border-solid border-[#D9D9E0] dark:border-[#202020] border-r-[1px] h-full w-[16rem] bg-lyx-lightmode-background dark:bg-lyx-background flex shadow-[1px_0_10px_#000000]"
:class="{
'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen,
'hidden lg:flex': !isOpen
}">
<div class="py-4 px-2 gap-6 flex flex-col w-full">
<!-- <div class="flex px-2" v-if="!isPremium">
<LyxUiButton type="primary" class="w-full text-center text-[.8rem] font-medium" @click="pricingDrawer.visible.value = true;">
Upgrade plan
</LyxUiButton>
</div> -->
<div class="py-4 pb-2 px-2 gap-6 flex flex-col w-full">
<div class="flex px-2 flex-col">
@@ -278,12 +237,6 @@ function openPendingInvites() {
</div>
</div>
<div class="w-full flex mt-4">
<LyxUiButton @click="generatePDF()" type="outline" class="w-full text-center text-[.8rem]">
Export report
</LyxUiButton>
</div>
</div>
<div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full"></div>
@@ -322,6 +275,16 @@ function openPendingInvites() {
<div class="grow"></div>
<LyxUiCard class="w-full mb-4" v-if="!isPremium">
<div class="flex flex-col gap-3">
<div class="text-center"> Upgrade to premium </div>
<LyxUiButton type="primary" class="w-full text-center text-[.8rem] font-medium"
@click="showDrawer('PRICING')">
Upgrade
</LyxUiButton>
</div>
</LyxUiCard>
<div v-if="pendingInvites && pendingInvites.length > 0" @click="openPendingInvites()"
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex flex-col justify-center cursor-pointer">
<div class="poppins font-medium dark:text-lyx-text text-lyx-lightmode-text">
@@ -334,25 +297,18 @@ function openPendingInvites() {
</div>
</div>
<div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full px-4 mb-3"></div>
<!-- <LyxUiSeparator class="px-4 mb-2"></LyxUiSeparator> -->
<div class="flex justify-end px-2">
<div class="grow flex gap-3">
<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">
<i class="far fa-cat"></i>
</NuxtLink>
<LyxUiCard class="flex py-1 px-1 w-full relative">
<div class="absolute top-[-22%] right-0">
<div v-if="isPremium" @click="showDrawer('PRICING')"
class="flex items-center gap-1 poppins text-[.5rem] bg-[#fbbe244f] outline outline-[1px] outline-[#fbbf24] px-3 py-[.12rem] rounded-sm">
<i class="far fa-crown mb-[2px]"></i>
<div>Premium</div>
</div>
<UTooltip text="Logout" :popper="{ arrow: true, placement: 'top' }">
<div @click="onLogout()"
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-arrow-right-from-bracket scale-x-[-100%]"></i>
</div>
</UTooltip>
</div>
<LayoutVerticalBottomMenu></LayoutVerticalBottomMenu>
</LyxUiCard>

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
const emits = defineEmits<{
(event: 'file_selected', value: string): void;
}>();
const fileInput = ref<HTMLElement | null>(null)
const isDragging = ref(false)
const triggerFileSelect = () => { (fileInput.value as any).click() }
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
if (file) {
const b64 = await getBase64FromFile(file);
emits('file_selected', b64);
}
}
function getBase64FromFile(file: File) {
return new Promise<string>(resolve => {
const reader = new FileReader();
reader.onloadend = async function () {
const base64String = reader.result;
if (!base64String) return alert('Error reading image');
resolve(base64String as string);
}
reader.readAsDataURL(file);
})
}
const handleDrop = async (event: any) => {
const file = event.dataTransfer.files[0]
isDragging.value = false
if (file) {
const b64 = await getBase64FromFile(file);
emits('file_selected', b64);
}
}
const handleDragOver = () => {
isDragging.value = true
}
const handleDragLeave = () => {
isDragging.value = false
}
</script>
<template>
<div id="drop-area"
class="w-full select-none max-w-md border-2 border-dashed border-gray-600 rounded-lg p-12 text-center cursor-pointer hover:border-blue-500 transition"
@click="triggerFileSelect" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave"
@drop.prevent="handleDrop" :class="{ 'border-blue-500': isDragging }">
<p class="text-gray-400">
Drag & drop an image here
<br>
or click to select a file
</p>
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="handleFileChange" />
</div>
</template>

View File

@@ -54,7 +54,7 @@ async function redeemCode() {
Redeemed codes: {{ valid_codes.data.value?.count || '0' }}
</div>
<div class="poppins text-[1.1rem] text-lyx-lightmode-text dark:text-yellow-400 mb-2">
*Plan upgrades are applicable exclusively to this project(workspace).
*Plan upgrades are applied to account level.
</div>
</template>
</SettingsTemplate>

View File

@@ -148,7 +148,7 @@ const sessionsLabel = computed(() => {
<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]">
<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 its initial state (0
visits 0 events 0 sessions) </div>
<div @click="openDeleteAllDomainDataDialog()"
class="text-[#e95b61] poppins font-semibold cursor-pointer hover:text-black hover:bg-red-700 outline rounded-lg w-fit px-8 py-2 outline-[1px] outline-[#532b26] bg-lyx-lightmode-widget-light dark:bg-[#291415]">

View File

@@ -215,7 +215,7 @@ function copyProjectId() {
</div>
</template>
<template #pdelete>
<div class="flex lg:justify-end" v-if="!isGuest">
<div class="flex lg:justify-end pb-30" v-if="!isGuest">
<LyxUiButton type="danger" @click="deleteProject()">
Delete project
</LyxUiButton>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs';
import type { SettingsTemplateEntry } from './Template.vue';
import { getPlanFromId, PREMIUM_PLAN, type PREMIUM_TAG } from '@data/PREMIUM';
import { getPlanFromId, PREMIUM_PLAN, type PLAN_TAG } from '@data/PLANS';
const { projectId, isGuest } = useProject();
@@ -53,10 +53,10 @@ function openInvoice(link: string) {
window.open(link, '_blank');
}
function getPremiumName(type: number) {
function getTagName(type: number) {
return Object.keys(PREMIUM_PLAN).map(e => ({
...PREMIUM_PLAN[e as PREMIUM_TAG], name: e
...PREMIUM_PLAN[e as PLAN_TAG], name: e
})).find(e => e.ID == type)?.name;
}
@@ -168,7 +168,7 @@ const { showDrawer } = useDrawer();
</div>
<div
class="flex lato text-[.7rem] bg-transparent border-[#262626] border-[1px] px-[.6rem] rounded-sm">
{{ planData.premium ? getPremiumName(planData.premium_type) : 'FREE' }}
{{ planData.premium ? getTagName(planData.premium_type) : 'FREE' }}
</div>
</div>
</div>
@@ -187,9 +187,6 @@ const { showDrawer } = useDrawer();
</div>
<div class="poppins"> {{ daysLeft }} days left </div>
</div>
<div class="flex justify-center">
Subscription: {{ planData.subscription_status }}
</div>
</div>
</div>
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
@@ -222,7 +219,7 @@ const { showDrawer } = useDrawer();
<div class="poppins"> Usage:</div>
<div class="flex items-center gap-2 md:gap-4 flex-col pt-4 md:pt-0 md:flex-row">
<div class="grow w-full md:w-auto">
<UProgress :color="color" :min="0" :max="planData.limit" :value="planData.count">
<UProgress v-if="planData" :color="color" :min="0" :max="planData.limit" :value="planData.count">
</UProgress>
</div>
<div class="poppins"> {{ percent }}</div>

View File

@@ -0,0 +1,101 @@
<script lang="ts" setup>
import { DialogShieldsDeleteAddress, DialogShieldsAddAddress } from '#components';
definePageMeta({ layout: 'dashboard' });
const { data: blackAddresses, refresh: refreshAddresses, pending: pendingAddresses } = useFetch('/api/shields/ip/list', {
headers: useComputedHeaders({})
});
const toast = useToast()
const modal = useModal();
function showAddAddressModal() {
modal.open(DialogShieldsAddAddress, {
onSuccess: () => {
refreshAddresses();
modal.close();
toast.add({
id: 'shield_address_add_success',
title: 'Success',
description: 'Blacklist updated with the new address',
timeout: 5000
});
},
onCancel: () => {
modal.close();
}
})
}
function showDeleteAddressModal(address: string) {
modal.open(DialogShieldsDeleteAddress, {
address,
onSuccess: () => {
refreshAddresses();
modal.close();
toast.add({
id: 'shield_address_remove_success',
title: 'Deleted',
description: 'Blacklist address deleted successfully',
timeout: 5000
});
},
onCancel: () => {
modal.close();
}
})
}
</script>
<template>
<div class="py-4 flex">
<LyxUiCard class="w-full mx-2">
<div>
<div class="text-[1.2rem] font-semibold"> IP Block List </div>
<div class="dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
Reject incoming traffic from specific IP addresses
</div>
</div>
<LyxUiSeparator class="my-3"></LyxUiSeparator>
<div class="flex justify-end pb-3">
<LyxUiButton type="primary" @click="showAddAddressModal()"> Add IP Address </LyxUiButton>
</div>
<div class="flex justify-center pb-8 text-[1.2rem]" v-if="pendingAddresses">
<i class="fas fa-loader animate-spin"></i>
</div>
<div v-if="!pendingAddresses && blackAddresses && blackAddresses.length == 0"
class="flex flex-col items-center pb-8">
<div>
No domain rules configured for this project.
</div>
<div class="font-semibold">
Traffic from all domains is currently accepted.
</div>
</div>
<div v-if="!pendingAddresses && blackAddresses && blackAddresses.length > 0"
class="grid grid-cols-[auto_auto_auto_auto] px-10">
<div> Domain </div>
<div class="col-span-2"> Description </div>
<div> Actions </div>
<LyxUiSeparator class="col-span-4 my-3"></LyxUiSeparator>
<template v-for="entry of blackAddresses">
<div class="mb-2"> {{ entry.address }} </div>
<div class="col-span-2">{{ entry.description || 'No description' }}</div>
<div> <i @click="showDeleteAddressModal(entry.address)"
class="far fa-trash cursor-pointer hover:text-lyx-text-dark"></i> </div>
</template>
</div>
</LyxUiCard>
</div>
</template>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const { data: botOptions, refresh: refreshBotOptions, pending: pendingBotOptions } = useFetch('/api/shields/bots/options', {
headers: useComputedHeaders({})
});
async function onChange(newValue: boolean) {
await $fetch('/api/shields/bots/update_options', {
method: 'POST',
headers: useComputedHeaders({ custom: { 'Content-Type': 'application/json' } }).value,
body: JSON.stringify({ block: newValue })
})
await refreshBotOptions();
}
</script>
<template>
<div class="py-4 flex">
<LyxUiCard class="w-full mx-2">
<div>
<div class="text-[1.2rem] font-semibold"> Block bot traffic </div>
<div class="dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
Automatically block unwanted bot and crawler traffic to protect your site from spam, scrapers, and
unnecessary server load.
</div>
</div>
<LyxUiSeparator class="my-3"></LyxUiSeparator>
<div class="flex justify-center pb-8 text-[1.2rem]" v-if="pendingBotOptions">
<i class="fas fa-loader animate-spin"></i>
</div>
<div v-if="!pendingBotOptions && botOptions">
<div class="flex gap-2">
<UToggle :modelValue="botOptions.block" @change="onChange"></UToggle>
<div> Enable bot protection </div>
</div>
</div>
</LyxUiCard>
</div>
</template>

View File

@@ -0,0 +1,99 @@
<script lang="ts" setup>
import { DialogShieldsAddDomain, DialogShieldsDeleteDomain } from '#components';
definePageMeta({ layout: 'dashboard' });
const { data: allowedDomains, refresh: refreshDomains, pending: pendingDomains } = useFetch('/api/shields/domains/list', {
headers: useComputedHeaders({})
});
const toast = useToast()
const modal = useModal();
function showAddDomainModal() {
modal.open(DialogShieldsAddDomain, {
onSuccess: () => {
refreshDomains();
modal.close();
toast.add({
id: 'shield_domain_add_success',
title: 'Success',
description: 'Whitelist updated with the new domain',
timeout: 5000
});
},
onCancel: () => {
modal.close();
}
})
}
function showDeleteDomainModal(domain: string) {
modal.open(DialogShieldsDeleteDomain, {
domain,
onSuccess: () => {
refreshDomains();
modal.close();
toast.add({
id: 'shield_domain_remove_success',
title: 'Deleted',
description: 'Whitelist domain deleted successfully',
timeout: 5000
});
},
onCancel: () => {
modal.close();
}
})
}
</script>
<template>
<div class="py-4 flex">
<LyxUiCard class="w-full mx-2">
<div>
<div class="text-[1.2rem] font-semibold"> Domains allow list </div>
<div class="dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
Accept incoming traffic only from familiar domains.
</div>
</div>
<LyxUiSeparator class="my-3"></LyxUiSeparator>
<div class="flex justify-end pb-3">
<LyxUiButton type="primary" @click="showAddDomainModal()"> Add Domain </LyxUiButton>
</div>
<div class="flex justify-center pb-8 text-[1.2rem]" v-if="pendingDomains">
<i class="fas fa-loader animate-spin"></i>
</div>
<div v-if="!pendingDomains && allowedDomains && allowedDomains.length == 0"
class="flex flex-col items-center pb-8">
<div>
No domain rules configured for this project.
</div>
<div class="font-semibold">
Traffic from all domains is currently accepted.
</div>
</div>
<div v-if="!pendingDomains && allowedDomains && allowedDomains.length > 0"
class="grid grid-cols-[auto_auto_auto_auto] px-10">
<div class="col-span-3">Domain</div>
<div>Actions</div>
<LyxUiSeparator class="col-span-4 my-3"></LyxUiSeparator>
<template v-for="domain of allowedDomains">
<div class="col-span-3 mb-3">{{ domain }}</div>
<div> <i @click="showDeleteDomainModal(domain)"
class="far fa-trash cursor-pointer hover:text-lyx-text-dark"></i> </div>
</template>
</div>
</LyxUiCard>
</div>
</template>

View File

@@ -11,17 +11,18 @@ const isLogged = computed(() => {
return loggedUser.value?.logged;
})
function getUserRoles() {
const isPremium = computed(() => {
if (!loggedUser.value?.logged) return false;
return loggedUser.value.user.roles.includes('PREMIUM');
});
const isAdmin = computed(() => {
const isAdmin = computed(() => {
if (!loggedUser.value?.logged) return false;
return loggedUser.value.user.roles.includes('ADMIN');
});
});
return { isPremium, isAdmin }
const isPremium = computed(() => {
if (!loggedUser.value?.logged) return false;
return loggedUser.value.user.roles.includes('PREMIUM');
});
function getUserRoles() {
return { isAdmin, isPremium }
}
export const isAdminHidden = ref<boolean>(false);
@@ -29,6 +30,8 @@ export const isAdminHidden = ref<boolean>(false);
export function useLoggedUser() {
return {
isLogged,
isPremium,
isAdmin,
user: loggedUser,
userRoles: getUserRoles(),
setLoggedUser

View File

@@ -18,7 +18,9 @@ const sections: Section[] = [
entries: [
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
{ label: 'Reports', to: '/reports', icon: 'fal fa-file' },
{ label: 'Members', to: '/members', icon: 'fal fa-users' },
{ label: 'Shields', to: '/shields', icon: 'fal fa-shield' },
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
// { label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
@@ -26,7 +28,7 @@ const sections: Section[] = [
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
{ grow: true, label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
// {
// grow: true,
// label: 'Leave a Feedback', icon: 'fal fa-message',

View File

@@ -45,15 +45,12 @@ export default defineNuxtConfig({
AI_PROJECT: process.env.AI_PROJECT,
AI_KEY: process.env.AI_KEY,
EMAIL_SECRET: process.env.EMAIL_SECRET,
PAYMENT_SECRET: process.env.PAYMENT_SECRET,
AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET,
GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID,
GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET,
GITHUB_AUTH_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID,
GITHUB_AUTH_CLIENT_SECRET: process.env.GITHUB_AUTH_CLIENT_SECRET,
STRIPE_SECRET: process.env.STRIPE_SECRET,
STRIPE_WH_SECRET: process.env.STRIPE_WH_SECRET,
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
NOAUTH_USER_PASS: process.env.NOAUTH_USER_PASS,
MODE: process.env.MODE || 'NONE',

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
</script>
<template>
<div class="p-8">
<div class="font-semibold text-[1.3rem] pl-4"> Account </div>
<SettingsAccount></SettingsAccount>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
</script>
<template>
<div class="p-8">
<div class="font-semibold text-[1.3rem] pl-4"> Billing </div>
<SettingsBilling></SettingsBilling>
</div>
</template>

View File

@@ -6,13 +6,6 @@ definePageMeta({ layout: 'dashboard' });
const { project } = useProject();
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
const selfhosted = useSelfhosted();
const canDownload = computed(() => {
if (selfhosted) return true;
return isPremium.value;
});
const metricsInfo = ref<number>(0);
const columns = [
@@ -110,17 +103,10 @@ function goToUpgrade() {
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div>
<div v-if="canDownload" @click="downloadCSV()"
<div @click="downloadCSV()"
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
Download CSV
</div>
<div v-if="!canDownload" @click="goToUpgrade()"
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
<i class="far fa-lock"></i>
Upgrade plan for CSV
</div>
</div>

View File

@@ -6,13 +6,6 @@ definePageMeta({ layout: 'dashboard' });
const { project } = useProject();
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
const selfhosted = useSelfhosted();
const canDownload = computed(() => {
if (selfhosted) return true;
return isPremium.value;
});
const metricsInfo = ref<number>(0);
const columns = [
@@ -115,17 +108,11 @@ function goToUpgrade() {
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
</div>
<div v-if="canDownload" @click="downloadCSV()"
<div @click="downloadCSV()"
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
Download CSV
</div>
<div v-if="!canDownload" @click="goToUpgrade()"
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
<i class="far fa-lock"></i>
Upgrade plan for CSV
</div>
</div>

View File

@@ -53,8 +53,17 @@ const eventsData = await useFetch(`/api/data/count`, {
</LyxUiCard>
<div>
<BarCardEvents :key="refreshKey"></BarCardEvents>
<div class="flex gap-6 flex-col xl:flex-row xl:h-full">
<BarCardEvents class="xl:flex-[4]" :key="refreshKey"></BarCardEvents>
<CardTitled :key="refreshKey" class="p-4 xl:flex-[2] w-full h-full" title="Top events"
sub="Displays key events.">
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
</CardTitled>
</div>
<div class="flex gap-6 flex-col xl:flex-row xl:h-full">
@@ -74,11 +83,6 @@ const eventsData = await useFetch(`/api/data/count`, {
</div>
</CardTitled>
<CardTitled :key="refreshKey" class="p-4 xl:flex-[2] w-full h-full" title="Top events"
sub="Displays key events.">
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
</CardTitled>
</div>

View File

@@ -2,30 +2,44 @@
definePageMeta({ layout: 'none' });
const colorMode = useColorMode();
</script>
<template>
<div class="w-full h-full">
<div
class="w-full h-full flex flex-col items-center pt-[35vh] dark:bg-lyx-background bg-lyx-lightmode-background">
<div class="flex items-center h-full flex-col gap-4">
<div class="text-accent mt-[20vh] poppins font-semibold text-[1.5rem]">
Payment success
<div>
<img v-if="colorMode.value === 'dark'" class="w-[9rem]" :src="'logo-white.png'">
<img v-if="colorMode.value === 'light'" class="w-[9rem]" :src="'logo-black.png'">
</div>
<div class="poppins">
We hope Lilyx can help you make better metrics-driven decision to help your business.
<div class="poppins text-[1.2rem] text-center font-semibold mt-10">
Payment successful
</div>
<NuxtLink to="/?just_logged=true" class="text-accent mt-10 bg-menu px-6 py-2 rounded-lg hover:bg-black font-semibold poppins cursor-pointer">
<div class="mt-2 dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
Thanks for choosing Litlyx. You're ready to go!
</div>
<LyxUiButton type="primary" class="mt-10 py-2" to="/?just_logged=true">
Go back to dashboard
</NuxtLink>
</LyxUiButton>
<!-- <div class="flex items-center h-full flex-col gap-4"> -->
<!-- <div class="poppins">
We hope Litlyx can help you make better metrics-driven decision to help your business.
</div>
<NuxtLink to="/?just_logged=true"
class="text-accent mt-10 bg-menu px-6 py-2 rounded-lg hover:bg-black font-semibold poppins cursor-pointer">
Go back to dashboard
</NuxtLink> -->
<!-- </div> -->
</div>

View File

@@ -61,7 +61,8 @@ async function registerAccount() {
Sign up
</div>
<div class="text-lyx-lightmode-text dark:text-lyx-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
<div
class="text-lyx-lightmode-text dark:text-lyx-text/80 text-[1.2rem] font-light text-center w-[70%] poppins mt-2">
Track web analytics and custom events
with extreme simplicity in under 30 sec.
<br>
@@ -88,8 +89,8 @@ async function registerAccount() {
Password must be at least 6 chars long
</div>
<div class="flex justify-center mt-4 z-[100]">
<LyxUiButton :disabled="!canRegister" @click="registerAccount()" class="text-center"
type="primary">
<LyxUiButton :disabled="!canRegister" @click="canRegister ? registerAccount() : ''"
class="text-center" type="primary">
Sign up
</LyxUiButton>
</div>

View File

@@ -1,93 +0,0 @@
<script setup lang="ts">
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
async function generatePDF() {
try {
const res = await $fetch<Blob>('/api/project/generate_pdf', {
...signHeaders(),
responseType: 'blob'
});
const url = URL.createObjectURL(res);
const a = document.createElement('a');
a.href = url;
a.download = `Report.pdf`;
a.click();
URL.revokeObjectURL(url);
} catch (ex: any) {
alert(ex.message);
}
}
</script>
<template>
<div class="home w-full h-full px-10 lg:px-0 overflow-y-auto pb-[12rem] md:pb-0">
<DialogCreateSnapshot></DialogCreateSnapshot>
<!-- <div class="flex flex-col items-center justify-center mt-20 gap-20">
<div class="flex flex-col items-center justify-center gap-10">
<div class="poppins text-[2.4rem] font-bold text-text">
Project Report
</div>
<div class="poppins text-[1.4rem] text-center lg:text-[1.8rem] text-text-sub/90">
One-Click, Comprehensive KPI PDF for Your Investors or Team.
</div>
<div v-if="activeProject" class="flex md:gap-2 flex-col md:flex-row">
<div class="poppins text-[1.4rem] font-semibold text-text-sub/90">
Relative to:
</div>
<div class="poppins text-[1.4rem] font-semibold text-text">
{{ activeProject.name }}
</div>
</div>
</div>
<div>
<div @click="generatePDF()"
class="flex flex-col rounded-xl overflow-hidden hover:shadow-[0_0_50px_#2969f1] hover:outline hover:outline-[2px] hover:outline-accent cursor-pointer">
<div class="h-[14rem] aspect-[9/7] bg-[#2f2a64] flex relative">
<img class="object-cover" :src="'/report/card_image.png'">
<div
class="absolute px-4 py-1 rounded-lg poppins left-2 flex gap-2 bottom-2 bg-orange-500/80 items-center">
<div class="flex items-center"> <i class="far fa-fire text-[1.1rem]"></i></div>
<div class="poppins text-[1rem] font-semibold"> Popular </div>
</div>
</div>
<div class="bg-[#444444cc] p-4 h-[7rem] relative">
<div class="poppins text-[1.2rem] font-bold text-text">
Generate
</div>
<div class="poppins text-[1rem] text-text-sub/90">
Create your report now
</div>
<div class="absolute right-4 bottom-3">
<i class="fas fa-arrow-right text-[1.2rem]"></i>
</div>
</div>
</div>
</div>
</div> -->
</div>
</template>

132
dashboard/pages/reports.vue Normal file
View File

@@ -0,0 +1,132 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const customization = ref<any>();
const { snapshot } = useSnapshot();
const { showDrawer } = useDrawer();
const { isPremium } = useLoggedUser()
onMounted(async () => {
const res = await $fetch('/api/report/customization', {
headers: useComputedHeaders().value
})
customization.value = res;
})
async function updateCustomization() {
await $fetch('/api/report/update_customization', {
method: 'POST',
headers: useComputedHeaders({
custom: {
'Content-Type': 'application/json'
}
}).value,
body: JSON.stringify(customization.value)
})
}
const generating = ref<boolean>(false);
async function generateReport(type: number) {
if (generating.value === true) return;
generating.value = true;
try {
const res = await $fetch<Blob>(`/api/project/generate_pdf?type=${type}`, {
headers: useComputedHeaders({
useSnapshotDates: false, custom: {
'x-snapshot-name': snapshot.value.name
}
}).value,
responseType: 'blob'
});
const url = URL.createObjectURL(res);
const a = document.createElement('a');
a.href = url;
a.download = `Report.pdf`;
a.click();
URL.revokeObjectURL(url);
} catch (ex: any) {
alert(ex.message);
}
generating.value = false;
}
function selectColor(color: string) {
customization.value.bg = color;
updateCustomization();
}
function onFileSelected(e: string) {
customization.value.logo = e;
updateCustomization();
}
</script>
<template>
<div class="p-6">
<div class="flex flex-col gap-4">
<CardTitled class="w-full h-full" title="Choose a report" sub="Select a report type">
<div class="w-full flex gap-4 h-[18rem]">
<LyxUiCard class="flex-1 h-full">
<div @click="generateReport(1)"
:class="{ 'cursor-pointer hover:text-lyx-text-darker': !generating }"
class="flex justify-center items-center text-[1.2rem] h-full">
<div v-if="!generating"> Easy report </div>
<div v-if="generating" class="flex justify-center pb-8 text-[1.2rem]">
<i class="fas fa-loader animate-spin"></i>
</div>
</div>
</LyxUiCard>
<LyxUiCard class="flex-1 h-full">
<div class="flex justify-center items-center text-[1.2rem] h-full">
<div class="text-gray-400">(coming soon)</div>
</div>
</LyxUiCard>
</div>
</CardTitled>
<div class="flex gap-4">
<CardTitled class="w-full h-full relative" title="Customize theme" sub="Choose the report colors">
<div v-if="!isPremium" @click="showDrawer('PRICING')"
class="absolute w-full h-full top-0 left-0 bg-black/80 rounded-lg flex items-center justify-center gap-1">
<div class="text-amber-300"> <i class="far fa-lock"></i> </div>
<div class="text-amber-300"> Premium only </div>
</div>
<div v-if="customization" class="w-full flex gap-2 h-[18rem]">
<div @click="selectColor('white')"
class="flex items-center justify-center rounded-lg bg-white border-solid border-[1px] border-gray-200 cursor-pointer w-[4rem] h-[2rem]">
<i v-if="customization.bg == 'white'" class="fas fa-check text-blue-600"></i>
</div>
<div @click="selectColor('black')"
class="flex items-center justify-center rounded-lg bg-black border-solid border-[1px] border-gray-200 cursor-pointer w-[4rem] h-[2rem]">
<i v-if="customization.bg == 'black'" class="fas fa-check text-blue-600"></i>
</div>
</div>
</CardTitled>
<CardTitled class="w-full h-full relative" title="Customize logo" sub="Upload your logo">
<div v-if="!isPremium" @click="showDrawer('PRICING')"
class="absolute w-full h-full top-0 left-0 bg-black/80 rounded-lg flex items-center justify-center gap-1">
<div class="text-amber-300"> <i class="far fa-lock"></i> </div>
<div class="text-amber-300"> Premium only </div>
</div>
<div v-if="customization" style="height: 18rem;" class="w-full flex gap-4">
<img v-if="customization.logo" :src="customization.logo" class="w-[256px] h-[256px]">
<div class="flex h-[10rem]">
<SelectorImageSelector class="w-fit" @file_selected="onFileSelected">
</SelectorImageSelector>
</div>
</div>
</CardTitled>
</div>
</div>
</div>
</template>

View File

@@ -7,9 +7,7 @@ const selfhosted = useSelfhosted();
const items = [
{ label: 'General', slot: 'general', tab: 'general' },
{ label: 'Domains', slot: 'domains', tab: 'domains' },
{ label: 'Billing', slot: 'billing', tab: 'billing' },
{ label: 'Codes', slot: 'codes', tab: 'codes' },
{ label: 'Account', slot: 'account', tab: 'account' }
]
</script>
@@ -26,13 +24,13 @@ const items = [
<template #domains>
<SettingsData :key="refreshKey"></SettingsData>
</template>
<template #billing>
<!-- <template #billing>
<SettingsBilling v-if="!selfhosted" :key="refreshKey"></SettingsBilling>
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"
v-if="selfhosted">
Billing disabled in self-host mode
</div>
</template>
</template> -->
<template #codes>
<SettingsCodes v-if="!selfhosted" :key="refreshKey"></SettingsCodes>
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const selfhosted = useSelfhosted();
const items = [
{ label: 'Domains', slot: 'domains', tab: 'domains' },
{ label: 'IP Addresses', slot: 'ipaddresses', tab: 'ipaddresses' },
{ label: 'Bot traffic', slot: 'bots', tab: 'bots' },
// { label: 'Countries', slot: 'countries', tab: 'countries' },
// { label: 'Pages', slot: 'pages', tab: 'pages' },
]
</script>
<template>
<div class="lg:px-10 h-full lg:py-8 overflow-hidden hide-scrollbars">
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Shields </div>
<CustomTab :items="items" :route="true" class="mt-8">
<template #domains>
<ShieldsDomains></ShieldsDomains>
</template>
<template #ipaddresses>
<ShieldsAddresses></ShieldsAddresses>
</template>
<template #bots>
<ShieldsBots></ShieldsBots>
</template>
</CustomTab>
</div>
</template>

View File

@@ -5255,8 +5255,8 @@ packages:
peerDependencies:
vue: ^3.2.0
vue3-google-signin@2.0.1:
resolution: {integrity: sha512-vZTlVrG56JERtqQ+6YI8e92wqfhAMDyNONCsLgKKXxzCNCWEfSkZcAvz7COm9V4bvzmGsebZ8KC3ljol2qsIcg==}
vue3-google-signin@2.1.1:
resolution: {integrity: sha512-RwlwyeCv8+PZjK35C/UyJN4/9MH+Fsz4bJDO7IjtmRuOaTMrvmMmI6SP5qkJgD7w9MIKOCj5rYVFMAL7+NCM0A==}
peerDependencies:
vue: ^3
@@ -9868,7 +9868,7 @@ snapshots:
dependencies:
'@nuxt/kit': 3.11.2(rollup@4.18.0)
unimport: 3.7.2(rollup@4.18.0)
vue3-google-signin: 2.0.1(vue@3.4.27(typescript@5.4.2))
vue3-google-signin: 2.1.1(vue@3.4.27(typescript@5.4.2))
transitivePeerDependencies:
- rollup
- supports-color
@@ -11525,7 +11525,7 @@ snapshots:
vue-observe-visibility: 2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2))
vue-resize: 2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2))
vue3-google-signin@2.0.1(vue@3.4.27(typescript@5.4.2)):
vue3-google-signin@2.1.1(vue@3.4.27(typescript@5.4.2)):
dependencies:
vue: 3.4.27(typescript@5.4.2)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
dashboard/public/flamy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
dashboard/public/lit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -14,7 +14,7 @@ export async function getUserProjectFromId(project_id: string, user: AuthContext
const project = await ProjectModel.findById(project_id);
if (!project) return;
const [hasAccess, role] = await hasAccessToProject(user.id, project_id, project);
const [hasAccess, role] = await hasAccessToProject(user.id, project_id, user.user.email, project);
if (!hasAccess) return;
if (role === 'GUEST' && !allowGuest) return false;

View File

@@ -1,127 +0,0 @@
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { AIPlugin } from "../Plugin";
import { MAX_LOG_LIMIT_PERCENT } from "@data/broker/Limits";
import { ProjectModel } from "@schema/project/ProjectSchema";
import StripeService from "~/server/services/StripeService";
import { InvoiceData } from "~/server/api/pay/invoices";
export class AiBilling extends AIPlugin<[
'getBillingInfo',
'getLimits',
'getInvoices'
]> {
constructor() {
super({
'getInvoices': {
handler: async (data: { project_id: string }) => {
const project = await ProjectModel.findOne({ _id: data.project_id });
if (!project) return { error: 'Project not found' };
const invoices = await StripeService.getInvoices(project.customer_id);
if (!invoices) return [];
return invoices?.data.map(e => {
const result: InvoiceData = {
link: e.invoice_pdf || '',
id: e.number || '',
date: e.created * 1000,
status: e.status || 'NO_STATUS',
cost: e.amount_due
}
return result;
});
},
tool: {
type: 'function',
function: {
name: 'getInvoices',
description: 'Gets the invoices of the user project',
parameters: {}
}
}
},
'getBillingInfo': {
handler: async (data: { project_id: string }) => {
const project = await ProjectModel.findOne({ _id: data.project_id });
if (!project) return { error: 'Project not found' };
if (project.subscription_id === 'onetime') {
const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
if (!projectLimits) return { error: 'Limits not found' }
const result = {
premium: project.premium,
premium_type: project.premium_type,
billing_start_at: projectLimits.billing_start_at,
billing_expire_at: projectLimits.billing_expire_at,
limit: projectLimits.limit,
count: projectLimits.events + projectLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : ('One time payment')
}
return result;
}
const subscription = await StripeService.getSubscription(project.subscription_id);
const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
if (!projectLimits) return { error: 'Limits not found' }
const result = {
premium: project.premium,
premium_type: project.premium_type,
billing_start_at: projectLimits.billing_start_at,
billing_expire_at: projectLimits.billing_expire_at,
limit: projectLimits.limit,
count: projectLimits.events + projectLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : (subscription?.status ?? '?')
}
return result;
},
tool: {
type: 'function',
function: {
name: 'getBillingInfo',
description: 'Gets the informations about the billing of the user project, limits, count, subscription_status, is premium, premium type, billing start at, billing expire at',
parameters: {}
}
}
},
'getLimits': {
handler: async (data: { project_id: string }) => {
const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
if (!projectLimits) return { error: 'Project limits not found' };
const TOTAL_COUNT = projectLimits.events + projectLimits.visits;
const COUNT_LIMIT = projectLimits.limit;
return {
total: TOTAL_COUNT,
limit: COUNT_LIMIT,
limited: TOTAL_COUNT > COUNT_LIMIT * MAX_LOG_LIMIT_PERCENT,
percent: Math.round(100 / COUNT_LIMIT * TOTAL_COUNT)
}
},
tool: {
type: 'function',
function: {
name: 'getLimits',
description: 'Gets the informations about the limits of the user project',
parameters: {}
}
}
},
})
}
}
export const AiBillingInstance = new AiBilling();

View File

@@ -0,0 +1,15 @@
import { FeedbackModel } from '@schema/FeedbackSchema';
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { id } = await readBody(event);
await FeedbackModel.deleteOne({ _id: id });
});

View File

@@ -1,8 +1,8 @@
import { ProjectModel } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { UserLimitModel } from "@schema/UserLimitSchema";
import { UserModel } from "@schema/UserSchema";
import StripeService from '~/server/services/StripeService';
import { PremiumModel } from "~/shared/schema/PremiumSchema";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
@@ -13,18 +13,24 @@ export default defineEventHandler(async event => {
if (!project_id) return setResponseStatus(event, 400, 'ProjectId is required');
const project = await ProjectModel.findById(project_id);
const limits = await ProjectLimitModel.findOne({ project_id });
const limits = await UserLimitModel.findOne({ user_id: userData.id });
const counts = await ProjectCountModel.findOne({ project_id });
const user = await UserModel.findOne({ project_id });
const subscription =
project?.subscription_id ?
await StripeService.getSubscription(project.subscription_id) : 'NONE';
const premium = await PremiumModel.findOne({ user_id: userData.id });
const customer =
project?.customer_id ?
await StripeService.getCustomer(project.customer_id) : 'NONE';
// const subscription =
// premium?.subscription_id ?
// await StripeService.getSubscription(premium.subscription_id) : 'NONE';
return { project, limits, counts, user, subscription, customer }
// const customer =
// premium?.customer_id ?
// await StripeService.getCustomer(premium.customer_id) : 'NONE';
return {
project, limits, counts, user,
subscription: '',
customer: ''
}
});

View File

@@ -2,6 +2,18 @@ import { ProjectModel } from "@schema/project/ProjectSchema";
import { UserModel } from "@schema/UserSchema";
import { EventModel } from "@schema/metrics/EventSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
import { PremiumModel } from "~/shared/schema/PremiumSchema";
function addFieldsFromArray(data: { fieldName: string, projectedName: string, arrayName: string }[]) {
const content: Record<string, any> = {};
data.forEach(e => {
content[e.projectedName] = {
"$ifNull": [{ "$getField": { "field": e.fieldName, "input": { "$arrayElemAt": [`$${e.arrayName}`, 0] } } }, 0]
}
});
return content;
}
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
@@ -19,9 +31,33 @@ export default defineEventHandler(async event => {
}
const totalProjects = await ProjectModel.countDocuments({ ...matchQuery });
const premiumProjects = await ProjectModel.countDocuments({ ...matchQuery, premium: true });
const premiumProjects = await PremiumModel.countDocuments({ ...matchQuery, premium_type: { $ne: 0 } });
const deadProjects = await ProjectModel.countDocuments({ ...matchQuery });
const deadProjects = await ProjectModel.aggregate([
{ $match: matchQuery },
{
$lookup: {
from: 'project_counts',
localField: '_id',
foreignField: 'project_id',
as: 'counts'
}
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'counts', fieldName: 'counts', projectedName: 'counts' },
{ arrayName: 'counts', fieldName: 'updated_at', projectedName: 'updated_at' },
])
},
{
$match: {
updated_at: {
$lte: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)
}
}
},
{ $count: 'count' }
])
const totalUsers = await UserModel.countDocuments({ ...matchQuery });
@@ -30,7 +66,11 @@ export default defineEventHandler(async event => {
const totalEvents = await EventModel.countDocuments({ ...matchQuery });
return { totalProjects, premiumProjects, deadProjects, totalUsers, totalVisits, totalEvents }
return {
totalProjects, premiumProjects,
deadProjects: (deadProjects && deadProjects.length > 0 ? deadProjects[0].count : 0) as number,
totalUsers, totalVisits, totalEvents
}
});

View File

@@ -1,8 +1,11 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
import { TPremium } from "~/shared/schema/PremiumSchema";
import { TUser } from "~/shared/schema/UserSchema";
type ExtendedProject = {
limits: TProjectLimit[],
user: [TUser],
premium: [TPremium],
counts: [{
events: number,
visits: number,
@@ -59,9 +62,9 @@ export default defineEventHandler(async event => {
},
{
$lookup: {
from: "project_limits",
localField: "_id",
foreignField: "project_id",
from: "user_limits",
localField: "owner",
foreignField: "user_id",
as: "limits"
}
},
@@ -73,6 +76,22 @@ export default defineEventHandler(async event => {
as: "counts"
}
},
{
$lookup: {
from: "users",
localField: "owner",
foreignField: "_id",
as: "user"
}
},
{
$lookup: {
from: "premiums",
localField: "owner",
foreignField: "user_id",
as: "premium"
}
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'counts', fieldName: 'visits', projectedName: 'visits' },

View File

@@ -0,0 +1,121 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
import { TUser, UserModel } from "~/shared/schema/UserSchema";
import { TPremium } from "~/shared/schema/PremiumSchema";
import { Types } from "mongoose";
import { TAdminProject } from "./projects";
export type TAdminUserInfo = {
user: TUser,
projects: (TAdminProject & { domains: string[] })[],
premium: TPremium
}
async function getProjects(user_id: string) {
function addFieldsFromArray(data: { fieldName: string, projectedName: string, arrayName: string }[]) {
const content: Record<string, any> = {};
data.forEach(e => {
content[e.projectedName] = {
"$ifNull": [{ "$getField": { "field": e.fieldName, "input": { "$arrayElemAt": [`$${e.arrayName}`, 0] } } }, 0]
}
});
return content;
}
const projects = await ProjectModel.aggregate([
{
$match: { owner: new Types.ObjectId(user_id) }
},
{
$lookup: {
from: "user_limits",
localField: "owner",
foreignField: "user_id",
as: "limits"
}
},
{
$lookup: {
from: "project_counts",
localField: "_id",
foreignField: "project_id",
as: "counts"
}
},
{
$lookup: {
from: "premiums",
localField: "owner",
foreignField: "user_id",
as: "premium"
}
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'counts', fieldName: 'visits', projectedName: 'visits' },
{ arrayName: 'counts', fieldName: 'events', projectedName: 'events' },
{ arrayName: 'counts', fieldName: 'session', projectedName: 'session' },
{ arrayName: 'counts', fieldName: 'updated_at', projectedName: 'last_log_at' },
]),
},
{
$addFields: addFieldsFromArray([
{ arrayName: 'limits', fieldName: 'visits', projectedName: 'limit_visits' },
{ arrayName: 'limits', fieldName: 'events', projectedName: 'limit_events' },
{ arrayName: 'limits', fieldName: 'limit', projectedName: 'limit_max' },
{ arrayName: 'limits', fieldName: 'ai_messages', projectedName: 'limit_ai_messages' },
{ arrayName: 'limits', fieldName: 'ai_limit', projectedName: 'limit_ai_max' },
]),
},
{
$addFields: {
limit_total: {
$add: [
{ $ifNull: ["$limit_visits", 0] },
{ $ifNull: ["$limit_events", 0] }
]
},
}
},
{ $unset: 'counts' },
{ $unset: 'limits' },
]);
return projects as TAdminProject[];
}
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return;
if (!userData.user.roles.includes('ADMIN')) return;
const { user_id } = getQuery(event);
const result: any = {}
result.user = await UserModel.findOne({ _id: user_id });
result.projects = await getProjects(user_id as string);
const promises: any[] = [];
for (const project of result.projects) {
promises.push(new Promise<void>(async resolve => {
const domains = await VisitModel.aggregate([
{ $match: { project_id: (project as TAdminProject)._id } },
{ $group: { _id: '$website', } }
]);
project.domains = domains.map(e => e._id);
resolve();
}));
}
await Promise.all(promises);
return result as TAdminUserInfo;
});

View File

@@ -4,7 +4,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event);
const data = await getRequestData(event, [], ['AI']);
if (!data) return;
const { project_id } = data;

View File

@@ -1,10 +1,10 @@
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
export async function getAiChatRemainings(project_id: string) {
const limits = await ProjectLimitModel.findOne({ project_id })
import { UserLimitModel } from "@schema/UserLimitSchema";
export async function getAiChatRemainings(user_id: string) {
const limits = await UserLimitModel.findOne({ user_id })
if (!limits) return 0;
const chatsRemaining = limits.ai_limit - limits.ai_messages;
if (isNaN(chatsRemaining)) return 0;
return chatsRemaining;
}
@@ -13,8 +13,8 @@ export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['AI']);
if (!data) return;
const { pid } = data;
const { pid, user } = data;
const chatsRemaining = await getAiChatRemainings(pid);
const chatsRemaining = await getAiChatRemainings(user.id);
return chatsRemaining;
});

View File

@@ -1,6 +1,6 @@
import { sendMessageOnChat, updateChatStatus } from "~/server/services/AiService";
import { getAiChatRemainings } from "./chats_remaining";
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { UserLimitModel } from "@schema/UserLimitSchema";
@@ -8,16 +8,15 @@ export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['AI']);
if (!data) return;
const { pid } = data;
const { pid, user } = data;
const { text, chat_id, timeOffset } = await readBody(event);
if (!text) return setResponseStatus(event, 400, 'text parameter missing');
const chatsRemaining = await getAiChatRemainings(pid);
const chatsRemaining = await getAiChatRemainings(user.id);
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } });
await UserLimitModel.updateOne({ user_id: user.id }, { $inc: { ai_messages: 1 } });
const currentStatus: string[] = [];

View File

@@ -4,6 +4,7 @@ import { UserModel } from '@schema/UserSchema';
import { PasswordModel } from '@schema/PasswordSchema';
import { EmailService } from '@services/EmailService';
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
export default defineEventHandler(async event => {
@@ -13,15 +14,22 @@ export default defineEventHandler(async event => {
if (!data) return setResponseStatus(event, 400, 'Error decoding register_code');
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() });
await PasswordModel.updateOne({ email: data.email }, { password: data.password }, { upsert: true });
const user = await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
const [ok, error] = await PaymentServiceHelper.create_customer(user.id);
if (!ok) throw error;
setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('welcome', { target: data.email });
EmailServiceHelper.sendEmail(emailData);
});
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
return sendRedirect(event, `https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
} catch (ex) {
console.error(ex);
return setResponseStatus(event, 400, 'Error creating user');
}

View File

@@ -4,6 +4,7 @@ import { createUserJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import { EmailService } from '@services/EmailService';
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
@@ -38,7 +39,6 @@ export default defineEventHandler(async event => {
const user = await UserModel.findOne({ email: payload.email });
if (user) {
user.google_tokens = tokens as any;
await user.save();
return {
error: false,
@@ -46,21 +46,28 @@ export default defineEventHandler(async event => {
}
}
const newUser = new UserModel({
email: payload.email,
given_name: payload.given_name,
name: payload.name,
locale: payload.locale,
picture: payload.picture,
google_tokens: tokens,
created_at: Date.now()
});
const savedUser = await newUser.save();
const [ok, error] = await PaymentServiceHelper.create_customer(savedUser.id);
if (!ok) throw error;
setImmediate(() => {
if (!payload.email) return;
const emailData = EmailService.getEmailServerInfo('brevolist_add', { email: payload.email });
EmailServiceHelper.sendEmail(emailData);
});
setImmediate(() => {
console.log('SENDING WELCOME EMAIL TO', payload.email);
if (!payload.email) return;
const emailData = EmailService.getEmailServerInfo('welcome', { target: payload.email });
EmailServiceHelper.sendEmail(emailData);

View File

@@ -34,6 +34,11 @@ export default defineEventHandler(async event => {
await RegisterModel.create({ email, password: hashedPassword });
setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('brevolist_add', { email });
EmailServiceHelper.sendEmail(emailData);
});
setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('confirm', { target: email, link: `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}` });
EmailServiceHelper.sendEmail(emailData);

View File

@@ -23,7 +23,7 @@ export default defineEventHandler(async event => {
...result
]
const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id, pending: false });
const member = await TeamMemberModel.findOne({ project_id, $or: [{ user_id: user.id }, { email: user.user.email }], pending: false });
if (!member) return setResponseStatus(event, 400, 'Not a member');
if (!member.permission) return setResponseStatus(event, 400, 'No permission');

View File

@@ -11,7 +11,7 @@ export default defineEventHandler(async event => {
const project = await ProjectModel.findOne({ _id: project_id });
if (!project) return;
const [hasAccess] = await hasAccessToProject(user.id, project_id, project)
const [hasAccess] = await hasAccessToProject(user.id, project_id, user.user.email, project)
if (!hasAccess) return;
const query = getQuery(event);

View File

@@ -1,38 +1,38 @@
import { getPlanFromId } from "@data/PREMIUM";
import { getPlanFromId } from "@data/PLANS";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService';
// import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
// const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
// if (!data) return;
const { project, pid } = data;
// const { project, pid } = data;
const body = await readBody(event);
// const body = await readBody(event);
const { planId } = body;
// const { planId } = body;
const PLAN = getPlanFromId(planId);
// const PLAN = getPlanFromId(planId);
if (!PLAN) {
console.error('PLAN', planId, 'NOT EXIST');
return setResponseStatus(event, 400, 'Plan not exist');
}
// if (!PLAN) {
// console.error('PLAN', planId, 'NOT EXIST');
// return setResponseStatus(event, 400, 'Plan not exist');
// }
const intent = await StripeService.createOnetimePayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok',
pid,
project.customer_id
)
// const intent = await StripeService.createOnetimePayment(
// StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
// 'https://dashboard.litlyx.com/payment_ok',
// pid,
// project.customer_id
// )
if (!intent) {
console.error('Cannot create Intent', { plan: PLAN });
return setResponseStatus(event, 400, 'Cannot create intent');
}
// if (!intent) {
// console.error('Cannot create Intent', { plan: PLAN });
// return setResponseStatus(event, 400, 'Cannot create intent');
// }
return intent.url;
// return intent.url;
});

View File

@@ -1,18 +1,18 @@
import { getPlanFromId } from "@data/PREMIUM";
import StripeService from '~/server/services/StripeService';
import { getPlanFromId } from "@data/PLANS";
import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper";
// import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
const data = await getRequestData(event, [], []);
if (!data) return;
const { project, pid } = data;
const { project, pid, user } = data;
const body = await readBody(event);
const { planId } = body;
const PLAN = getPlanFromId(planId);
if (!PLAN) {
@@ -20,18 +20,13 @@ export default defineEventHandler(async event => {
return setResponseStatus(event, 400, 'Plan not exist');
}
const checkout = await StripeService.createPayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok',
pid,
project.customer_id
);
const [ok, res] = await PaymentServiceHelper.create_payment(user.id, PLAN.ID);
if (!checkout) {
if (!ok) {
console.error('Cannot create payment', { plan: PLAN });
return setResponseStatus(event, 400, 'Cannot create payment');
return setResponseStatus(event, 400, res.message ?? 'Cannot create payment');
}
return checkout.url;
return res.url;
});

View File

@@ -1,16 +1,18 @@
import StripeService from '~/server/services/StripeService';
import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
import { PremiumModel } from '~/shared/schema/PremiumSchema';
export default defineEventHandler(async event => {
const data = await getRequestData(event, []);
if (!data) return;
const { project } = data;
const premium = await PremiumModel.findOne({ user_id: data.user.id })
if (!premium) return;
const customer = await StripeService.getCustomer(project.customer_id);
if (customer?.deleted) return;
const [ok, customerInfoOrError] = await PaymentServiceHelper.customer_info(data.user.id);
if (!ok) throw customerInfoOrError;
return customer?.address;
return customerInfoOrError;
});

View File

@@ -1,7 +1,6 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { Redis } from "~/server/services/CacheService";
import StripeService from '~/server/services/StripeService';
import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper";
import { PremiumModel } from "~/shared/schema/PremiumSchema";
export type InvoiceData = {
date: number,
@@ -15,16 +14,19 @@ export default defineEventHandler(async event => {
const data = await getRequestData(event, []);
if (!data) return;
const { project, pid } = data;
if (!project.customer_id) return [];
return await Redis.useCache({ key: `invoices:${data.user.id}`, exp: 10 }, async () => {
return await Redis.useCache({ key: `invoices:${pid}`, exp: 10 }, async () => {
const premium = await PremiumModel.findOne({ user_id: data.user.id });
if (!premium) return [];
const invoices = await StripeService.getInvoices(project.customer_id);
if (!invoices) return [];
const [ok, invoicesOrError] = await PaymentServiceHelper.invoices_list(data.user.id);
if (!ok) {
console.error(invoicesOrError);
return [];
}
return invoices?.data.map(e => {
return invoicesOrError.invoices.map(e => {
const result: InvoiceData = {
link: e.invoice_pdf || '',
id: e.number || '',
@@ -35,7 +37,6 @@ export default defineEventHandler(async event => {
return result;
});
});
});

View File

@@ -1,17 +1,13 @@
import { getPlanFromId, PREMIUM_PLAN } from "@data/PREMIUM";
import { getPlanFromId, PREMIUM_PLAN } from "@data/PLANS";
import { canTryAppsumoCode, checkAppsumoCode, useAppsumoCode, useTryAppsumoCode } from "~/server/services/AppsumoService";
import StripeService from '~/server/services/StripeService';
import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper";
import { PremiumModel } from "~/shared/schema/PremiumSchema";
function getPlanToActivate(current_plan_id: number) {
if (current_plan_id === PREMIUM_PLAN.FREE.ID) {
return PREMIUM_PLAN.APPSUMO_INCUBATION;
}
// if (current_plan_id === PREMIUM_PLAN.INCUBATION.ID) {
// return PREMIUM_PLAN.APPSUMO_ACCELERATION;
// }
// if (current_plan_id === PREMIUM_PLAN.ACCELERATION.ID) {
// return PREMIUM_PLAN.APPSUMO_GROWTH;
// }
if (current_plan_id === PREMIUM_PLAN.APPSUMO_INCUBATION.ID) {
return PREMIUM_PLAN.APPSUMO_ACCELERATION;
}
@@ -38,12 +34,17 @@ export default defineEventHandler(async event => {
const valid = await checkAppsumoCode(code);
if (!valid) return setResponseStatus(event, 400, 'Code not valid');
const currentPlan = getPlanFromId(project.premium_type);
const currentPremiumData = await PremiumModel.findOne({ user_id: user.id });
if (!currentPremiumData) return setResponseStatus(event, 400, 'Error finding user');
const currentPlan = getPlanFromId(currentPremiumData.premium_type);
if (!currentPlan) return setResponseStatus(event, 400, 'Current plan not found');
const planToActivate = getPlanToActivate(currentPlan.ID);
if (!planToActivate) return setResponseStatus(event, 400, 'Cannot use code on current plan');
await StripeService.createSubscription(project.customer_id, planToActivate.ID);
const sub = await PaymentServiceHelper.create_subscription(user.id, planToActivate.TAG);
console.log(sub);
await useAppsumoCode(pid, code);

View File

@@ -1,19 +1,17 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService';
import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
import { PremiumModel } from '~/shared/schema/PremiumSchema';
export default defineEventHandler(async event => {
const data = await getRequestData(event, []);
if (!data) return;
const { project } = data;
if (!project.customer_id) return setResponseStatus(event, 400, 'Project has no customer_id');
const premium = await PremiumModel.findOne({ user_id: data.user.id })
if (!premium) return;
const body = await readBody(event);
const res = await StripeService.setCustomerInfo(project.customer_id, body);
return { ok: true, data: res }
return await PaymentServiceHelper.update_customer_info(data.user.id, body);
});

View File

@@ -1,268 +1,270 @@
import StripeService from '~/server/services/StripeService';
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 { UserModel } from '@schema/UserSchema';
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
// import StripeService from '~/server/services/StripeService';
// import type Event from 'stripe';
// import { ProjectModel } from '@schema/project/ProjectSchema';
// import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PLANS';
// import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
// import { EmailService } from '@services/EmailService'
// import { UserModel } from '@schema/UserSchema';
// import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
async function addSubscriptionToProject(project_id: string, plan: PREMIUM_DATA, subscription_id: string, current_period_start: number, current_period_end: number) {
// async function addSubscriptionToProject(project_id: string, plan: PREMIUM_DATA, subscription_id: string, current_period_start: number, current_period_end: number) {
await ProjectModel.updateOne({ _id: project_id }, {
premium: plan.ID != 0,
premium_type: plan.ID,
subscription_id,
premium_expire_at: current_period_end * 1000
});
// await ProjectModel.updateOne({ _id: project_id }, {
// premium: plan.ID != 0,
// premium_type: plan.ID,
// subscription_id,
// premium_expire_at: current_period_end * 1000
// });
await ProjectLimitModel.updateOne({ project_id }, {
events: 0,
visits: 0,
ai_messages: 0,
limit: plan.COUNT_LIMIT,
ai_limit: plan.AI_MESSAGE_LIMIT,
billing_start_at: current_period_start * 1000,
billing_expire_at: current_period_end * 1000,
}, { upsert: true })
// await ProjectLimitModel.updateOne({ project_id }, {
// events: 0,
// visits: 0,
// ai_messages: 0,
// limit: plan.COUNT_LIMIT,
// ai_limit: plan.AI_MESSAGE_LIMIT,
// billing_start_at: current_period_start * 1000,
// billing_expire_at: current_period_end * 1000,
// }, { upsert: true })
}
// }
async function onPaymentFailed(event: Event.InvoicePaymentFailedEvent) {
// async function onPaymentFailed(event: Event.InvoicePaymentFailedEvent) {
//TODO: Send emails
// //TODO: Send emails
if (event.data.object.attempt_count > 1) {
const customer_id = event.data.object.customer as string;
const project = await ProjectModel.findOne({ customer_id });
if (!project) return { error: 'CUSTOMER NOT EXIST' }
// if (event.data.object.attempt_count > 1) {
// const customer_id = event.data.object.customer as string;
// const project = await ProjectModel.findOne({ customer_id });
// if (!project) return { error: 'CUSTOMER NOT EXIST' }
const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
if (!allSubscriptions) return;
// const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
// if (!allSubscriptions) return;
for (const subscription of allSubscriptions.data) {
await StripeService.deleteSubscription(subscription.id);
}
// for (const subscription of allSubscriptions.data) {
// await StripeService.deleteSubscription(subscription.id);
// }
const freeSub = await StripeService.createFreeSubscription(customer_id);
if (!freeSub) return;
// const freeSub = await StripeService.createFreeSubscription(customer_id);
// if (!freeSub) return;
await addSubscriptionToProject(
project._id.toString(),
getPlanFromTag('FREE'),
freeSub.id,
freeSub.current_period_start,
freeSub.current_period_end
)
// await addSubscriptionToProject(
// project._id.toString(),
// getPlanFromTag('FREE'),
// freeSub.id,
// freeSub.current_period_start,
// freeSub.current_period_end
// )
return { ok: true };
}
// return { ok: true };
// }
}
// }
async function onPaymentOnetimeSuccess(event: Event.PaymentIntentSucceededEvent) {
const customer_id = event.data.object.customer as string;
const project = await ProjectModel.findOne({ customer_id });
if (!project) return { error: 'CUSTOMER NOT EXIST' }
// async function onPaymentOnetimeSuccess(event: Event.PaymentIntentSucceededEvent) {
// const customer_id = event.data.object.customer as string;
// const project = await ProjectModel.findOne({ customer_id });
// if (!project) return { error: 'CUSTOMER NOT EXIST' }
if (event.data.object.status === 'succeeded') {
// if (event.data.object.status === 'succeeded') {
const PLAN = getPlanFromPrice(event.data.object.metadata.price, StripeService.testMode || false);
if (!PLAN) return { error: 'Plan not found' }
const dummyPlan = PLAN.ID + 3000;
// const PLAN = getPlanFromPrice(event.data.object.metadata.price, StripeService.testMode || false);
// if (!PLAN) return { error: 'Plan not found' }
// const dummyPlan = PLAN.ID + 3000;
const subscription = await StripeService.createOneTimeSubscriptionDummy(customer_id, dummyPlan);
if (!subscription) return { error: 'Error creating subscription' }
// const subscription = await StripeService.createOneTimeSubscriptionDummy(customer_id, dummyPlan);
// if (!subscription) return { error: 'Error creating subscription' }
const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
if (!allSubscriptions) return;
for (const subscription of allSubscriptions.data) {
if (subscription.id === subscription.id) continue;
await StripeService.deleteSubscription(subscription.id);
}
// const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
// if (!allSubscriptions) return;
// for (const subscription of allSubscriptions.data) {
// if (subscription.id === subscription.id) continue;
// await StripeService.deleteSubscription(subscription.id);
// }
await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end)
// await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end)
const user = await UserModel.findOne({ _id: project.owner });
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
// const user = await UserModel.findOne({ _id: project.owner });
// if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
setTimeout(() => {
const emailData = EmailService.getEmailServerInfo('purchase', { target: user.email, projectName: project.name });
EmailServiceHelper.sendEmail(emailData);
}, 1);
// setTimeout(() => {
// const emailData = EmailService.getEmailServerInfo('purchase', { target: user.email, projectName: project.name });
// EmailServiceHelper.sendEmail(emailData);
// }, 1);
return { ok: true };
}
// return { ok: true };
// }
return { received: true, warn: 'object status not succeeded' }
}
// return { received: true, warn: 'object status not succeeded' }
// }
async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
// async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
const customer_id = event.data.object.customer as string;
const project = await ProjectModel.findOne({ customer_id });
if (!project) return { error: 'CUSTOMER NOT EXIST' }
// const customer_id = event.data.object.customer as string;
// const project = await ProjectModel.findOne({ customer_id });
// if (!project) return { error: 'CUSTOMER NOT EXIST' }
if (event.data.object.status === 'paid') {
// if (event.data.object.status === 'paid') {
const subscription_id = event.data.object.subscription as string;
// const subscription_id = event.data.object.subscription as string;
const isNewSubscription = project.subscription_id != subscription_id;
// const isNewSubscription = project.subscription_id != subscription_id;
const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
if (!allSubscriptions) return;
// const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
// if (!allSubscriptions) return;
const currentSubscription = allSubscriptions.data.find(e => e.id === subscription_id);
if (!currentSubscription) return { error: 'SUBSCRIPTION NOT EXIST' }
// const currentSubscription = allSubscriptions.data.find(e => e.id === subscription_id);
// if (!currentSubscription) return { error: 'SUBSCRIPTION NOT EXIST' }
if (currentSubscription.status !== 'active') return { error: 'SUBSCRIPTION NOT ACTIVE' }
// if (currentSubscription.status !== 'active') return { error: 'SUBSCRIPTION NOT ACTIVE' }
for (const subscription of allSubscriptions.data) {
if (subscription.id === subscription_id) continue;
await StripeService.deleteSubscription(subscription.id);
}
// for (const subscription of allSubscriptions.data) {
// if (subscription.id === subscription_id) continue;
// await StripeService.deleteSubscription(subscription.id);
// }
const price = currentSubscription.items.data[0].price.id;
if (!price) return { error: 'Price not found' }
// const price = currentSubscription.items.data[0].price.id;
// if (!price) return { error: 'Price not found' }
const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
if (!PLAN) return { error: `Plan not found. Price: ${price}. TestMode: ${StripeService.testMode}` }
// const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
// if (!PLAN) return { error: `Plan not found. Price: ${price}. TestMode: ${StripeService.testMode}` }
await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
// await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
const user = await UserModel.findOne({ _id: project.owner });
if (!user) return { ok: false, error: 'USER NOT EXIST FOR PROJECT' + project.id }
// 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;
const emailData = EmailService.getEmailServerInfo('purchase', { target: user.email, projectName: project.name });
EmailServiceHelper.sendEmail(emailData);
}, 1);
// setTimeout(() => {
// if (PLAN.ID == 0) return;
// const emailData = EmailService.getEmailServerInfo('purchase', { target: user.email, projectName: project.name });
// EmailServiceHelper.sendEmail(emailData);
// }, 1);
return { ok: true };
// return { ok: true };
}
return { received: true, warn: 'payment status not paid' }
}
// }
// return { received: true, warn: 'payment status not paid' }
// }
async function onSubscriptionCreated(event: Event.CustomerSubscriptionCreatedEvent) {
// async function onSubscriptionCreated(event: Event.CustomerSubscriptionCreatedEvent) {
// const project = await ProjectModel.findOne({ customer_id: event.data.object.customer });
// if (!project) return { error: 'CUSTOMER NOT EXIST' }
// // const project = await ProjectModel.findOne({ customer_id: event.data.object.customer });
// // if (!project) return { error: 'CUSTOMER NOT EXIST' }
// const price = event.data.object.items.data[0].price.id;
// if (!price) return { error: 'Price not found' }
// // const price = event.data.object.items.data[0].price.id;
// // if (!price) return { error: 'Price not found' }
// const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
// if (!PLAN) return { error: 'Plan not found' }
// // const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
// // if (!PLAN) return { error: 'Plan not found' }
// if (project.subscription_id != event.data.object.id) {
// try {
// await StripeService.deleteSubscription(project.subscription_id);
// } catch (ex) { }
// }
// // if (project.subscription_id != event.data.object.id) {
// // try {
// // await StripeService.deleteSubscription(project.subscription_id);
// // } catch (ex) { }
// // }
// if (event.data.object.status === 'active') {
// await addSubscriptionToProject(
// project._id.toString(),
// PLAN,
// event.data.object.id,
// event.data.object.current_period_start,
// event.data.object.current_period_end
// );
// }
// // if (event.data.object.status === 'active') {
// // await addSubscriptionToProject(
// // project._id.toString(),
// // PLAN,
// // event.data.object.id,
// // event.data.object.current_period_start,
// // event.data.object.current_period_end
// // );
// // }
return { ok: true }
}
// return { ok: true }
// }
async function onSubscriptionDeleted(event: Event.CustomerSubscriptionDeletedEvent) {
// async function onSubscriptionDeleted(event: Event.CustomerSubscriptionDeletedEvent) {
// const project = await ProjectModel.findOne({
// customer_id: event.data.object.customer,
// subscription_id: event.data.object.id
// });
// // const project = await ProjectModel.findOne({
// // customer_id: event.data.object.customer,
// // subscription_id: event.data.object.id
// // });
// if (!project) return { error: 'PROJECT WITH SUBSCRIPTION NOT FOUND' }
// // if (!project) return { error: 'PROJECT WITH SUBSCRIPTION NOT FOUND' }
// const targetCustomer = await StripeService.getCustomer(project.customer_id);
// // const targetCustomer = await StripeService.getCustomer(project.customer_id);
// let customer: Event.Customer;
// // let customer: Event.Customer;
// if (!targetCustomer.deleted) {
// customer = targetCustomer;
// } else {
// const user = await UserModel.findById(project._id, { email: 1 });
// if (!user) return { error: 'User not found' }
// const newCustomer = await StripeService.createCustomer(user.email);
// customer = newCustomer;
// }
// // if (!targetCustomer.deleted) {
// // customer = targetCustomer;
// // } else {
// // const user = await UserModel.findById(project._id, { email: 1 });
// // if (!user) return { error: 'User not found' }
// // const newCustomer = await StripeService.createCustomer(user.email);
// // customer = newCustomer;
// // }
// await StripeService.createFreeSubscription(customer.id);
// // await StripeService.createFreeSubscription(customer.id);
return { received: true }
}
// return { received: true }
// }
async function onSubscriptionUpdated(event: Event.CustomerSubscriptionUpdatedEvent) {
// async function onSubscriptionUpdated(event: Event.CustomerSubscriptionUpdatedEvent) {
// const project = await ProjectModel.findOne({
// customer_id: event.data.object.customer,
// });
// // const project = await ProjectModel.findOne({
// // customer_id: event.data.object.customer,
// // });
// if (!project) return { error: 'Project not found' }
// // if (!project) return { error: 'Project not found' }
// const price = event.data.object.items.data[0].price.id;
// if (!price) return { error: 'Price not found' }
// // const price = event.data.object.items.data[0].price.id;
// // if (!price) return { error: 'Price not found' }
// const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
// if (!PLAN) return { error: 'Plan not found' }
// // const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
// // if (!PLAN) return { error: 'Plan not found' }
// if (event.data.object.status === 'active') {
// await addSubscriptionToProject(
// project._id.toString(),
// PLAN,
// event.data.object.id,
// event.data.object.current_period_start,
// event.data.object.current_period_end
// );
// }
// // if (event.data.object.status === 'active') {
// // await addSubscriptionToProject(
// // project._id.toString(),
// // PLAN,
// // event.data.object.id,
// // event.data.object.current_period_start,
// // event.data.object.current_period_end
// // );
// // }
return { ok: true }
}
// return { ok: true }
// }
export default defineEventHandler(async event => {
const body = await readRawBody(event);
const signature = getHeader(event, 'stripe-signature') || '';
const eventData = StripeService.parseWebhook(body, signature);
if (!eventData) return;
return { error: 'NOT IMPLEMENTED ANYMORE' }
// const body = await readRawBody(event);
// const signature = getHeader(event, 'stripe-signature') || '';
// console.log('WEBHOOK FIRED', eventData.type);
// const eventData = StripeService.parseWebhook(body, signature);
// if (!eventData) return;
if (eventData.type === 'invoice.paid') return await onPaymentSuccess(eventData);
if (eventData.type === 'payment_intent.succeeded') return await onPaymentOnetimeSuccess(eventData);
if (eventData.type === 'invoice.payment_failed') return await onPaymentFailed(eventData);
if (eventData.type === 'customer.subscription.deleted') return await onSubscriptionDeleted(eventData);
if (eventData.type === 'customer.subscription.created') return await onSubscriptionCreated(eventData);
if (eventData.type === 'customer.subscription.updated') return await onSubscriptionUpdated(eventData);
// // console.log('WEBHOOK FIRED', eventData.type);
return { received: true }
// if (eventData.type === 'invoice.paid') return await onPaymentSuccess(eventData);
// if (eventData.type === 'payment_intent.succeeded') return await onPaymentOnetimeSuccess(eventData);
// if (eventData.type === 'invoice.payment_failed') return await onPaymentFailed(eventData);
// if (eventData.type === 'customer.subscription.deleted') return await onSubscriptionDeleted(eventData);
// if (eventData.type === 'customer.subscription.created') return await onSubscriptionCreated(eventData);
// if (eventData.type === 'customer.subscription.updated') return await onSubscriptionUpdated(eventData);
// return { received: true }
});

View File

@@ -1,8 +1,5 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { UserSettingsModel } from "@schema/UserSettings";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
@@ -21,68 +18,12 @@ export default defineEventHandler(async event => {
const existingUserProjects = await ProjectModel.countDocuments({ owner: userData.id });
if (existingUserProjects >= maxProjects) return setResponseStatus(event, 400, 'Already have max number of projects');
if (StripeService.isDisabled()) {
const project = await ProjectModel.create({ owner: userData.id, name: newProjectName });
const project = await ProjectModel.create({
owner: userData.id,
name: newProjectName,
premium: false,
premium_type: 0,
customer_id: 'DISABLED_MODE',
subscription_id: "DISABLED_MODE",
premium_expire_at: new Date(3000, 1, 1)
});
await ProjectCountModel.create({
project_id: project._id,
events: 0,
visits: 0,
sessions: 0
});
await ProjectLimitModel.updateOne({ project_id: project._id }, {
events: 0,
visits: 0,
ai_messages: 0,
limit: 10_000_000,
ai_limit: 1_000_000,
billing_start_at: Date.now(),
billing_expire_at: new Date(3000, 1, 1)
}, { upsert: true })
await ProjectCountModel.create({ project_id: project._id, events: 0, visits: 0, sessions: 0 });
return project.toJSON() as TProject;
} else {
const customer = await StripeService.createCustomer(userData.user.email);
if (!customer) return setResponseStatus(event, 400, 'Error creating customer');
const subscription = await StripeService.createFreeSubscription(customer.id);
if (!subscription) return setResponseStatus(event, 400, 'Error creating subscription');
const project = await ProjectModel.create({
owner: userData.id,
name: newProjectName,
premium: false,
premium_type: 0,
customer_id: customer.id,
subscription_id: subscription.id,
premium_expire_at: subscription.current_period_end * 1000
});
await ProjectCountModel.create({
project_id: project._id,
events: 0,
visits: 0,
sessions: 0
});
return project.toJSON() as TProject;
}
});

View File

@@ -1,14 +1,11 @@
import { ProjectModel } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { SessionModel } from "@schema/metrics/SessionSchema";
import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
import StripeService from '~/server/services/StripeService';
import { AiChatModel } from "@schema/ai/AiChatSchema";
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
const data = await getRequestData(event, [], []);
if (!data) return;
const { project, user, project_id } = data;
@@ -16,25 +13,16 @@ export default defineEventHandler(async event => {
const projects = await ProjectModel.countDocuments({ owner: user.id });
if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project');
if (project.premium === true) return setResponseStatus(event, 400, 'Cannot delete premium project');
if (project.customer_id) {
await StripeService.deleteCustomer(project.customer_id);
}
const projectDeletation = ProjectModel.deleteOne({ _id: project_id });
const countDeletation = ProjectCountModel.deleteMany({ project_id });
const limitdeletation = ProjectLimitModel.deleteMany({ project_id });
const sessionsDeletation = SessionModel.deleteMany({ project_id });
const notifiesDeletation = LimitNotifyModel.deleteMany({ project_id });
const aiChatsDeletation = AiChatModel.deleteMany({ project_id });
const results = await Promise.all([
projectDeletation,
countDeletation,
limitdeletation,
sessionsDeletation,
notifiesDeletation,
aiChatsDeletation
])

View File

@@ -1,24 +1,14 @@
import { UserModel } from "@schema/UserSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
import { EventModel } from "~/shared/schema/metrics/EventSchema";
const { SELFHOSTED } = useRuntimeConfig();
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false });
const data = await getRequestData(event, [], []);
if (!data) return;
const { project, project_id, user } = data;
if (SELFHOSTED.toString() !== 'TRUE' && SELFHOSTED.toString() !== 'true') {
const PREMIUM_TYPE = project.premium_type;
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
}
const { project_id } = data;
const { mode, slice } = getQuery(event);

View File

@@ -5,7 +5,18 @@ import { PassThrough } from 'node:stream';
import { ProjectModel } from "@schema/project/ProjectSchema";
import { VisitModel } from '@schema/metrics/VisitSchema';
import { EventModel } from '@schema/metrics/EventSchema';
import { ReportCustomizationModel, TReportCustomization } from '~/shared/schema/report/ReportCustomizationSchema';
import { getInstance } from '~/server/services/AiService';
import { zodResponseFormat } from 'openai/helpers/zod';
import z from 'zod';
const ZPromptResponse = z.object({
report: z.string({ description: 'Short, user-facing report summarizing website analytics data. Should be professional but slightly discursive, not just a list of stats, feel like a human summary — similar to an executive update. Highlight key numbers and insights (like visits, top countries, referrers). Use just text, no markdown. Max 620 chars.' }),
insights: z.string({ description: 'Growth hacker, product expert and marketing expert. Simple and effective actionable insights. Max 3. Short.' }).array()
})
type PDFGenerationData = {
projectName: string,
@@ -17,8 +28,9 @@ type PDFGenerationData = {
topDevice: string,
topCountries: string[],
topReferrers: string[],
avgGrowthText: string,
customization?: any,
naturalText: string,
insights: string[]
}
function formatNumberK(value: string | number, decimals: number = 1) {
@@ -36,15 +48,42 @@ const resourcePath = process.env.MODE === 'TEST' ? './public/pdf/' : './.output/
function createPdf(data: PDFGenerationData) {
const pdf = new pdfkit({ size: 'A4', margins: { top: 50, bottom: 50, left: 50, right: 50 }, });
pdf.fillColor('#ffffff').rect(0, 0, pdf.page.width, pdf.page.height).fill('#000000');
const pdf = new pdfkit({
size: 'A4',
margins: {
top: 30, bottom: 30, left: 50, right: 50
},
});
pdf.font(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(16).fillColor('#ffffff');
let bgColor = '#0A0A0A';
let textColor = '#FFFFFF';
let logo = data.customization?.logo ?? resourcePath + 'pdf_images/logo.png'
pdf.text(`Project name: ${data.projectName}`, { align: 'left' }).moveDown(LINE_SPACING);
if (data.customization?.bg) {
bgColor = data.customization.bg === 'white' ? '#FFFFFF' : '#0A0A0A';
textColor = data.customization.bg === 'white' ? '#000000' : '#FFFFFF';
}
pdf.fillColor(bgColor).rect(0, 0, pdf.page.width, pdf.page.height).fill(bgColor);
pdf.font(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(16).fillColor(textColor);
pdf.text(`Report for: ${data.projectName} project`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.moveDown(LINE_SPACING)
pdf.font(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(13).fillColor(textColor);
pdf.text(`Timeframe name: ${data.snapshotName}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf').fontSize(12).fillColor('#ffffff')
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf').fontSize(12).fillColor(textColor)
pdf.text(`${data.naturalText}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.moveDown(LINE_SPACING)
pdf.font(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(13).fillColor(textColor);
pdf.text(`Plain metrics:`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf').fontSize(12).fillColor(textColor)
pdf.text(`Total visits: ${data.totalVisits}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.text(`Average visits per day: ${data.avgVisitsDay}`, { align: 'left' }).moveDown(LINE_SPACING);
@@ -52,37 +91,37 @@ function createPdf(data: PDFGenerationData) {
pdf.text(`Top domain: ${data.topDomain}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.text(`Top device: ${data.topDevice}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.text('Top 3 countries:', { align: 'left' }).moveDown(LINE_SPACING);
data.topCountries.forEach((country: any) => {
pdf.text(`${country}`, { align: 'left' }).moveDown(LINE_SPACING);
});
pdf.text(`Top 3 countries: ${data.topCountries.join(', ')}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.text('Top 3 best acquisition channels (referrers):', { align: 'left' }).moveDown(LINE_SPACING);
data.topReferrers.forEach((channel: any) => {
pdf.text(`${channel}`, { align: 'left' }).moveDown(LINE_SPACING);
});
pdf.text(`Top 3 best acquisition channels (referrers): ${data.topReferrers.join(', ')}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.text('Average growth:', { align: 'left' }).moveDown(LINE_SPACING);
pdf.text(`${data.avgGrowthText}`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.moveDown(LINE_SPACING)
pdf.font(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(13).fillColor(textColor);
pdf.text(`Actionable insights:`, { align: 'left' }).moveDown(LINE_SPACING);
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf').fontSize(12).fillColor(textColor)
pdf.font(resourcePath + 'pdf_fonts/Poppins-Italic.ttf')
.text('This gives you an idea of the average growth your website is experiencing over time.', { align: 'left' })
.moveDown(LINE_SPACING);
for (let i = 0; i < data.insights.length; i++) {
pdf.text(`${data.insights[i]}`, { align: 'left' }).moveDown(LINE_SPACING);
}
pdf.font(resourcePath + 'pdf_fonts/Poppins-Regular.ttf')
.fontSize(10)
.fillColor('#ffffff')
.text('Created with Litlyx.com', 50, 760, { align: 'center' });
.fillColor(textColor)
.text(`Created with Litlyx.com, ${new Date().toLocaleDateString('en-US')}`, 50, 780, { align: 'left' });
pdf.image(logo, 465, 695, { width: 85 });
pdf.image(resourcePath + 'pdf_images/logo.png', 460, 700, { width: 100 });
pdf.end();
return pdf;
}
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: true, requireRange: false });
const data = await getRequestData(event, [], []);
if (!data) return;
const userData = getRequestUser(event);
@@ -147,18 +186,38 @@ export default defineEventHandler(async event => {
{ $limit: 3 }
]);
const pdf = createPdf({
const customization = await ReportCustomizationModel.findOne({ project_id: project._id });
const textData: Omit<PDFGenerationData, 'naturalText' | 'insights' | 'customization'> = {
projectName: project.name,
snapshotName: snapshotHeader || 'NO_NAME',
totalVisits: formatNumberK(visitsCount),
avgVisitsDay: formatNumberK(avgVisitDay()) + '/day',
totalEvents: formatNumberK(eventsCount),
avgGrowthText: 'Insufficient Data (Requires at least 2 months of tracking)',
topDevice: topDevice,
topDomain: topDomain,
topCountries: topCountries.map(e => e._id),
topReferrers: topReferrers.map(e => e._id)
});
topReferrers: topReferrers.map(e => e._id),
}
const openai = getInstance();
const res = await openai.chat.completions.create({
messages: [
{
role: 'user', content: `${JSON.stringify(textData)}`,
}
],
response_format: zodResponseFormat(ZPromptResponse, 'response'),
model: 'gpt-4o-mini'
})
const resObject = JSON.parse(res.choices[0].message.content ?? '{}');
const pdf = createPdf({ ...textData, naturalText: resObject.report, insights: resObject.insights, customization: customization?.toJSON() as TReportCustomization, });
const passThrough = new PassThrough();
pdf.pipe(passThrough);

View File

@@ -1,17 +1,15 @@
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { UserLimitModel } from "@schema/UserLimitSchema";
import { MAX_LOG_LIMIT_PERCENT } from '@data/broker/Limits';
export default defineEventHandler(async event => {
const data = await getRequestDataOld(event);
const data = await getRequestData(event, [], []);
if (!data) return;
const { project_id } = data;
const { user } = data;
const projectLimits = await ProjectLimitModel.findOne({ project_id });
const projectLimits = await UserLimitModel.findOne({ user_id: user.id });
if (!projectLimits) return;
const TOTAL_COUNT = projectLimits.events + projectLimits.visits;

View File

@@ -7,7 +7,13 @@ export default defineEventHandler(async event => {
if (!userData?.logged) return [];
const members = await TeamMemberModel.find({ user_id: userData.id, pending: false });
const members = await TeamMemberModel.find({
$or: [
{ user_id: userData.id },
{ email: userData.user.email }
],
pending: false
});
const projects: TProject[] = [];

View File

@@ -12,7 +12,12 @@ export default defineEventHandler(async event => {
console.log({ project_id, user_id: data.user.id });
const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id });
const member = await TeamMemberModel.findOne({
project_id, $or: [
{ user_id: data.user.id },
{ email: data.user.user.email }
]
});
if (!member) return setResponseStatus(event, 400, 'member not found');
member.pending = false;

View File

@@ -16,6 +16,7 @@ export default defineEventHandler(async event => {
if (!user) return setResponseStatus(event, 400, 'Email not found');
await TeamMemberModel.deleteOne({ project_id, user_id: user.id });
await TeamMemberModel.deleteOne({ project_id, email: email });
return { ok: true }

View File

@@ -42,8 +42,15 @@ export default defineEventHandler(async event => {
})
for (const member of members) {
const userMember = member.user_id ? await UserModel.findById(member.user_id) : await UserModel.findOne({ email: member.email });
if (!userMember) continue;
let userMember;
if (member.user_id) {
userMember = await UserModel.findById(member.user_id);
} else {
userMember = await UserModel.findOne({ email: member.email });
}
const permission: TPermission = {
webAnalytics: member.permission?.webAnalytics || false,
@@ -54,11 +61,11 @@ export default defineEventHandler(async event => {
result.push({
id: member.id,
email: userMember.email,
name: userMember.name,
email: userMember?.email || member.email || 'NO_EMAIL',
name: userMember?.name || 'NO_NAME',
role: member.role,
pending: member.pending,
me: user.id === userMember.id,
me: user.id === (userMember?.id || member.user_id || 'NO_ID'),
permission
})
}

View File

@@ -19,13 +19,18 @@ export default defineEventHandler(async event => {
webAnalytics: true
}
const member = await TeamMemberModel.findOne({ project_id, user_id: user.id });
const member = await TeamMemberModel.findOne({
project_id,
$or: [
{ user_id: user.id }, { email: user.user.email }
]
});
if (!member) return {
ai: true,
domains: ['All domains'],
events: true,
webAnalytics: true
ai: false,
domains: [],
events: false,
webAnalytics: false
}
return {

View File

@@ -1,45 +1,44 @@
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import StripeService from '~/server/services/StripeService';
import { UserLimitModel } from "@schema/UserLimitSchema";
import { PremiumModel } from "~/shared/schema/PremiumSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, []);
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const { project, project_id } = data;
const premium = await PremiumModel.findOne({ user_id: data.user.id });
if (!premium) return;
if (project.subscription_id === 'onetime') {
if (premium.subscription_id === 'onetime') {
const projectLimits = await ProjectLimitModel.findOne({ project_id });
if (!projectLimits) return setResponseStatus(event, 400, 'Project limits not found');
const userLimits = await UserLimitModel.findOne({ user_id: data.user.id });
if (!userLimits) return setResponseStatus(event, 400, 'User limits not found');
const result = {
premium: project.premium,
premium_type: project.premium_type,
billing_start_at: projectLimits.billing_start_at,
billing_expire_at: projectLimits.billing_expire_at,
limit: projectLimits.limit,
count: projectLimits.events + projectLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : ('One time payment')
premium: premium.premium_type > 0,
premium_type: premium.premium_type,
billing_start_at: userLimits.billing_start_at,
billing_expire_at: userLimits.billing_expire_at,
limit: userLimits.limit,
count: userLimits.events + userLimits.visits,
subscription_status: 'One time'
}
return result;
}
const subscription = await StripeService.getSubscription(project.subscription_id);
const projectLimits = await ProjectLimitModel.findOne({ project_id });
if (!projectLimits) return setResponseStatus(event, 400, 'Project limits not found');
const userLimits = await UserLimitModel.findOne({ user_id: data.user.id });
if (!userLimits) return setResponseStatus(event, 400, 'User limits not found');
const result = {
premium: project.premium,
premium_type: project.premium_type,
billing_start_at: projectLimits.billing_start_at,
billing_expire_at: projectLimits.billing_expire_at,
limit: projectLimits.limit,
count: projectLimits.events + projectLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : (subscription?.status ?? '?')
premium: premium.premium_type > 0,
premium_type: premium.premium_type,
billing_start_at: userLimits.billing_start_at,
billing_expire_at: userLimits.billing_expire_at,
limit: userLimits.limit,
count: userLimits.events + userLimits.visits,
subscription_status: ''
}
return result;

View File

@@ -0,0 +1,20 @@
import { ReportCustomizationModel, TReportCustomization } from "~/shared/schema/report/ReportCustomizationSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, []);
if (!data) return;
const customization = await ReportCustomizationModel.findOne({ project_id: data.project_id });
if (!customization) return {
_id: '' as any,
project_id: data.project_id.toString() as any,
bg: 'black',
logo: undefined,
text: 'white'
} as TReportCustomization;
return customization.toJSON() as TReportCustomization;
});

View File

@@ -0,0 +1,32 @@
import z from 'zod';
import { PremiumModel } from '~/shared/schema/PremiumSchema';
import { ReportCustomizationModel } from "~/shared/schema/report/ReportCustomizationSchema";
const ZUpdateCustomizationBody = z.object({
logo: z.string().optional(),
bg: z.enum(['black', 'white'])
})
export default defineEventHandler(async event => {
const data = await getRequestData(event, []);
if (!data) return;
const premium = await PremiumModel.findOne({ user_id: data.user.id });
if (!premium) return createError({ status: 400, message: 'Not premium' });
if (premium.premium_type == 0) return createError({ status: 400, message: 'Not premium' });
const body = await readBody(event);
const bodyData = ZUpdateCustomizationBody.parse(body);
await ReportCustomizationModel.updateOne({ project_id: data.project_id }, {
logo: bodyData.logo,
bg: bodyData.bg,
text: bodyData.bg === 'white' ? 'black' : 'white'
}, { upsert: true });
return { ok: true }
});

View File

@@ -0,0 +1,9 @@
import { BotTrafficOptionModel } from "~/shared/schema/shields/BotTrafficOptionSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const result = await BotTrafficOptionModel.findOne({ project_id: data.project_id });
if (!result) return { block: false };
return { block: result.block }
});

View File

@@ -0,0 +1,14 @@
import { BotTrafficOptionModel } from "~/shared/schema/shields/BotTrafficOptionSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const body = await readBody(event);
const { block } = body;
if (block != true && block != false)
return setResponseStatus(event, 400, 'block is required and must be true or false');
const result = await BotTrafficOptionModel.updateOne({ project_id: data.project_id }, { block }, { upsert: true });
return { ok: result.acknowledged };
});

View File

@@ -0,0 +1,11 @@
import { CountryBlacklistModel } from "~/shared/schema/shields/CountryBlacklistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const body = await readBody(event);
const { country, description } = body;
if (country.trim().length == 0) return setResponseStatus(event, 400, 'Country is required');
const result = await CountryBlacklistModel.updateOne({ project_id: data.project_id, country }, { description }, { upsert: true });
return { ok: result.acknowledged };
});

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