Compare commits
23 Commits
refactorin
...
payments
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1917b74c32 | ||
|
|
b709ad285a | ||
|
|
6a157b81c9 | ||
|
|
82b0f6aac4 | ||
|
|
407c84f59c | ||
|
|
e8f8600df4 | ||
|
|
eb954cac6c | ||
|
|
a9bbc58ad1 | ||
|
|
f631c29fb2 | ||
|
|
946f9d4d32 | ||
|
|
1d5dad44fa | ||
|
|
1187cafd07 | ||
|
|
475512711b | ||
|
|
8f7f89e0bd | ||
|
|
4d51676a2e | ||
|
|
72ceb7971d | ||
|
|
10d4a9f1bc | ||
|
|
70c15238a0 | ||
|
|
7e093251fa | ||
|
|
7658dbe85c | ||
|
|
1f9ef5d18c | ||
|
|
94a28b31d3 | ||
|
|
87c9aca5c4 |
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"github.copilot.enable": {
|
||||
"*": false,
|
||||
"plaintext": false,
|
||||
"markdown": true,
|
||||
"scminput": false
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
notifications: {
|
||||
position: 'top-0 bottom-[unset]'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -69,6 +69,7 @@ const { drawerVisible, hideDrawer, drawerClasses } = useDrawer();
|
||||
|
||||
|
||||
<UModals />
|
||||
<UNotifications />
|
||||
|
||||
<LazyOnboarding> </LazyOnboarding>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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 [];
|
||||
@@ -26,7 +42,7 @@ const durationsDatasets = computed(() => {
|
||||
|
||||
datasets.push({
|
||||
points: consumerDurations.map((e: any) => {
|
||||
return 1000 / parseInt(e[1])
|
||||
return 1000 / parseInt(e[1])
|
||||
}),
|
||||
color: colors[i],
|
||||
chartType: 'line',
|
||||
@@ -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>
|
||||
|
||||
@@ -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,11 +23,19 @@ 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 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>
|
||||
<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 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>
|
||||
{{ feedback.text }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
35
dashboard/components/admin/dialog/UserDetails.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -7,11 +7,7 @@ function goToUpgrade() {
|
||||
showDrawer('PRICING');
|
||||
}
|
||||
|
||||
const { project } = useProject()
|
||||
|
||||
const isPremium = computed(() => {
|
||||
return project.value?.premium ?? false;
|
||||
});
|
||||
const { isPremium } = useLoggedUser()
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -70,27 +70,27 @@ const avgBouncingRate = computed(() => {
|
||||
|
||||
function weightedAverage(data: number[]): number {
|
||||
if (data.length === 0) return 0;
|
||||
|
||||
|
||||
// Compute median
|
||||
const sortedData = [...data].sort((a, b) => a - b);
|
||||
const middle = Math.floor(sortedData.length / 2);
|
||||
const median = sortedData.length % 2 === 0
|
||||
? (sortedData[middle - 1] + sortedData[middle]) / 2
|
||||
const median = sortedData.length % 2 === 0
|
||||
? (sortedData[middle - 1] + sortedData[middle]) / 2
|
||||
: sortedData[middle];
|
||||
|
||||
|
||||
// Define a threshold (e.g., 3 times the median) to filter out extreme values
|
||||
const threshold = median * 3;
|
||||
const filteredData = data.filter(num => num <= threshold);
|
||||
|
||||
|
||||
if (filteredData.length === 0) return median; // Fallback to median if all are removed
|
||||
|
||||
|
||||
// Compute weights based on inverse absolute deviation from median
|
||||
const weights = filteredData.map(num => 1 / (1 + Math.abs(num - median)));
|
||||
|
||||
|
||||
// Compute weighted sum and sum of weights
|
||||
const weightedSum = filteredData.reduce((sum, num, i) => sum + num * weights[i], 0);
|
||||
const sumOfWeights = weights.reduce((sum, weight) => sum + weight, 0);
|
||||
|
||||
|
||||
return weightedSum / sumOfWeights;
|
||||
}
|
||||
const avgSessionDuration = computed(() => {
|
||||
@@ -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`
|
||||
});
|
||||
|
||||
|
||||
80
dashboard/components/dialog/shields/AddAddress.vue
Normal 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>
|
||||
67
dashboard/components/dialog/shields/AddDomain.vue
Normal 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>
|
||||
56
dashboard/components/dialog/shields/DeleteAddress.vue
Normal 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>
|
||||
56
dashboard/components/dialog/shields/DeleteDomain.vue
Normal 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>
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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="">
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
146
dashboard/components/layout/VerticalBottomMenu.vue
Normal 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>
|
||||
@@ -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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<LayoutVerticalBottomMenu></LayoutVerticalBottomMenu>
|
||||
</LyxUiCard>
|
||||
|
||||
|
||||
|
||||
|
||||
73
dashboard/components/selector/ImageSelector.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
101
dashboard/components/shields/Addresses.vue
Normal 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>
|
||||
47
dashboard/components/shields/Bots.vue
Normal 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>
|
||||
99
dashboard/components/shields/Domains.vue
Normal 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>
|
||||
@@ -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(() => {
|
||||
if (!loggedUser.value?.logged) return false;
|
||||
return loggedUser.value.user.roles.includes('ADMIN');
|
||||
});
|
||||
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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
10
dashboard/pages/account.vue
Normal 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>
|
||||
10
dashboard/pages/billing.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -2,29 +2,43 @@
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
const colorMode = useColorMode();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="w-full h-full">
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
<div class="poppins">
|
||||
We hope Lilyx 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
|
||||
class="w-full h-full flex flex-col items-center pt-[35vh] dark:bg-lyx-background bg-lyx-lightmode-background">
|
||||
|
||||
<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 text-[1.2rem] text-center font-semibold mt-10">
|
||||
Payment successful
|
||||
</div>
|
||||
<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
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
35
dashboard/pages/shields.vue
Normal 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>
|
||||
8
dashboard/pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
|
||||
BIN
dashboard/public/flamy-black.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
dashboard/public/flamy.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
dashboard/public/lit.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
dashboard/public/logo-black.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
dashboard/public/logo-white.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 20 KiB |
BIN
dashboard/public/tech-icons/shopify.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
15
dashboard/server/api/admin/delete_feedback.delete.ts
Normal 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 });
|
||||
|
||||
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
// const customer =
|
||||
// premium?.customer_id ?
|
||||
// await StripeService.getCustomer(premium.customer_id) : 'NONE';
|
||||
|
||||
return {
|
||||
project, limits, counts, user,
|
||||
subscription: '',
|
||||
customer: ''
|
||||
}
|
||||
|
||||
return { project, limits, counts, user, subscription, customer }
|
||||
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -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,
|
||||
@@ -54,14 +57,14 @@ export default defineEventHandler(async event => {
|
||||
const count = await ProjectModel.countDocuments(matchQuery);
|
||||
|
||||
const projects = await ProjectModel.aggregate([
|
||||
{
|
||||
{
|
||||
$match: matchQuery
|
||||
},
|
||||
},
|
||||
{
|
||||
$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' },
|
||||
|
||||
121
dashboard/server/api/admin/user_info.ts
Normal 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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
return customer?.address;
|
||||
const [ok, customerInfoOrError] = await PaymentServiceHelper.customer_info(data.user.id);
|
||||
if (!ok) throw customerInfoOrError;
|
||||
|
||||
return customerInfoOrError;
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
});
|
||||
@@ -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 }
|
||||
});
|
||||
@@ -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,67 +18,11 @@ 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 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 })
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
return project.toJSON() as TProject;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
])
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -82,12 +72,12 @@ export default defineEventHandler(async event => {
|
||||
const csvHeader = [
|
||||
"name",
|
||||
"session",
|
||||
"metadata",
|
||||
"metadata",
|
||||
"website",
|
||||
"created_at",
|
||||
];
|
||||
|
||||
|
||||
|
||||
const lines: any[] = [];
|
||||
eventsReportData.forEach(line => lines.push(line.toJSON()));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
dashboard/server/api/report/customization.ts
Normal 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;
|
||||
|
||||
});
|
||||
32
dashboard/server/api/report/update_customization.post.ts
Normal 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 }
|
||||
|
||||
});
|
||||
9
dashboard/server/api/shields/bots/options.ts
Normal 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 }
|
||||
});
|
||||
14
dashboard/server/api/shields/bots/update_options.post.ts
Normal 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 };
|
||||
});
|
||||
11
dashboard/server/api/shields/countries/add.post.ts
Normal 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 };
|
||||
});
|
||||