19 Commits

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,15 +52,6 @@ const { safeSnapshotDates } = useSnapshot();
Docs
</NuxtLink>
<div>
<UTooltip :text="isDark ? 'Toggle light mode' : 'Toggle dark mode'">
<i @click="isDark = !isDark"
class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark"
:class="isDark ? 'far fa-moon' : 'far fa-sun'"></i>
</UTooltip>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -148,7 +148,7 @@ const sessionsLabel = computed(() => {
<div v-if="!isGuest"
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-lyx-lightmode-widget-light dark:bg-[#1e1412]">
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
<div class="poppins font-semibold"> This operation will reset this project to its initial state (0
visits 0 events 0 sessions) </div>
<div @click="openDeleteAllDomainDataDialog()"
class="text-[#e95b61] poppins font-semibold cursor-pointer hover:text-black hover:bg-red-700 outline rounded-lg w-fit px-8 py-2 outline-[1px] outline-[#532b26] bg-lyx-lightmode-widget-light dark:bg-[#291415]">

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ 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' },
@@ -27,7 +28,7 @@ const sections: Section[] = [
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
{ grow: true, label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
// {
// grow: true,
// label: 'Leave a Feedback', icon: 'fal fa-message',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
dashboard/public/flamy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
dashboard/public/lit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
import { TPremium } from "~/shared/schema/PremiumSchema";
import { TUser } from "~/shared/schema/UserSchema";
type ExtendedProject = {
limits: TProjectLimit[],
user: [TUser],
premium: [TPremium],
counts: [{
events: number,
visits: number,
@@ -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' },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { createUserJwt } from '~/server/AuthManager';
import { UserModel } from '@schema/UserSchema';
import { EmailService } from '@services/EmailService';
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
@@ -38,7 +39,6 @@ export default defineEventHandler(async event => {
const user = await UserModel.findOne({ email: payload.email });
if (user) {
user.google_tokens = tokens as any;
await user.save();
return {
error: false,
@@ -46,28 +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(() => {
const emailData = EmailService.getEmailServerInfo('brevolist_add', { email: payload.email as string });
if (!payload.email) return;
const emailData = EmailService.getEmailServerInfo('brevolist_add', { email: payload.email });
EmailServiceHelper.sendEmail(emailData);
});
setImmediate(() => {
console.log('SENDING WELCOME EMAIL TO', payload.email);
if (!payload.email) return;
const emailData = EmailService.getEmailServerInfo('welcome', { target: payload.email });
EmailServiceHelper.sendEmail(emailData);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { UserSettingsModel } from "@schema/UserSettings";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
@@ -21,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,11 @@
import { ProjectModel } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
import { UserLimitModel } from "@schema/UserLimitSchema";
import { UserSettingsModel } from "@schema/UserSettings";
import { AiChatModel } from "@schema/ai/AiChatSchema";
import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
import { SessionModel } from "@schema/metrics/SessionSchema";
import StripeService from "~/server/services/StripeService";
import { UserModel } from "@schema/UserSchema";
import { AddressBlacklistModel } from "~/shared/schema/shields/AddressBlacklistSchema";
import { DomainWhitelistModel } from "~/shared/schema/shields/DomainWhitelistSchema";
@@ -14,6 +13,10 @@ import { CountryBlacklistModel } from "~/shared/schema/shields/CountryBlacklistS
import { BotTrafficOptionModel } from "~/shared/schema/shields/BotTrafficOptionSchema";
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
import { PasswordModel } from "~/shared/schema/PasswordSchema";
import { PremiumModel } from "~/shared/schema/PremiumSchema";
import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper";
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
import { EventModel } from "~/shared/schema/metrics/EventSchema";
export default defineEventHandler(async event => {
@@ -22,36 +25,45 @@ export default defineEventHandler(async event => {
const projects = await ProjectModel.find({ owner: userData.id });
const premiumProjects = projects.filter(e => { return e.premium && e.premium_type != 0 }).length;
if (premiumProjects > 0) return setResponseStatus(event, 400, 'Cannot delete an account with a premium project');
const premium = await PremiumModel.findOne({ user_id: userData.id });
if (premium && premium.premium_type > 0) return setResponseStatus(event, 400, 'Cannot delete an account with a premium project');
const membersDeletation = await TeamMemberModel.deleteMany({ user_id: userData.id });
const membersEmailDeletation = await TeamMemberModel.deleteMany({ email: userData.user.email });
const passwordDeletation = await PasswordModel.deleteMany({ user_id: userData.id });
const limitdeletation = await UserLimitModel.deleteMany({ user_id: userData.id });
const notifiesDeletation = await LimitNotifyModel.deleteMany({ user_id: userData.id });
if (premium) PaymentServiceHelper.delete_customer(premium.customer_id);
for (const project of projects) {
const project_id = project._id;
await StripeService.deleteCustomer(project.customer_id);
const projectDeletation = await ProjectModel.deleteOne({ _id: project_id });
const userSettingsDeletation = await UserSettingsModel.deleteOne({ project_id });
const countDeletation = await ProjectCountModel.deleteMany({ project_id });
const limitdeletation = await ProjectLimitModel.deleteMany({ project_id });
const sessionsDeletation = await SessionModel.deleteMany({ project_id });
const notifiesDeletation = await LimitNotifyModel.deleteMany({ project_id });
const aiChatsDeletation = await AiChatModel.deleteMany({ project_id });
const sessionsDeletation = SessionModel.deleteMany({ project_id });
const visitsDeletation = VisitModel.deleteMany({ project_id });
const eventsDeletation = EventModel.deleteMany({ project_id });
const aiChatsDeletation = AiChatModel.deleteMany({ project_id });
//Shields
const addressBlacklistDeletation = await AddressBlacklistModel.deleteMany({ project_id });
const botTrafficOptionsDeletation = await BotTrafficOptionModel.deleteMany({ project_id });
const countryBlacklistDeletation = await CountryBlacklistModel.deleteMany({ project_id });
const domainWhitelistDeletation = await DomainWhitelistModel.deleteMany({ project_id });
const userDeletation = await UserModel.deleteOne({ _id: userData.id });
const addressBlacklistDeletation = AddressBlacklistModel.deleteMany({ project_id });
const botTrafficOptionsDeletation = BotTrafficOptionModel.deleteMany({ project_id });
const countryBlacklistDeletation = CountryBlacklistModel.deleteMany({ project_id });
const domainWhitelistDeletation = DomainWhitelistModel.deleteMany({ project_id });
}
const premiumDeletation = await PremiumModel.deleteOne({ user_id: userData.id });
const userDeletation = await UserModel.deleteOne({ _id: userData.id });
return { ok: true };

View File

@@ -4,7 +4,5 @@ import { AuthContext } from "~/server/middleware/01-authorization";
export default defineEventHandler(async event => {
const userData: AuthContext = getRequestUser(event) as any;
if (!userData.logged) return;
// const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { max_projects: 1 });
// return userSettings?.max_projects || 3;
return 20;
});

View File

@@ -1,6 +1,5 @@
import mongoose from "mongoose";
import { Redis } from "~/server/services/CacheService";
import StripeService from '~/server/services/StripeService';
import { logger } from "./Logger";
@@ -13,16 +12,6 @@ export default async () => {
logger.info('[SERVER] Initializing');
if (config.STRIPE_SECRET) {
const TEST_MODE = config.MODE === 'TEST';
StripeService.init(config.STRIPE_SECRET, config.STRIPE_WH_SECRET, TEST_MODE);
logger.info('[STRIPE] Initialized');
} else {
StripeService.disable();
logger.warn('[STRIPE] No stripe key - Disabled mode');
}
if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) {
logger.info('[DATABASE] Connecting');
connection = await mongoose.connect(config.MONGO_CONNECTION_STRING);

View File

@@ -4,6 +4,7 @@ import { UserModel } from "@schema/UserSchema";
import { ADMIN_EMAILS } from '@data/ADMINS';
import type { H3Event, EventHandlerRequest } from 'h3';
import { PremiumModel } from "~/shared/schema/PremiumSchema";
export type AuthContextLogged = {
id: string,
@@ -30,7 +31,7 @@ async function authorizationMiddleware(event: H3Event<EventHandlerRequest>) {
} else {
const [type, token] = authorization.split(' ');
const valid = readUserJwt(token);
if (!valid) return event.context.auth = { logged: false }
@@ -38,22 +39,28 @@ async function authorizationMiddleware(event: H3Event<EventHandlerRequest>) {
if (!user) return event.context.auth = { logged: false };
const roles: string[] = [];
if (ADMIN_EMAILS.includes(user.email)) {
roles.push('ADMIN');
}
const premium = await PremiumModel.findOne({ user_id: user.id });
if ((premium?.premium_type || 0) > 0) {
roles.push('PREMIUM');
}
const authContext: AuthContext = {
logged: true,
user: {
email: user.email,
name: user.name,
picture: user.picture || `https://robohash.org/${user.email}?set=set4`,
roles
roles,
},
id: user._id.toString()
}
event.context.auth = authContext;
}

View File

@@ -2,12 +2,10 @@
import OpenAI from "openai";
import { AiChatModel } from '@schema/ai/AiChatSchema';
import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
import { AiEventsInstance } from '../ai/functions/AI_Events';
import { AiVisitsInstance } from '../ai/functions/AI_Visits';
import { AiSessionsInstance } from '../ai/functions/AI_Sessions';
import { AiBillingInstance } from '../ai/functions/AI_Billing';
import { AiSnapshotInstance } from '../ai/functions/AI_Snapshots';
import { AiComposableChartInstance } from '../ai/functions/AI_ComposableChart';
@@ -21,7 +19,6 @@ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
...AiVisitsInstance.getTools(),
...AiEventsInstance.getTools(),
...AiSessionsInstance.getTools(),
...AiBillingInstance.getTools(),
...AiSnapshotInstance.getTools(),
...AiComposableChartInstance.getTools(),
]
@@ -31,7 +28,6 @@ const functions: any = {
...AiVisitsInstance.getHandlers(),
...AiEventsInstance.getHandlers(),
...AiSessionsInstance.getHandlers(),
...AiBillingInstance.getHandlers(),
...AiSnapshotInstance.getHandlers(),
...AiComposableChartInstance.getHandlers()
}
@@ -95,6 +91,10 @@ type ElaborateResponseCallbacks = {
onChatId?: (chat_id: string) => any
}
export function getInstance() {
return openai;
}
async function elaborateResponse(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], pid: string, time_offset: number, chat_id: string, callbacks?: ElaborateResponseCallbacks) {
console.log('[ELABORATING RESPONSE]');
@@ -218,34 +218,5 @@ export async function sendMessageOnChat(text: string, pid: string, time_offset:
return { content: ex.message, charts: [] };
}
// let response = await openai.chat.completions.create({ model: OPENAI_MODEL, messages, n: 1, tools });
// const chartsData: string[][] = [];
// while ((response.choices[0].message.tool_calls?.length || 0) > 0) {
// await addMessageToChat(response.choices[0].message, chat_id);
// messages.push(response.choices[0].message);
// if (response.choices[0].message.tool_calls) {
// console.log('Tools to call', response.choices[0].message.tool_calls.length);
// chartsData.push(getChartsInMessage(response.choices[0].message));
// for (const toolCall of response.choices[0].message.tool_calls) {
// const functionName = toolCall.function.name;
// console.log('Calling tool function', functionName);
// const functionToCall = functions[functionName];
// const functionArgs = JSON.parse(toolCall.function.arguments);
// const functionResponse = await functionToCall({ project_id: pid, ...functionArgs });
// messages.push({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) });
// await addMessageToChat({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }, chat_id);
// }
// }
// response = await openai.chat.completions.create({ model: OPENAI_MODEL, messages, n: 1, tools });
// }
// await addMessageToChat(response.choices[0].message, chat_id);
// await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } })
// return { content: response.choices[0].message.content, charts: chartsData.filter(e => e.length > 0).flat() };
}

View File

@@ -0,0 +1,61 @@
const { PAYMENT_SECRET } = useRuntimeConfig();
type ErrorResponse = [false, Error];
type OkResponse<T> = [true, T];
type PaymentServiceResponse<T> = Promise<OkResponse<T> | ErrorResponse>
export class PaymentServiceHelper {
static BASE_URL = 'https://payments.litlyx.com/payment';
private static async send(endpoint: string, body: Record<string, any>): PaymentServiceResponse<any> {
try {
const res = await $fetch(`${this.BASE_URL}${endpoint}`, {
body: JSON.stringify(body),
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-litlyx-token': PAYMENT_SECRET
}
})
return [true, res];
} catch (ex: any) {
console.log('---')
console.log(ex.response?._data);
console.log('---')
console.error(ex);
return [false, ex];
}
}
static async create_customer(user_id: string): PaymentServiceResponse<{ ok: true }> {
return await this.send('/create_customer', { user_id });
}
static async create_subscription(user_id: string, plan_tag: string): PaymentServiceResponse<{ ok: true }> {
return await this.send('/create_subscription', { user_id, plan_tag });
}
static async create_payment(user_id: string, plan_id: number): PaymentServiceResponse<{ url: string }> {
return await this.send('/create_payment', { user_id, plan_id });
}
static async invoices_list(user_id: string): PaymentServiceResponse<{ invoices: any[] }> {
return await this.send('/invoices_list', { user_id });
}
static async customer_info(user_id: string): PaymentServiceResponse<any> {
return await this.send('/customer_info', { user_id });
}
static async update_customer_info(user_id: string, address: { line1: string, line2: string, city: string, country: string, postal_code: string, state: string }): PaymentServiceResponse<{ ok: true }> {
return await this.send('/update_customer_info', { user_id, address });
}
static async delete_customer(customer_id: string): PaymentServiceResponse<{ ok: true }> {
return await this.send('/delete_customer', { customer_id });
}
}

View File

@@ -1,225 +0,0 @@
import { getPlanFromId, getPlanFromTag, PREMIUM_TAG } from '@data/PREMIUM';
import Stripe from 'stripe';
class StripeService {
private stripe?: Stripe;
private privateKey?: string;
private webhookSecret?: string;
public testMode?: boolean;
private disabledMode: boolean = false;
init(privateKey: string, webhookSecret: string, testMode: boolean = false) {
this.privateKey = privateKey;
this.webhookSecret = webhookSecret;
this.stripe = new Stripe(this.privateKey);
this.testMode = testMode;
}
disable() { this.disabledMode = true; }
enable() { this.disabledMode = false; }
isDisabled() { return this.disabledMode; }
parseWebhook(body: any, sig: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
if (!this.webhookSecret) {
console.error('Stripe not initialized')
return;
}
return this.stripe.webhooks.constructEvent(body, sig, this.webhookSecret);
}
async createOnetimePayment(price: string, success_url: string, pid: string, customer?: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const checkout = await this.stripe.checkout.sessions.create({
allow_promotion_codes: true,
payment_method_types: ['card'],
invoice_creation: {
enabled: true,
},
line_items: [
{ price, quantity: 1 }
],
payment_intent_data: {
metadata: {
pid, price
}
},
customer,
success_url,
mode: 'payment'
});
return checkout;
}
async createPayment(price: string, success_url: string, pid: string, customer?: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const checkout = await this.stripe.checkout.sessions.create({
allow_promotion_codes: true,
payment_method_types: ['card'],
line_items: [
{ price, quantity: 1 }
],
subscription_data: {
metadata: { pid },
},
customer,
success_url,
mode: 'subscription'
});
return checkout;
}
async getPriceData(priceId: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const priceData = await this.stripe.prices.retrieve(priceId);
return priceData;
}
async deleteSubscription(subscriptionId: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const subscription = await this.stripe.subscriptions.cancel(subscriptionId);
return subscription;
}
async getSubscription(subscriptionId: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
return subscription;
}
async getAllSubscriptions(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const subscriptions = await this.stripe.subscriptions.list({ customer: customer_id });
return subscriptions;
}
async getInvoices(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const invoices = await this.stripe?.invoices.list({ customer: customer_id });
return invoices;
}
async getCustomer(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const customer = await this.stripe.customers.retrieve(customer_id, { expand: [] })
return customer;
}
async createCustomer(email: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const customer = await this.stripe.customers.create({ email });
return customer;
}
async setCustomerInfo(customer_id: string, address: { line1: string, line2: string, city: string, country: string, postal_code: string, state: string }) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const customer = await this.stripe.customers.update(customer_id, {
address: {
line1: address.line1,
line2: address.line2,
city: address.city,
country: address.country,
postal_code: address.postal_code,
state: address.state
}
})
return customer.id;
}
async deleteCustomer(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const { deleted } = await this.stripe.customers.del(customer_id);
return deleted;
}
async createStripeCode(plan: PREMIUM_TAG) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const INCUBATION_COUPON = 'sDD7Weh3';
if (plan === 'INCUBATION') {
await this.stripe.promotionCodes.create({
coupon: INCUBATION_COUPON,
active: true,
code: 'TESTCACCA1',
max_redemptions: 1,
})
return true;
}
return false;
}
async createSubscription(customer_id: string, planId: number) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const PLAN = getPlanFromId(planId);
if (!PLAN) throw Error('Plan not found');
const subscription = await this.stripe.subscriptions.create({
customer: customer_id,
items: [
{ price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 }
],
});
return subscription;
}
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const PLAN = getPlanFromId(planId);
if (!PLAN) throw Error('Plan not found');
const subscription = await this.stripe.subscriptions.create({
customer: customer_id,
items: [
{ price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 }
],
});
return subscription;
}
async createFreeSubscription(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const FREE_PLAN = getPlanFromTag('FREE');
const subscription = await this.stripe.subscriptions.create({
customer: customer_id,
items: [
{ price: this.testMode ? FREE_PLAN.PRICE_TEST : FREE_PLAN.PRICE, quantity: 1 }
]
});
return subscription;
}
}
const instance = new StripeService();
export default instance;

View File

@@ -62,7 +62,7 @@ export class EmailService {
}
}
static async sendLimitEmail50(target: string, projectName: string) {
static async sendLimitEmail50(target: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "⚡ You've reached 50% limit on Litlyx";
@@ -70,7 +70,6 @@ export class EmailService {
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = TEMPLATE.LIMIT_50_EMAIL
.replace(/\[Project Name\]/, projectName)
.toString();
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
@@ -81,14 +80,13 @@ export class EmailService {
}
}
static async sendLimitEmail90(target: string, projectName: string) {
static async sendLimitEmail90(target: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "⚡ You've reached 90% limit on Litlyx";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = TEMPLATE.LIMIT_90_EMAIL
.replace(/\[Project Name\]/, projectName)
.toString();
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true;
@@ -98,14 +96,13 @@ export class EmailService {
}
}
static async sendLimitEmailMax(target: string, projectName: string) {
static async sendLimitEmailMax(target: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "🚨 You've reached your limit on Litlyx!";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = TEMPLATE.LIMIT_MAX_EMAIL
.replace(/\[Project Name\]/, projectName)
.toString();
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true;
@@ -130,14 +127,13 @@ export class EmailService {
}
}
static async sendPurchaseEmail(target: string, projectName: string) {
static async sendPurchaseEmail(target: string) {
try {
const sendSmtpEmail = new SendSmtpEmail();
sendSmtpEmail.subject = "Thank You for Upgrading Your Litlyx Plan!";
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
sendSmtpEmail.to = [{ "email": target }];
sendSmtpEmail.htmlContent = TEMPLATE.PURCHASE_EMAIL
.replace(/\[Project Name\]/, projectName)
.toString();
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
return true;

View File

@@ -90,10 +90,8 @@ app.post('/send/welcome', express.json(), async (req, res) => {
app.post('/send/purchase', express.json(), async (req, res) => {
try {
console.log('PURCHASE EMAIL DISABLED')
return;
const { target, projectName } = req.body;
const ok = await EmailService.sendPurchaseEmail(target, projectName);
const { target } = req.body;
const ok = await EmailService.sendPurchaseEmail(target);
res.json({ ok });
} catch (ex) {
res.status(500).json({ error: ex.message });
@@ -132,8 +130,8 @@ app.post('/send/anomaly/visits_events', express.json(), async (req, res) => {
app.post('/send/limit/50', express.json(), async (req, res) => {
try {
const { target, projectName } = req.body;
const ok = await EmailService.sendLimitEmail50(target, projectName);
const { target } = req.body;
const ok = await EmailService.sendLimitEmail50(target);
res.json({ ok });
} catch (ex) {
res.status(500).json({ error: ex.message });
@@ -142,8 +140,8 @@ app.post('/send/limit/50', express.json(), async (req, res) => {
app.post('/send/limit/90', express.json(), async (req, res) => {
try {
const { target, projectName } = req.body;
const ok = await EmailService.sendLimitEmail90(target, projectName);
const { target } = req.body;
const ok = await EmailService.sendLimitEmail90(target);
res.json({ ok });
} catch (ex) {
res.status(500).json({ error: ex.message });
@@ -152,8 +150,8 @@ app.post('/send/limit/90', express.json(), async (req, res) => {
app.post('/send/limit/max', express.json(), async (req, res) => {
try {
const { target, projectName } = req.body;
const ok = await EmailService.sendLimitEmailMax(target, projectName);
const { target } = req.body;
const ok = await EmailService.sendLimitEmailMax(target);
res.json({ ok });
} catch (ex) {
res.status(500).json({ error: ex.message });

View File

@@ -1,5 +1,6 @@
export const CONFIRM_EMAIL = `<!DOCTYPE html>
export const CONFIRM_EMAIL = `
<!DOCTYPE html>
<html>
<head>
@@ -35,7 +36,7 @@ export const CONFIRM_EMAIL = `<!DOCTYPE html>
.button {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
background-color: #0a0a0a;
color: #ffffff;
text-decoration: none;
border-radius: 5px;
@@ -61,7 +62,7 @@ export const CONFIRM_EMAIL = `<!DOCTYPE html>
<p>We hope to hear from you soon!</p>
<div class="footer">
<p>&copy; 2024 Litlyx. All rights reserved.</p>
<p>2025 &copy; Litlyx. All rights reserved.</p>
</div>
</div>
</body>

View File

@@ -1,14 +1,10 @@
export const LIMIT_50_EMAIL = `<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
export const LIMIT_50_EMAIL = `
<!DOCTYPE html>
<html>
<head>
<link rel="preload" as="image" href="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/airbnb-logo.png" />
<link rel="preload" as="image"
href="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/airbnb-review-user.jpg" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
</head>
<body
@@ -42,13 +38,13 @@ export const LIMIT_50_EMAIL = `<!DOCTYPE html
<tr style="width:100%">
<p
style="font-size:32px;line-height:1.3;margin:16px 0;font-weight:700;color:#484848">
Youve Reached 50% of Your Litlyx Project Limit on [Project Name]
Youve Reached 50% of Your Litlyx's Tracking Limits
</p>
<p
style="font-size:18px;line-height:1.4;margin:16px 0;color:#484848;padding:24px;background-color:#f2f3f3;border-radius:4px">
To avoid losing precious data, please remember to monitor your usage
on the <strong>Litlyx Dashboard</strong>. You can find your current
usage details under <strong>Settings > Billing Tab</strong>
usage details in your account section.
</p>
<p>If you need more data collection storage, you may consider upgrading
@@ -65,7 +61,7 @@ export const LIMIT_50_EMAIL = `<!DOCTYPE html
<p>Have a nice day!</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
<p>CEO at Litlyx</p>
<a href="https://dashboard.litlyx.com/"
style="line-height:100%;text-decoration:none;display:block;max-width:100%;mso-padding-alt:0px;background-color:#5680f8;border-radius:3px;color:#fff;font-size:18px;padding-top:19px;padding-bottom:19px;text-align:center;width:100%;padding:19px 0px 19px 0px"
@@ -92,7 +88,7 @@ export const LIMIT_50_EMAIL = `<!DOCTYPE html
<tr style="width:100%">
<p
style="font-size:14px;line-height:24px;margin:16px 0;color:#9ca299;margin-bottom:10px">
2024 © Litlyx. All rights reserved.
2025 © Litlyx. All rights reserved.
<br>
Litlyx S.R.L. - Viale Tirreno, 187 - 00141 Rome - P.IVA:
17814721001- REA: RM-1743194

View File

@@ -1,14 +1,10 @@
export const LIMIT_90_EMAIL = `<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
export const LIMIT_90_EMAIL = `
<!DOCTYPE html>
<html>
<head>
<link rel="preload" as="image" href="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/airbnb-logo.png" />
<link rel="preload" as="image"
href="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/airbnb-review-user.jpg" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
</head>
<body
@@ -42,13 +38,13 @@ export const LIMIT_90_EMAIL = `<!DOCTYPE html
<tr style="width:100%">
<p
style="font-size:32px;line-height:1.3;margin:16px 0;font-weight:700;color:#484848">
Youve Reached 90% of Your Litlyx Project Limit on [Project Name]
Youve Reached 90% of Your Litlyx's Tracking Limits
</p>
<p
style="font-size:18px;line-height:1.4;margin:16px 0;color:#484848;padding:24px;background-color:#f2f3f3;border-radius:4px">
To avoid losing precious data, please remember to monitor your usage
on the <strong>Litlyx Dashboard</strong>. You can find your current
usage details under <strong>Settings > Billing Tab</strong>
usage details in your account section.
</p>
<p>If you need more data collection storage, you may consider upgrading
@@ -65,7 +61,7 @@ export const LIMIT_90_EMAIL = `<!DOCTYPE html
<p>Have a nice day!</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
<p>CEO at Litlyx</p>
<a href="https://dashboard.litlyx.com/"
style="line-height:100%;text-decoration:none;display:block;max-width:100%;mso-padding-alt:0px;background-color:#5680f8;border-radius:3px;color:#fff;font-size:18px;padding-top:19px;padding-bottom:19px;text-align:center;width:100%;padding:19px 0px 19px 0px"
@@ -92,7 +88,7 @@ export const LIMIT_90_EMAIL = `<!DOCTYPE html
<tr style="width:100%">
<p
style="font-size:14px;line-height:24px;margin:16px 0;color:#9ca299;margin-bottom:10px">
2024 © Litlyx. All rights reserved.
2025 © Litlyx. All rights reserved.
<br>
Litlyx S.R.L. - Viale Tirreno, 187 - 00141 Rome - P.IVA:
17814721001- REA: RM-1743194
@@ -113,5 +109,4 @@ export const LIMIT_90_EMAIL = `<!DOCTYPE html
</body>
</html>
`

View File

@@ -1,14 +1,9 @@
export const LIMIT_MAX_EMAIL = `<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
export const LIMIT_MAX_EMAIL = `<!DOCTYPE html>
<html>
<head>
<link rel="preload" as="image" href="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/airbnb-logo.png" />
<link rel="preload" as="image"
href="https://react-email-demo-lpdmf0ryo-resend.vercel.app/static/airbnb-review-user.jpg" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
</head>
<body
@@ -42,11 +37,11 @@ export const LIMIT_MAX_EMAIL = `<!DOCTYPE html
<tr style="width:100%">
<p
style="font-size:32px;line-height:1.3;margin:16px 0;font-weight:700;color:#484848">
Youve Reached Your Litlyx Project Limit on [Project Name]
Youve Reached Your Litlyx's Tracking Limits
</p>
<p
style="font-size:18px;line-height:1.4;margin:16px 0;color:#484848;padding:24px;background-color:#ffbb03;border-radius:4px">
We noticed that Litlyx has stopped collecting data for your project.
<strong>Litlyx has stopped collecting data for your project.</strong>
</p>
<p>
@@ -62,14 +57,9 @@ export const LIMIT_MAX_EMAIL = `<!DOCTYPE html
<p>
If you need additional storage for data collection, consider
upgrading your plan to unlock more benefits and ensure uninterrupted
service.
service. You can find out more here: <a href="https://litlyx.com/pricing">Our pricing</a>
</p>
<p style="font-weight: 700;">
As a token of appreciation, we're offering you 25% off for life at
checkout with the code LIT25.
</p>
Thank you for choosing Litlyx as your trusted analytics tool.
<p></p>
<p>
If you have any questions or need assistance, feel free to reply to
@@ -82,8 +72,8 @@ export const LIMIT_MAX_EMAIL = `<!DOCTYPE html
</p>
<p>
Antonio
CEO | Litlyx
Antonio,
CEO at Litlyx
</p>
@@ -113,7 +103,7 @@ export const LIMIT_MAX_EMAIL = `<!DOCTYPE html
<tr style="width:100%">
<p
style="font-size:14px;line-height:24px;margin:16px 0;color:#9ca299;margin-bottom:10px">
2024 © Litlyx. All rights reserved.
2025 © Litlyx. All rights reserved.
<br>
Litlyx S.R.L. - Viale Tirreno, 187 - 00141 Rome - P.IVA:
17814721001- REA: RM-1743194
@@ -134,5 +124,4 @@ export const LIMIT_MAX_EMAIL = `<!DOCTYPE html
</body>
</html>
`

View File

@@ -1,29 +1,104 @@
export const PURCHASE_EMAIL = `<!DOCTYPE html>
<html lang="en">
export const PURCHASE_EMAIL = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thank You for Upgrading Your Litlyx Plan!</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<!-- Email Content -->
<body style='background-color:#ffffff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif'>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="max-width:100%;margin:0 auto;padding:20px 0 48px;width:580px">
<tbody>
<tr style="width:100%">
<td>
<p>Dear User,</p>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>
<img alt="Founder" height="96"
src="https://litlyx.com/images/founder.jpg"
style="display:block;outline:none;border:none;text-decoration:none;margin:0 auto;margin-bottom:16px;border-radius:50%"
width="96" />
</td>
</tr>
</tbody>
</table>
<p>We are thrilled to inform you that <strong>[Project Name]</strong> on <strong>Litlyx</strong> has successfully been upgraded to a higher plan! Thank you for choosing to elevate your experience with us and for believing in our project.</p>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="padding-bottom:20px">
<tbody>
<tr>
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<td>
<p style="font-size:32px;line-height:1.3;margin:16px 0;font-weight:700;color:#484848">
Your upgrade is live!
</p>
<p>We appreciate your trust in Litlyx and are committed to providing you with the best analytics experience. Your support helps us to continually improve our platform and bring new features to make your analytics journey even better.</p>
<p>Thank you for choosing Litlyx as your go-to website analytics tool. Our mission is simple: to make everyday tracking effortless and effective.</p>
<p>You can find your current plan details and download your invoices under <strong>Settings > Billing Tab</strong>.</p>
<p>Were super happy to have you with us, and we cant wait to keep improving Litlyx every single day. Youre part of this journey now, and were building it with you in mind.</p>
<p>If you have any questions about your new plan or need assistance, feel free to reach out to our support team at <a href="mailto:help@litlyx.com" style="color: #28a745; text-decoration: none;"><strong>help@litlyx.com</strong></a>. Were here to help you make the most out of your upgraded plan!</p>
<p>If you have any questions, need help, or want to share feedback, just reply to this email or reach out at <a
href="mailto:help@litlyx.com"
style="color: #FF5733; text-decoration: none;">help@litlyx.com</a>. Id love to hear from you.</p>
<p>Best regards,</p>
<p>Thanks again for being with us. We won't let you down.</p>
<p>Antonio,</p>
<p>CEO @ Litlyx</p>
<p>Have a great day!</p>
<p>Antonio,<br />CEO at Litlyx</p>
<a href="https://dashboard.litlyx.com/"
style="line-height:100%;text-decoration:none;display:block;max-width:100%;mso-padding-alt:0px;background-color:#5680f8;border-radius:3px;color:#fff;font-size:18px;padding-top:19px;padding-bottom:19px;text-align:center;width:100%;padding:19px 0px 19px 0px"
target="_blank">
<span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:14.25px">
Go to Dashboard
</span>
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#cccccc;margin:20px 0" />
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<td>
<p style="font-size:14px;line-height:24px;margin:16px 0;color:#9ca299;margin-bottom:10px">
2025 © Litlyx. All rights reserved.
<br />
Litlyx S.R.L. - Viale Tirreno, 187 - 00141 Rome - P.IVA: 17814721001 - REA: RM-1743194
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>`
</html>
`

View File

@@ -1,39 +1,158 @@
export const WELCOME_EMAIL = `
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Litlyx!</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<p>Were happy to have you onboard,</p>
<p>At Litlyx, were committed to creating the best analytics collection experience for everybody, starting from developers.</p>
<p>Here are a few things you can do to get started tracking analytics today:</p>
<ol>
<li><strong><a href="https://dashboard.litlyx.com" style="color: #007BFF; text-decoration: none;">Create a new project</a></strong> by just naming it</li>
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Copy the universal Script</a></strong> we provide you the snippets to copy in your index.html file and start instantly to track metrics on your website or web app.</li>
<li><strong><a style="color: #0a0a0a; text-decoration: none;">Deploy</a></strong> Litlyx is production ready.</li>
</ol>
<p>If you have any questions or need support, visit <a href="http://docs.litlyx.com" style="color: #007BFF;">docs.litlyx.com</a>.</p>
<p>Feel free to reply to this email or reach out to our team at <a href="mailto:help@litlyx.com" style="color: #007BFF;">help@litlyx.com</a>. Were here to help!</p>
<p>Link to Discord for developer support: <a href="https://discord.com/invite/9cQykjsmWX" style="color: #007BFF;">https://discord.com/invite/9cQykjsmWX</a></p>
<p>Thank you for joining us, and we look forward to seeing you around.</p>
<p>We want to make analytics the freshest thing on the web.</p>
<p>Antonio,</p>
<p>CEO | Litlyx</p>
<body
style='background-color:#ffffff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif'>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="max-width:100%;margin:0 auto;padding:20px 0 48px;width:580px">
<tbody>
<tr style="width:100%">
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>
<img alt="Founder" height="96" src="https://litlyx.com/images/founder.jpg"
style="display:block;outline:none;border:none;text-decoration:none;margin:0 auto;margin-bottom:16px;border-radius:50%"
width="96" />
</td>
</tr>
</tbody>
</table>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="padding-bottom:20px">
<tbody>
<tr>
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<td>
<p
style="font-size:32px;line-height:1.3;margin:16px 0;font-weight:700;color:#484848">
Welcome to Litlyx!
</p>
<p
style="font-size:18px;line-height:1.4;margin:16px 0;color:#484848;">
Hey, Im Antonio, CEO at Litlyx. Just wanted to say a quick
thank you for signing up.
</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">
We built Litlyx to make website analytics easy, fast, and
privacy-conscious. Your personal AI assistant can learn to serve your needs over time advicing you on growing your audience and improve your product.
</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">
Heres how to get started in under <b>1 minute</b>:
</p>
<ol
style="font-size:16px;line-height:1.6;color:#484848;padding-left: 20px;">
<li><strong><a href="https://dashboard.litlyx.com"
style="color: #007BFF; text-decoration: none;">Create
a new project</a></strong> name it and youre good
to go</li>
<li><strong>Copy the tracking script</strong> paste it in your
<code>index.html</code> and start collecting data instantly
</li>
<li><strong>Deploy</strong> Litlyx is ready for production and
incredibly lightweight</li>
</ol>
<p style="font-size:16px;line-height:1.6;color:#484848;">
Using WordPress, Google Tag Manager, or a modern framework? Head
over to <a href="http://docs.litlyx.com"
style="color: #007BFF;">our docs</a> for full setup
instructions.
</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">
Got questions or feedback? Just reply to this email or reach out
to <a href="mailto:help@litlyx.com"
style="color: #007BFF;">help@litlyx.com</a>. Were always
here to help.
</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">
You can also join our <a
href="https://discord.com/invite/9cQykjsmWX"
style="color: #007BFF;">Discord community</a> for live
support and product updates.
</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">
Thanks again for joining us. Were excited to have you on board.
</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">
Lets make analytics the simplest part of your stack.
</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">Antonio,</p>
<p style="font-size:16px;line-height:1.6;color:#484848;">CEO at
Litlyx</p>
<a href="https://dashboard.litlyx.com/"
style="line-height:100%;text-decoration:none;display:block;max-width:100%;mso-padding-alt:0px;background-color:#5680f8;border-radius:3px;color:#fff;font-size:18px;padding-top:19px;padding-bottom:19px;text-align:center;width:100%;padding:19px 0px 19px 0px"
target="_blank">
<span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:14.25px">
Go to Dashboard
</span>
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<hr
style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#cccccc;margin:20px 0" />
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody>
<tr>
<td>
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
role="presentation">
<tbody style="width:100%">
<tr style="width:100%">
<td>
<p
style="font-size:14px;line-height:24px;margin:16px 0;color:#9ca299;margin-bottom:10px">
2025 © Litlyx. All rights reserved.
<br />
Litlyx S.R.L. Viale Tirreno, 187 00141 Rome P.IVA:
17814721001 REA: RM-1743194
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
`

View File

@@ -0,0 +1,97 @@
import type Event from 'stripe';
import StripeService from '../services/StripeService';
import { getPlanFromPrice, getPlanFromTag, PLAN_DATA } from '../shared/data/PLANS';
import { PremiumModel } from '../shared/schema/PremiumSchema';
import { UserLimitModel } from '../shared/schema/UserLimitSchema';
import { EmailService } from '../shared/services/EmailService';
async function addSubscriptionToUser(user_id: string, plan: PLAN_DATA, subscription_id: string, current_period_start: number, current_period_end: number) {
await PremiumModel.updateOne({ user_id }, {
premium_type: plan.ID,
subscription_id,
expire_at: current_period_end * 1000
}, { upsert: true });
await UserLimitModel.updateOne({ user_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 })
}
export async function onPaymentFailed(event: Event.InvoicePaymentFailedEvent) {
if (event.data.object.attempt_count == 0) return { received: true, warn: 'attempt_count = 0' }
//TODO: Send emails
const customer_id = event.data.object.customer as string;
const premiumData = await PremiumModel.findOne({ customer_id });
if (!premiumData) return { error: 'customer not found' }
const subscription_id = event.data.object.subscription as string;
await StripeService.deleteSubscription(subscription_id);
const freeSub = await StripeService.createFreeSubscription(customer_id);
await PremiumModel.updateOne({ customer_id }, { subscription_id: freeSub.id });
await addSubscriptionToUser(premiumData.user_id.toString(), getPlanFromTag('FREE'), subscription_id, event.data.object.period_start, event.data.object.period_end);
return { ok: true }
}
export async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
const customer_id = event.data.object.customer;
const premiumData = await PremiumModel.findOne({ customer_id });
if (!premiumData) return { error: 'customer not found' }
if (event.data.object.status !== 'paid') return { received: true, warn: 'payment status not paid' }
const subscription_id = event.data.object.subscription as string;
const price = event.data.object.lines.data[0].price.id;
if (!price) return { error: 'price not found' }
const plan = getPlanFromPrice(price, StripeService.testMode);
if (!plan) return { error: 'plan not found' }
const databaseSubscription = premiumData.subscription_id;
const currentSubscriptionData = await StripeService.getSubscription(subscription_id);
if (!currentSubscriptionData || currentSubscriptionData.status !== 'active') return { error: 'subscription not active' }
if (databaseSubscription != subscription_id) {
try {
await StripeService.deleteSubscription(databaseSubscription);
} catch (ex) {
console.error(ex);
}
}
await addSubscriptionToUser(premiumData.user_id.toString(), plan, subscription_id,
event.data.object.lines.data[0].period.start,
event.data.object.lines.data[0].period.end
);
setTimeout(() => {
if (plan.ID == 0) return;
//TODO: Email service template
// const emailData = EmailService.getEmailServerInfo('purchase', { target: user.email, projectName: project.name });
// EmailServiceHelper.sendEmail(emailData);
}, 1);
return { ok: true };
}

View File

@@ -0,0 +1,47 @@
import express from 'express';
import StripeService from './services/StripeService'
import { webhookRouter } from './routers/WebhookRouter';
import { paymentRouter } from './routers/PaymentRouter';
import { connectDatabase } from './shared/services/DatabaseService';
const STRIPE_PRIVATE_KEY = process.env.STRIPE_PRIVATE_KEY;
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
const STRIPE_TESTMODE = process.env.STRIPE_TESTMODE === 'true';
const MONGO_CONNECTION_STRING = process.env.MONGO_CONNECTION_STRING;
StripeService.init(STRIPE_PRIVATE_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_TESTMODE);
connectDatabase(MONGO_CONNECTION_STRING);
console.log('Stripe started in', STRIPE_TESTMODE ? 'TESTMODE' : 'LIVEMODE');
const app = express();
const TOKEN = process.env.TOKEN;
if (!TOKEN || TOKEN.length == 0) {
console.log('TOKEN not set');
process.exit();
}
app.use('/webhook', webhookRouter);
app.use((req, res, next) => {
const token = req.header('x-litlyx-token');
if (token != TOKEN) {
res.status(403).json({ error: 'token not valid' });
return;
}
console.log(req.path);
next();
});
app.use('/payment', paymentRouter);
const port = parseInt(process.env.PORT);
if (!port) {
console.error('PORT is not set');
process.exit();
}
app.listen(port, () => console.log(`Listening on port ${port}`));

View File

@@ -3,34 +3,62 @@ import z from 'zod';
import { getPlanFromId } from '../shared/data/PLANS';
import StripeService from '../services/StripeService';
import { sendJson } from '../Utils';
import { ProjectModel } from '../shared/schema/project/ProjectSchema';
import { PremiumModel } from '../shared/schema/PremiumSchema';
import { Types } from 'mongoose';
import { UserModel } from '../shared/schema/UserSchema';
export const paymentRouter = Router();
export const ZBodyCreateCustomer = z.object({
user_id: z.string()
});
paymentRouter.post('/create_customer', json(), async (req, res) => {
try {
const createCustomerData = ZBodyCreateCustomer.parse(req.body);
const user = await UserModel.findOne({ _id: createCustomerData.user_id });
if (!user) return sendJson(res, 400, { error: 'user not found' });
const customer = await StripeService.createCustomer(user.email);
const freesub = await StripeService.createFreeSubscription(customer.id);
await PremiumModel.create({
user_id: user.id,
customer_id: customer.id,
premium_type: 0,
subscription_id: freesub.id
})
return sendJson(res, 200, { ok: true });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});
export const ZBodyCreatePayment = z.object({
pid: z.string(),
user_id: z.string(),
plan_id: z.number()
})
});
paymentRouter.post('/create', json(), async (req, res) => {
paymentRouter.post('/create_payment', json(), async (req, res) => {
try {
const createPaymentData = ZBodyCreatePayment.parse(req.body);
const plan = getPlanFromId(createPaymentData.plan_id);
if (!plan) return sendJson(res, 400, { error: 'plan not found' });
const project = await ProjectModel.findById(createPaymentData.pid);
if (!project) return sendJson(res, 400, { error: 'project not found' });
if (!project.customer_id) return sendJson(res, 400, { error: 'project have no customer_id' });
const premiumData = await PremiumModel.findOne({ user_id: createPaymentData.user_id });
if (!premiumData) return sendJson(res, 400, { error: 'user not found' });
if (!premiumData.customer_id) return sendJson(res, 400, { error: 'user have no customer_id' });
const price = StripeService.testMode ? plan.PRICE_TEST : plan.PRICE;
const checkout = await StripeService.createPayment(
price,
'https://dashboard.litlyx.com/payment_ok',
createPaymentData.pid,
project.customer_id
createPaymentData.user_id,
premiumData.customer_id
);
if (!checkout) return sendJson(res, 400, { error: 'cannot create payment' });
@@ -40,4 +68,123 @@ paymentRouter.post('/create', json(), async (req, res) => {
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});
export const ZBodyInvoicesList = z.object({
user_id: z.string()
});
paymentRouter.post('/invoices_list', json(), async (req, res) => {
try {
const invoicesListData = ZBodyInvoicesList.parse(req.body);
const premiumData = await PremiumModel.findOne({ user_id: invoicesListData.user_id });
if (!premiumData) return sendJson(res, 400, { error: 'user not found' });
if (!premiumData.customer_id) return sendJson(res, 400, { error: 'user have no customer_id' });
const invoices = await StripeService.getInvoices(premiumData.customer_id);
return sendJson(res, 200, { invoices: invoices.data });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});
export const ZBodyCustomerInfo = z.object({
user_id: z.string()
});
paymentRouter.post('/customer_info', json(), async (req, res) => {
try {
const customerInfoData = ZBodyCustomerInfo.parse(req.body);
const premiumData = await PremiumModel.findOne({ user_id: customerInfoData.user_id });
if (!premiumData) return sendJson(res, 400, { error: 'user not found' });
if (!premiumData.customer_id) return sendJson(res, 400, { error: 'user have no customer_id' });
const customer = await StripeService.getCustomer(premiumData.customer_id);
if (!customer) return sendJson(res, 200, {});
if (customer.deleted === true) return sendJson(res, 200, {});
return sendJson(res, 200, customer.address);
} catch (ex) {
console.error(ex);
res.status(500).json({ error: ex.message });
}
});
export const ZBodyUpdateCustomerInfo = z.object({
user_id: z.string(),
address: z.object({
line1: z.string(),
line2: z.string(),
city: z.string(),
country: z.string(),
postal_code: z.string(),
state: z.string()
})
});
paymentRouter.post('/update_customer_info', json(), async (req, res) => {
try {
const updateCustomerInfoData = ZBodyUpdateCustomerInfo.parse(req.body);
const premiumData = await PremiumModel.findOne({ user_id: updateCustomerInfoData.user_id });
if (!premiumData) return sendJson(res, 400, { error: 'user not found' });
if (!premiumData.customer_id) return sendJson(res, 400, { error: 'user have no customer_id' });
await StripeService.setCustomerInfo(
premiumData.customer_id,
updateCustomerInfoData.address as any
);
return sendJson(res, 200, { ok: true });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});
export const ZBodyDeleteCustomer = z.object({
customer_id: z.string(),
});
paymentRouter.post('/delete_customer', json(), async (req, res) => {
try {
const deleteCustomerData = ZBodyDeleteCustomer.parse(req.body);
await StripeService.deleteCustomer(deleteCustomerData.customer_id);
return sendJson(res, 200, { ok: true });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});
export const ZBodyCreateSubscription = z.object({
user_id: z.string(),
plan_tag: z.string()
});
paymentRouter.post('/create_subscription', json(), async (req, res) => {
try {
const createSubscriptionData = ZBodyCreateSubscription.parse(req.body);
const premiumData = await PremiumModel.findOne({ user_id: createSubscriptionData.user_id });
if (!premiumData) return sendJson(res, 400, { error: 'user not found' });
if (!premiumData.customer_id) return sendJson(res, 400, { error: 'user have no customer_id' });
await StripeService.createSubscription(
premiumData.customer_id,
createSubscriptionData.plan_tag
);
return sendJson(res, 200, { ok: true });
} catch (ex) {
console.error(ex);
res.status(500).json({ error: ex.message });
}
});

View File

@@ -1,15 +1,30 @@
import { json, Router } from 'express';
import { raw, Router } from 'express';
import { sendJson } from '../Utils';
import StripeService from '../services/StripeService';
import * as WebhookController from '../controllers/WebhookController'
export const webhookRouter = Router();
webhookRouter.get('/', json(), async (req, res) => {
webhookRouter.post('/', raw({ type: 'application/json' }), async (req, res) => {
try {
const signature = req.header('stripe-signature');
if (!signature) {
console.error('No signature on the webhook')
if (!signature) return sendJson(res, 400, { error: 'No signature' });
const eventData = StripeService.parseWebhook(req.body, signature);
if (!eventData) return sendJson(res, 400, { error: 'Error parsing event data' });
if (eventData.type === 'invoice.paid') {
const response = await WebhookController.onPaymentSuccess(eventData);
return sendJson(res, 200, response);
}
if (eventData.type === 'invoice.payment_failed') {
const response = await WebhookController.onPaymentFailed(eventData);
return sendJson(res, 200, response);
}

View File

@@ -1,5 +1,6 @@
import Stripe from "stripe";
import { getPlanFromTag } from "../shared/data/PLANS";
@@ -51,7 +52,7 @@ class StripeService {
return checkout;
}
async createPayment(price: string, success_url: string, pid: string, customer: string) {
async createPayment(price: string, success_url: string, user_id: string, customer: string) {
if (!this.stripe) throw Error('Stripe not initialized');
const checkout = await this.stripe.checkout.sessions.create({
@@ -61,7 +62,7 @@ class StripeService {
{ price, quantity: 1 }
],
subscription_data: {
metadata: { pid },
metadata: { user_id },
},
customer,
success_url,
@@ -152,22 +153,21 @@ class StripeService {
// return false;
// }
// async createSubscription(customer_id: string, planId: number) {
// if (this.disabledMode) return;
// if (!this.stripe) throw Error('Stripe not initialized');
async createSubscription(customer_id: string, planTag: string) {
if (!this.stripe) throw Error('Stripe not initialized');
// const PLAN = getPlanFromId(planId);
// if (!PLAN) throw Error('Plan not found');
const PLAN_DATA = getPlanFromTag(planTag as any);
if (!PLAN_DATA) throw Error('Plan not found');
// const subscription = await this.stripe.subscriptions.create({
// customer: customer_id,
// items: [
// { price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 }
// ],
// });
const subscription = await this.stripe.subscriptions.create({
customer: customer_id,
items: [
{ price: this.testMode ? PLAN_DATA.PRICE_TEST : PLAN_DATA.PRICE, quantity: 1 }
],
});
// return subscription;
// }
return subscription;
}
// async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
// if (this.disabledMode) return;
@@ -186,22 +186,21 @@ class StripeService {
// return subscription;
// }
// async createFreeSubscription(customer_id: string) {
// if (this.disabledMode) return;
// if (!this.stripe) throw Error('Stripe not initialized');
async createFreeSubscription(customer_id: string) {
if (!this.stripe) throw Error('Stripe not initialized');
// const FREE_PLAN = getPlanFromTag('FREE');
const FREE_PLAN = getPlanFromTag('FREE');
// const subscription = await this.stripe.subscriptions.create({
// customer: customer_id,
// items: [
// { price: this.testMode ? FREE_PLAN.PRICE_TEST : FREE_PLAN.PRICE, quantity: 1 }
// ]
// });
const subscription = await this.stripe.subscriptions.create({
customer: customer_id,
items: [
{ price: this.testMode ? FREE_PLAN.PRICE_TEST : FREE_PLAN.PRICE, quantity: 1 }
]
});
// return subscription;
return subscription;
// }
}
}

View File

@@ -16,6 +16,7 @@ const streamName = requireEnv('STREAM_NAME');
import DeprecatedRouter from "./deprecated";
import { isAllowedToLog } from "./controller";
import { connectDatabase } from "./shared/services/DatabaseService";
app.use('/v1', DeprecatedRouter);
app.post('/event', express.json(jsonOptions), async (req, res) => {

View File

@@ -4,7 +4,7 @@ import path from 'path';
import child from 'child_process';
import { createZip } from '../helpers/zip-helper';
import { DeployHelper } from '../helpers/deploy-helper';
import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config';
import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REDIS_URL_PRODUCTION, REDIS_URL_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config';
const TMP_PATH = path.join(__dirname, '../../tmp');
const LOCAL_PATH = path.join(__dirname, '../../consumer');
@@ -30,19 +30,24 @@ async function main() {
}
console.log('Creting zip file');
console.log('Creating zip file');
const archive = createZip(TMP_PATH + '/' + ZIP_NAME);
archive.directory(LOCAL_PATH + '/dist', '/dist');
if (MODE === 'testmode') {
const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8');
const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1];
const devContent = ecosystemContent
.replace(REDIS_URL, `redis://${REMOTE_HOST_TESTMODE}`)
.replace(DATABASE_CONNECTION_STRING_PRODUCTION, `redis://${DATABASE_CONNECTION_STRING_TESTMODE}`);
.replace("$REDIS_URL$", `${REDIS_URL_TESTMODE}`)
.replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_TESTMODE}`)
.replace("$DEV_MODE$", `true`);
archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' });
} else {
archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' })
const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8');
const devContent = ecosystemContent
.replace("$REDIS_URL$", `${REDIS_URL_PRODUCTION}`)
.replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_PRODUCTION}`)
.replace("$DEV_MODE$", `false`);
archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' });
}

View File

@@ -16,13 +16,13 @@ helper.copy('services/EmailService.ts');
helper.create('schema');
helper.copy('schema/UserSchema.ts');
helper.copy('schema/UserLimitSchema.ts');
helper.create('schema/broker');
helper.copy('schema/broker/LimitNotifySchema.ts');
helper.create('schema/project');
helper.copy('schema/project/ProjectSchema.ts');
helper.copy('schema/project/ProjectsLimits.ts');
helper.copy('schema/project/ProjectsCounts.ts');
helper.create('schema/metrics');

View File

@@ -11,7 +11,7 @@ helper.copy('services/EmailService.ts');
helper.create('data');
helper.copy('data/PREMIUM.ts');
helper.copy('data/PLANS.ts');
helper.copy('data/ADMINS.ts');
helper.create('data/broker');

View File

@@ -1,87 +1,94 @@
// import fs from 'fs-extra';
// import path from 'path';
// import child from 'child_process';
// import { createZip } from '../helpers/zip-helper';
// import { DeployHelper } from '../helpers/deploy-helper';
// import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config';
import fs from 'fs-extra';
import path from 'path';
import child from 'child_process';
import { createZip } from '../helpers/zip-helper';
import { DeployHelper } from '../helpers/deploy-helper';
import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE, STRIPE_PRIVATE_KEY_PRODUCTION, STRIPE_PRIVATE_KEY_TESTMODE, STRIPE_WEBHOOK_SECRET_PRODUCTION, STRIPE_WEBHOOK_SECRET_TESTMODE } from '../.config';
// const TMP_PATH = path.join(__dirname, '../../tmp');
// const LOCAL_PATH = path.join(__dirname, '../../consumer');
// const REMOTE_PATH = '/home/litlyx/consumer';
// const ZIP_NAME = 'consumer.zip';
const TMP_PATH = path.join(__dirname, '../../tmp');
const LOCAL_PATH = path.join(__dirname, '../../payments');
const REMOTE_PATH = '/home/litlyx/payments';
const ZIP_NAME = 'payments.zip';
// const MODE = DeployHelper.getMode();
// const SKIP_BUILD = DeployHelper.getArgAt(0) == '--no-build';
const MODE = DeployHelper.getMode();
const SKIP_BUILD = DeployHelper.getArgAt(0) == '--no-build';
// console.log('Deploying consumer in mode:', MODE);
console.log('Deploying payments in mode:', MODE);
// setTimeout(() => { main(); }, 3000);
setTimeout(() => { main(); }, 3000);
// async function main() {
async function main() {
// if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true });
// fs.ensureDirSync(TMP_PATH);
if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true });
fs.ensureDirSync(TMP_PATH);
// if (!SKIP_BUILD) {
// console.log('Building');
// child.execSync(`cd ${LOCAL_PATH} && pnpm run build`);
// }
if (!SKIP_BUILD) {
console.log('Building');
child.execSync(`cd ${LOCAL_PATH} && pnpm run build`);
}
// console.log('Creting zip file');
// const archive = createZip(TMP_PATH + '/' + ZIP_NAME);
// archive.directory(LOCAL_PATH + '/dist', '/dist');
console.log('Creating zip file');
const archive = createZip(TMP_PATH + '/' + ZIP_NAME);
archive.directory(LOCAL_PATH + '/dist', '/dist');
// if (MODE === 'testmode') {
// const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8');
// const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1];
// const devContent = ecosystemContent
// .replace(REDIS_URL, `redis://${REMOTE_HOST_TESTMODE}`)
// .replace(DATABASE_CONNECTION_STRING_PRODUCTION, `redis://${DATABASE_CONNECTION_STRING_TESTMODE}`);
// archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' });
// } else {
// archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' })
// }
if (MODE === 'testmode') {
const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8');
const devContent = ecosystemContent
.replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_TESTMODE}`)
.replace("$STRIPE_PRIVATE_KEY$", `${STRIPE_PRIVATE_KEY_TESTMODE}`)
.replace("$STRIPE_WEBHOOK_SECRET$", `${STRIPE_WEBHOOK_SECRET_TESTMODE}`)
.replace("$STRIPE_TESTMODE$", `true`);
archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' });
} else {
const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8');
const devContent = ecosystemContent
.replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_PRODUCTION}`)
.replace("$STRIPE_PRIVATE_KEY$", `${STRIPE_PRIVATE_KEY_PRODUCTION}`)
.replace("$STRIPE_WEBHOOK_SECRET$", `${STRIPE_WEBHOOK_SECRET_PRODUCTION}`)
.replace("$STRIPE_TESTMODE$", `false`);
archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' });
}
// archive.file(LOCAL_PATH + '/package.json', { name: '/package.json' });
// archive.file(LOCAL_PATH + '/pnpm-lock.yaml', { name: '/pnpm-lock.yaml' });
// await archive.finalize();
archive.file(LOCAL_PATH + '/package.json', { name: '/package.json' });
archive.file(LOCAL_PATH + '/pnpm-lock.yaml', { name: '/pnpm-lock.yaml' });
await archive.finalize();
// await DeployHelper.connect();
await DeployHelper.connect();
// const { scp, ssh } = DeployHelper.instances();
const { scp, ssh } = DeployHelper.instances();
// console.log('Creating remote structure');
// console.log('Check existing');
// const remoteExist = await scp.exists(REMOTE_PATH);
// console.log('Exist', remoteExist);
// if (remoteExist) {
// console.log('Deleting');
// await DeployHelper.execute(`rm -r ${REMOTE_PATH}`);
// }
console.log('Creating remote structure');
console.log('Check existing');
const remoteExist = await scp.exists(REMOTE_PATH);
console.log('Exist', remoteExist);
if (remoteExist) {
console.log('Deleting');
await DeployHelper.execute(`rm -r ${REMOTE_PATH}`);
}
// console.log('Creating folder');
// await scp.mkdir(REMOTE_PATH);
console.log('Creating folder');
await scp.mkdir(REMOTE_PATH);
// console.log('Uploading zip file');
// await scp.uploadFile(TMP_PATH + '/' + ZIP_NAME, REMOTE_PATH + '/' + ZIP_NAME);
// scp.close();
console.log('Uploading zip file');
await scp.uploadFile(TMP_PATH + '/' + ZIP_NAME, REMOTE_PATH + '/' + ZIP_NAME);
scp.close();
// console.log('Cleaning local');
// fs.rmSync(TMP_PATH + '/' + ZIP_NAME, { force: true, recursive: true });
console.log('Cleaning local');
fs.rmSync(TMP_PATH + '/' + ZIP_NAME, { force: true, recursive: true });
// console.log('Extracting remote');
// await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip ${ZIP_NAME} && rm -r ${ZIP_NAME}`);
console.log('Extracting remote');
await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip ${ZIP_NAME} && rm -r ${ZIP_NAME}`);
// console.log('Installing remote');
// await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pnpm i`);
console.log('Installing remote');
await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pnpm i`);
// console.log('Executing remote');
// await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pm2 start ecosystem.config.js`);
console.log('Executing remote');
await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pm2 start ecosystem.config.js`);
// ssh.dispose();
ssh.dispose();
// }
}

View File

@@ -15,9 +15,8 @@ helper.copy('services/EmailService.ts');
helper.create('schema');
helper.copy('schema/UserSchema.ts');
helper.create('schema/project');
helper.copy('schema/project/ProjectSchema.ts');
helper.copy('schema/PremiumSchema.ts');
helper.copy('schema/UserLimitSchema.ts');
helper.create('data');
helper.copy('data/PLANS.ts');

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