4 Commits

Author SHA1 Message Date
Emily
7658dbe85c fix members 2025-03-26 16:15:46 +01:00
Emily
1f9ef5d18c add payment service 2025-03-26 15:30:22 +01:00
Emily
94a28b31d3 update shields 2025-03-24 18:54:15 +01:00
Emily
87c9aca5c4 shields update 2025-03-20 16:04:00 +01:00
67 changed files with 3294 additions and 38 deletions

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel']);
const props = defineProps<{ address: string }>();
async function deleteAddress() {
if (!props.address) return;
try {
const res = await $fetch('/api/shields/ip/delete', {
method: 'DELETE',
headers: useComputedHeaders({}).value,
body: JSON.stringify({ address: props.address })
});
emit('success');
} catch (ex: any) {
alert(ex.message);
emit('cancel');
}
}
</script>
<template>
<UModal :ui="{
strategy: 'override',
overlay: {
background: 'bg-lyx-background/85'
},
background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
ring: 'border-solid border-[1px] border-[#262626]'
}">
<div class="h-full flex flex-col gap-2 p-4">
<div class="flex flex-col gap-3">
<div class="font-semibold text-[1.1rem]"> IP Address delete </div>
<div> Are you sure to delete the blacklisted IP Address
<span class="font-semibold">{{ props.address }}</span>
</div>
<div class="flex justify-end gap-2">
<LyxUiButton type="secondary" @click="emit('cancel')">
Cancel
</LyxUiButton>
<LyxUiButton @click="deleteAddress()" type="danger">
Delete
</LyxUiButton>
</div>
</div>
</div>
</UModal>
</template>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel']);
const props = defineProps<{ domain: string }>();
async function deleteDomain() {
if (!props.domain) return;
try {
const res = await $fetch('/api/shields/domains/delete', {
method: 'DELETE',
headers: useComputedHeaders({}).value,
body: JSON.stringify({ domain: props.domain })
});
emit('success');
} catch (ex: any) {
alert(ex.message);
emit('cancel');
}
}
</script>
<template>
<UModal :ui="{
strategy: 'override',
overlay: {
background: 'bg-lyx-background/85'
},
background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
ring: 'border-solid border-[1px] border-[#262626]'
}">
<div class="h-full flex flex-col gap-2 p-4">
<div class="flex flex-col gap-3">
<div class="font-semibold text-[1.1rem]"> Domain delete </div>
<div> Are you sure to delete the whitelisted domain
<span class="font-semibold">{{ props.domain }}</span>
</div>
<div class="flex justify-end gap-2">
<LyxUiButton type="secondary" @click="emit('cancel')">
Cancel
</LyxUiButton>
<LyxUiButton @click="deleteDomain()" type="danger">
Delete
</LyxUiButton>
</div>
</div>
</div>
</UModal>
</template>

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ const sections: Section[] = [
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' }, { label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' }, { label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
{ label: 'Members', to: '/members', icon: 'fal fa-users' }, { label: 'Members', to: '/members', icon: 'fal fa-users' },
{ label: 'Shields', to: '/shields', icon: 'fal fa-shield' },
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' }, { label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
// { label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted }, // { label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },

View File

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

View File

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

View File

@@ -59,6 +59,13 @@ export default defineEventHandler(async event => {
const savedUser = await newUser.save(); const savedUser = await newUser.save();
setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('brevolist_add', { email: payload.email as string });
EmailServiceHelper.sendEmail(emailData);
});
setImmediate(() => { setImmediate(() => {
console.log('SENDING WELCOME EMAIL TO', payload.email); console.log('SENDING WELCOME EMAIL TO', payload.email);
if (!payload.email) return; if (!payload.email) return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
import { CountryBlacklistModel } from "~/shared/schema/shields/CountryBlacklistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const body = await readBody(event);
const { country } = body;
const removal = await CountryBlacklistModel.deleteOne({ project_id: data.project_id, country });
return { ok: removal.deletedCount == 1 };
});

View File

@@ -0,0 +1,8 @@
import { CountryBlacklistModel } from "~/shared/schema/shields/CountryBlacklistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const blacklist = await CountryBlacklistModel.find({ project_id: data.project_id });
return blacklist.map(e => e.toJSON());
});

View File

@@ -0,0 +1,21 @@
import { DomainWhitelistModel } from "~/shared/schema/shields/DomainWhitelistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const body = await readBody(event);
const { domain } = body;
if (domain.trim().length == 0) return setResponseStatus(event, 400, 'Domain is required');
const whitelist = await DomainWhitelistModel.updateOne({
project_id: data.project_id
},
{ $push: { domains: domain } },
{ upsert: true }
);
return { ok: true };
});

View File

@@ -0,0 +1,18 @@
import { DomainWhitelistModel } from "~/shared/schema/shields/DomainWhitelistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const body = await readBody(event);
const { domain } = body;
const removal = await DomainWhitelistModel.updateOne({
project_id: data.project_id
},
{ $pull: { domains: domain } },
);
return { ok: removal.modifiedCount == 1 };
});

View File

@@ -0,0 +1,10 @@
import { DomainWhitelistModel } from "~/shared/schema/shields/DomainWhitelistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const whitelist = await DomainWhitelistModel.findOne({ project_id: data.project_id });
if (!whitelist) return [];
const domains = whitelist.domains;
return domains;
});

View File

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

View File

@@ -0,0 +1,14 @@
import { AddressBlacklistModel } from "~/shared/schema/shields/AddressBlacklistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const body = await readBody(event);
const { address } = body;
const removal = await AddressBlacklistModel.deleteOne({ project_id: data.project_id, address });
return { ok: removal.deletedCount == 1 };
});

View File

@@ -0,0 +1,8 @@
import { AddressBlacklistModel } from "~/shared/schema/shields/AddressBlacklistSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
const blacklist = await AddressBlacklistModel.find({ project_id: data.project_id });
return blacklist.map(e => e.toJSON());
});

View File

@@ -8,6 +8,12 @@ import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
import { SessionModel } from "@schema/metrics/SessionSchema"; import { SessionModel } from "@schema/metrics/SessionSchema";
import StripeService from "~/server/services/StripeService"; import StripeService from "~/server/services/StripeService";
import { UserModel } from "@schema/UserSchema"; import { UserModel } from "@schema/UserSchema";
import { AddressBlacklistModel } from "~/shared/schema/shields/AddressBlacklistSchema";
import { DomainWhitelistModel } from "~/shared/schema/shields/DomainWhitelistSchema";
import { CountryBlacklistModel } from "~/shared/schema/shields/CountryBlacklistSchema";
import { BotTrafficOptionModel } from "~/shared/schema/shields/BotTrafficOptionSchema";
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
import { PasswordModel } from "~/shared/schema/PasswordSchema";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -19,6 +25,11 @@ export default defineEventHandler(async event => {
const premiumProjects = projects.filter(e => { return e.premium && e.premium_type != 0 }).length; 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'); if (premiumProjects > 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 });
for (const project of projects) { for (const project of projects) {
const project_id = project._id; const project_id = project._id;
await StripeService.deleteCustomer(project.customer_id); await StripeService.deleteCustomer(project.customer_id);
@@ -28,9 +39,16 @@ export default defineEventHandler(async event => {
const limitdeletation = await ProjectLimitModel.deleteMany({ project_id }); const limitdeletation = await ProjectLimitModel.deleteMany({ project_id });
const sessionsDeletation = await SessionModel.deleteMany({ project_id }); const sessionsDeletation = await SessionModel.deleteMany({ project_id });
const notifiesDeletation = await LimitNotifyModel.deleteMany({ project_id }); const notifiesDeletation = await LimitNotifyModel.deleteMany({ project_id });
const aiChatsDeletation = await AiChatModel.deleteMany({ project_id }); const aiChatsDeletation = await AiChatModel.deleteMany({ project_id });
const userDeletation = await UserModel.deleteOne({ _id: userData.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 });
} }

View File

@@ -35,7 +35,7 @@ export type GetRequestDataOptions = {
export type RequestDataScope = 'SCHEMA' | 'ANON' | 'SLICE' | 'RANGE' | 'OFFSET' | 'DOMAIN'; export type RequestDataScope = 'SCHEMA' | 'ANON' | 'SLICE' | 'RANGE' | 'OFFSET' | 'DOMAIN';
export type RequestDataPermissions = 'WEB' | 'EVENTS' | 'AI' | 'OWNER'; export type RequestDataPermissions = 'WEB' | 'EVENTS' | 'AI' | 'OWNER';
async function getAccessPermission(user_id: string, project: TProject): Promise<TPermission> { async function getAccessPermission(user_id: string, project: TProject, email: string): Promise<TPermission> {
if (!project) return { ai: false, domains: [], events: false, webAnalytics: false } if (!project) return { ai: false, domains: [], events: false, webAnalytics: false }
//TODO: Create table with admins //TODO: Create table with admins
@@ -44,17 +44,17 @@ async function getAccessPermission(user_id: string, project: TProject): Promise<
const owner = project.owner.toString(); const owner = project.owner.toString();
const project_id = project._id; const project_id = project._id;
if (owner === user_id) return { ai: true, domains: ['All domains'], events: true, webAnalytics: true } if (owner === user_id) return { ai: true, domains: ['All domains'], events: true, webAnalytics: true }
const member = await TeamMemberModel.findOne({ project_id, user_id }, { permission: 1 }); const member = await TeamMemberModel.findOne({ project_id, $or: [{ user_id }, { email }] }, { permission: 1 });
if (!member) return { ai: false, domains: [], events: false, webAnalytics: false } if (!member) return { ai: false, domains: [], events: false, webAnalytics: false }
return { ai: false, domains: [], events: false, webAnalytics: false, ...member.permission as any } return { ai: false, domains: [], events: false, webAnalytics: false, ...member.permission as any }
} }
async function hasAccessToProject(user_id: string, project: TProject) { async function hasAccessToProject(user_id: string, project: TProject, email: string) {
if (!project) return [false, 'NONE']; if (!project) return [false, 'NONE'];
const owner = project.owner.toString(); const owner = project.owner.toString();
const project_id = project._id; const project_id = project._id;
if (owner === user_id) return [true, 'OWNER']; if (owner === user_id) return [true, 'OWNER'];
const isGuest = await TeamMemberModel.exists({ project_id, user_id }); const isGuest = await TeamMemberModel.exists({ project_id, $or: [{ user_id }, { email }] });
if (isGuest) return [true, 'GUEST']; if (isGuest) return [true, 'GUEST'];
//TODO: Create table with admins //TODO: Create table with admins
@@ -128,14 +128,14 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, requir
if (user.id != project.owner.toString()) { if (user.id != project.owner.toString()) {
if (required_permissions.includes('OWNER')) return setResponseStatus(event, 403, 'ADMIN permission required'); if (required_permissions.includes('OWNER')) return setResponseStatus(event, 403, 'ADMIN permission required');
const hasAccess = await TeamMemberModel.findOne({ project_id, user_id: user.id }); const hasAccess = await TeamMemberModel.findOne({ project_id, $or: [{ user_id: user.id }, { email: user.user.email }] });
if (!hasAccess) return setResponseStatus(event, 403, 'No permissions'); if (!hasAccess) return setResponseStatus(event, 403, 'No permissions');
} }
if (required_permissions.length > 0 || requireDomain) { if (required_permissions.length > 0 || requireDomain) {
const permission = await getAccessPermission(user.id, project); const permission = await getAccessPermission(user.id, project, user.user.email);
if (required_permissions.includes('WEB') && permission.webAnalytics === false) { if (required_permissions.includes('WEB') && permission.webAnalytics === false) {
return setResponseStatus(event, 403, 'WEB permission required'); return setResponseStatus(event, 403, 'WEB permission required');
@@ -220,7 +220,7 @@ export async function getRequestDataOld(event: H3Event<EventHandlerRequest>, opt
if (pid !== "6643cd08a1854e3b81722ab5") { if (pid !== "6643cd08a1854e3b81722ab5") {
const [hasAccess, role] = await hasAccessToProject(user.id, project); const [hasAccess, role] = await hasAccessToProject(user.id, project, user.user.email);
if (!hasAccess) return setResponseStatus(event, 400, 'no access to project'); if (!hasAccess) return setResponseStatus(event, 400, 'no access to project');
if (role === 'GUEST' && !allowGuests) return setResponseStatus(event, 403, 'only owner can access this'); if (role === 'GUEST' && !allowGuests) return setResponseStatus(event, 403, 'only owner can access this');
} else { } else {

View File

@@ -1,11 +1,11 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema"; import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema"; import { TeamMemberModel } from "@schema/TeamMemberSchema";
export async function hasAccessToProject(user_id: string, project_id: string, project?: TProject) { export async function hasAccessToProject(user_id: string, project_id: string, email: string, project?: TProject) {
const targetProject = project ?? await ProjectModel.findById(project_id, { owner: true }); const targetProject = project ?? await ProjectModel.findById(project_id, { owner: true });
if (!targetProject) return [false, 'NONE']; if (!targetProject) return [false, 'NONE'];
if (targetProject.owner.toString() === user_id) return [true, 'OWNER']; if (targetProject.owner.toString() === user_id) return [true, 'OWNER'];
const isGuest = await TeamMemberModel.exists({ project_id, user_id }); const isGuest = await TeamMemberModel.exists({ project_id, $or: [{ user_id }, { email }] });
if (isGuest) return [true, 'GUEST']; if (isGuest) return [true, 'GUEST'];
return [false, 'NONE']; return [false, 'NONE'];
} }

View File

@@ -55,6 +55,7 @@ export class EmailService {
try { try {
await this.apiContacts.createContact({ email }); await this.apiContacts.createContact({ email });
await this.apiContacts.addContactToList(12, { emails: [email] }) await this.apiContacts.addContactToList(12, { emails: [email] })
return true;
} catch (ex) { } catch (ex) {
console.error('ERROR ADDING CONTACT', ex); console.error('ERROR ADDING CONTACT', ex);
return false; return false;

View File

@@ -58,7 +58,7 @@ app.post('/send/invite/noaccount', express.json(), async (req, res) => {
} }
}); });
app.post('/brevolist/add', express.json(), async (req, res) => { app.post('/send/brevolist/add', express.json(), async (req, res) => {
try { try {
const { email } = req.body; const { email } = req.body;
const ok = await EmailService.createContact(email); const ok = await EmailService.createContact(email);

View File

@@ -14,6 +14,9 @@
"consumer:shared": "ts-node scripts/consumer/shared.ts", "consumer:shared": "ts-node scripts/consumer/shared.ts",
"consumer:deploy": "ts-node scripts/consumer/deploy.ts", "consumer:deploy": "ts-node scripts/consumer/deploy.ts",
"payments:shared": "ts-node scripts/payments/shared.ts",
"payments:deploy": "ts-node scripts/payments/deploy.ts",
"email:deploy": "ts-node scripts/email/deploy.ts" "email:deploy": "ts-node scripts/email/deploy.ts"
}, },
"keywords": [], "keywords": [],

9
payments/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
node_modules
static
ecosystem.config.cjs
ecosystem.config.js
dist
start_dev.js
package-lock.json
build_all.bat
src/shared

28
payments/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "payments",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"dev": "node scripts/start_dev.js",
"dev_prod": "node scripts/start_dev_prod.js",
"compile": "tsc",
"build": "npm run compile",
"workspace:shared": "ts-node ../scripts/payments/shared.ts",
"workspace:deploy": "ts-node ../scripts/payments/deploy.ts"
},
"keywords": [],
"author": "Emily",
"license": "MIT",
"dependencies": {
"@types/express": "^5.0.1",
"cors": "^2.8.5",
"express": "^4.21.2",
"mongoose": "^8.13.0",
"stripe": "^17.7.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/cors": "^2.8.17"
}
}

901
payments/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,901 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@types/express':
specifier: ^5.0.1
version: 5.0.1
cors:
specifier: ^2.8.5
version: 2.8.5
express:
specifier: ^4.21.2
version: 4.21.2
mongoose:
specifier: ^8.13.0
version: 8.13.0
stripe:
specifier: ^17.7.0
version: 17.7.0
zod:
specifier: ^3.24.2
version: 3.24.2
devDependencies:
'@types/cors':
specifier: ^2.8.17
version: 2.8.17
packages:
'@mongodb-js/saslprep@1.2.0':
resolution: {integrity: sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==}
'@types/body-parser@1.19.5':
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cors@2.8.17':
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
'@types/express-serve-static-core@5.0.6':
resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==}
'@types/express@5.0.1':
resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==}
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/node@22.13.13':
resolution: {integrity: sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==}
'@types/qs@6.9.18':
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/send@0.17.4':
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
'@types/serve-static@1.15.7':
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
'@types/webidl-conversions@7.0.3':
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
'@types/whatwg-url@11.0.5':
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
bson@6.10.3:
resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==}
engines: {node: '>=16.20.1'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
cookie@0.7.1:
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
engines: {node: '>= 0.6'}
cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
express@4.21.2:
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
engines: {node: '>= 0.10.0'}
finalhandler@1.3.1:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
kareem@2.6.3:
resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==}
engines: {node: '>=12.0.0'}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
merge-descriptors@1.0.3:
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
hasBin: true
mongodb-connection-string-url@3.0.2:
resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==}
mongodb@6.15.0:
resolution: {integrity: sha512-ifBhQ0rRzHDzqp9jAQP6OwHSH7dbYIQjD3SbJs9YYk9AikKEettW/9s/tbSFDTpXcRbF+u1aLrhHxDFaYtZpFQ==}
engines: {node: '>=16.20.1'}
peerDependencies:
'@aws-sdk/credential-providers': ^3.188.0
'@mongodb-js/zstd': ^1.1.0 || ^2.0.0
gcp-metadata: ^5.2.0
kerberos: ^2.0.1
mongodb-client-encryption: '>=6.0.0 <7'
snappy: ^7.2.2
socks: ^2.7.1
peerDependenciesMeta:
'@aws-sdk/credential-providers':
optional: true
'@mongodb-js/zstd':
optional: true
gcp-metadata:
optional: true
kerberos:
optional: true
mongodb-client-encryption:
optional: true
snappy:
optional: true
socks:
optional: true
mongoose@8.13.0:
resolution: {integrity: sha512-e/iYV1mPeOkg+SWAMHzt3t42/EZyER3OB1H2pjP9C3vQ+Qb5DMeV9Kb+YCUycKgScA3fbwL7dKG4EpinGlg21g==}
engines: {node: '>=16.20.1'}
mpath@0.9.0:
resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==}
engines: {node: '>=4.0.0'}
mquery@5.0.0:
resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==}
engines: {node: '>=14.0.0'}
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
path-to-regexp@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
serve-static@1.16.2:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
sift@17.1.3:
resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==}
sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
stripe@17.7.0:
resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==}
engines: {node: '>=12.*'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tr46@5.1.0:
resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==}
engines: {node: '>=18'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
zod@3.24.2:
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
snapshots:
'@mongodb-js/saslprep@1.2.0':
dependencies:
sparse-bitfield: 3.0.3
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.13.13
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.13.13
'@types/cors@2.8.17':
dependencies:
'@types/node': 22.13.13
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 22.13.13
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
'@types/express@5.0.1':
dependencies:
'@types/body-parser': 1.19.5
'@types/express-serve-static-core': 5.0.6
'@types/serve-static': 1.15.7
'@types/http-errors@2.0.4': {}
'@types/mime@1.3.5': {}
'@types/node@22.13.13':
dependencies:
undici-types: 6.20.0
'@types/qs@6.9.18': {}
'@types/range-parser@1.2.7': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.13.13
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 22.13.13
'@types/send': 0.17.4
'@types/webidl-conversions@7.0.3': {}
'@types/whatwg-url@11.0.5':
dependencies:
'@types/webidl-conversions': 7.0.3
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
array-flatten@1.1.1: {}
body-parser@1.20.3:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.13.0
raw-body: 2.5.2
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
bson@6.10.3: {}
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
content-disposition@0.5.4:
dependencies:
safe-buffer: 5.2.1
content-type@1.0.5: {}
cookie-signature@1.0.6: {}
cookie@0.7.1: {}
cors@2.8.5:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
debug@2.6.9:
dependencies:
ms: 2.0.0
debug@4.4.0:
dependencies:
ms: 2.1.3
depd@2.0.0: {}
destroy@1.2.0: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
ee-first@1.1.1: {}
encodeurl@1.0.2: {}
encodeurl@2.0.0: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
escape-html@1.0.3: {}
etag@1.8.1: {}
express@4.21.2:
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.3
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.7.1
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.3.1
fresh: 0.5.2
http-errors: 2.0.0
merge-descriptors: 1.0.3
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.12
proxy-addr: 2.0.7
qs: 6.13.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.19.0
serve-static: 1.16.2
setprototypeof: 1.2.0
statuses: 2.0.1
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
finalhandler@1.3.1:
dependencies:
debug: 2.6.9
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
forwarded@0.2.0: {}
fresh@0.5.2: {}
function-bind@1.1.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
gopd@1.2.0: {}
has-symbols@1.1.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
http-errors@2.0.0:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
inherits@2.0.4: {}
ipaddr.js@1.9.1: {}
kareem@2.6.3: {}
math-intrinsics@1.1.0: {}
media-typer@0.3.0: {}
memory-pager@1.5.0: {}
merge-descriptors@1.0.3: {}
methods@1.1.2: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime@1.6.0: {}
mongodb-connection-string-url@3.0.2:
dependencies:
'@types/whatwg-url': 11.0.5
whatwg-url: 14.2.0
mongodb@6.15.0:
dependencies:
'@mongodb-js/saslprep': 1.2.0
bson: 6.10.3
mongodb-connection-string-url: 3.0.2
mongoose@8.13.0:
dependencies:
bson: 6.10.3
kareem: 2.6.3
mongodb: 6.15.0
mpath: 0.9.0
mquery: 5.0.0
ms: 2.1.3
sift: 17.1.3
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- gcp-metadata
- kerberos
- mongodb-client-encryption
- snappy
- socks
- supports-color
mpath@0.9.0: {}
mquery@5.0.0:
dependencies:
debug: 4.4.0
transitivePeerDependencies:
- supports-color
ms@2.0.0: {}
ms@2.1.3: {}
negotiator@0.6.3: {}
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
parseurl@1.3.3: {}
path-to-regexp@0.1.12: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
punycode@2.3.1: {}
qs@6.13.0:
dependencies:
side-channel: 1.1.0
qs@6.14.0:
dependencies:
side-channel: 1.1.0
range-parser@1.2.1: {}
raw-body@2.5.2:
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
send@0.19.0:
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
serve-static@1.16.2:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.19.0
transitivePeerDependencies:
- supports-color
setprototypeof@1.2.0: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
side-channel@1.1.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-list: 1.0.0
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
sift@17.1.3: {}
sparse-bitfield@3.0.3:
dependencies:
memory-pager: 1.5.0
statuses@2.0.1: {}
stripe@17.7.0:
dependencies:
'@types/node': 22.13.13
qs: 6.14.0
toidentifier@1.0.1: {}
tr46@5.1.0:
dependencies:
punycode: 2.3.1
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
undici-types@6.20.0: {}
unpipe@1.0.0: {}
utils-merge@1.0.1: {}
vary@1.1.2: {}
webidl-conversions@7.0.0: {}
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.0
webidl-conversions: 7.0.0
zod@3.24.2: {}

6
payments/src/Utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { Response } from "express";
export function sendJson(res: Response, status: number, data: Record<string, any>): void {
res.status(status).json(data);
}

0
payments/src/index.ts Normal file
View File

View File

@@ -0,0 +1,43 @@
import { json, Router } from 'express';
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';
export const paymentRouter = Router();
export const ZBodyCreatePayment = z.object({
pid: z.string(),
plan_id: z.number()
})
paymentRouter.post('/create', 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 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
);
if (!checkout) return sendJson(res, 400, { error: 'cannot create payment' });
return sendJson(res, 200, { url: checkout.url });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});

View File

@@ -0,0 +1,19 @@
import { json, Router } from 'express';
export const webhookRouter = Router();
webhookRouter.get('/', json(), async (req, res) => {
try {
const signature = req.header('stripe-signature');
if (!signature) {
console.error('No signature on the webhook')
}
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});

View File

@@ -0,0 +1,209 @@
import Stripe from "stripe";
class StripeService {
private stripe?: Stripe;
private privateKey?: string;
private webhookSecret?: string;
public testMode?: boolean;
init(privateKey: string, webhookSecret: string, testMode: boolean = false) {
this.privateKey = privateKey;
this.webhookSecret = webhookSecret;
this.stripe = new Stripe(this.privateKey);
this.testMode = testMode;
}
parseWebhook(body: any, sig: string) {
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.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.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.stripe) throw Error('Stripe not initialized');
const priceData = await this.stripe.prices.retrieve(priceId);
return priceData;
}
async deleteSubscription(subscriptionId: string) {
if (!this.stripe) throw Error('Stripe not initialized');
const subscription = await this.stripe.subscriptions.cancel(subscriptionId);
return subscription;
}
async getSubscription(subscriptionId: string) {
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.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.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.stripe) throw Error('Stripe not initialized');
const customer = await this.stripe.customers.retrieve(customer_id, { expand: [] })
return customer;
}
async createCustomer(email: string) {
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.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.stripe) throw Error('Stripe not initialized');
const { deleted } = await this.stripe.customers.del(customer_id);
return deleted;
}
// async createStripeCode(plan: PREMIUM_TAG) {
// 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;

13
payments/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "NodeNext",
"target": "ESNext",
"outDir": "dist"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -2,6 +2,7 @@
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.19.2", "express": "^4.19.2",
"mongoose": "^8.12.1",
"redis": "^4.7.0" "redis": "^4.7.0"
}, },
"devDependencies": { "devDependencies": {

173
producer/pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
express: express:
specifier: ^4.19.2 specifier: ^4.19.2
version: 4.19.2 version: 4.19.2
mongoose:
specifier: ^8.12.1
version: 8.12.1
redis: redis:
specifier: ^4.7.0 specifier: ^4.7.0
version: 4.7.0 version: 4.7.0
@@ -50,6 +53,9 @@ packages:
'@jridgewell/trace-mapping@0.3.9': '@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@mongodb-js/saslprep@1.2.0':
resolution: {integrity: sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==}
'@redis/bloom@1.2.0': '@redis/bloom@1.2.0':
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies: peerDependencies:
@@ -127,6 +133,12 @@ packages:
'@types/serve-static@1.15.7': '@types/serve-static@1.15.7':
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
'@types/webidl-conversions@7.0.3':
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
'@types/whatwg-url@11.0.5':
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
accepts@1.3.8: accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -150,6 +162,10 @@ packages:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
bson@6.10.3:
resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==}
engines: {node: '>=16.20.1'}
bytes@3.1.2: bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -192,6 +208,15 @@ packages:
supports-color: supports-color:
optional: true optional: true
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
define-data-property@1.1.4: define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -290,6 +315,10 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
kareem@2.6.3:
resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==}
engines: {node: '>=12.0.0'}
make-error@1.3.6: make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
@@ -297,6 +326,9 @@ packages:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
merge-descriptors@1.0.1: merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
@@ -317,6 +349,48 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
hasBin: true hasBin: true
mongodb-connection-string-url@3.0.2:
resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==}
mongodb@6.14.2:
resolution: {integrity: sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==}
engines: {node: '>=16.20.1'}
peerDependencies:
'@aws-sdk/credential-providers': ^3.188.0
'@mongodb-js/zstd': ^1.1.0 || ^2.0.0
gcp-metadata: ^5.2.0
kerberos: ^2.0.1
mongodb-client-encryption: '>=6.0.0 <7'
snappy: ^7.2.2
socks: ^2.7.1
peerDependenciesMeta:
'@aws-sdk/credential-providers':
optional: true
'@mongodb-js/zstd':
optional: true
gcp-metadata:
optional: true
kerberos:
optional: true
mongodb-client-encryption:
optional: true
snappy:
optional: true
socks:
optional: true
mongoose@8.12.1:
resolution: {integrity: sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==}
engines: {node: '>=16.20.1'}
mpath@0.9.0:
resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==}
engines: {node: '>=4.0.0'}
mquery@5.0.0:
resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==}
engines: {node: '>=14.0.0'}
ms@2.0.0: ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@@ -349,6 +423,10 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.11.0: qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
@@ -389,6 +467,12 @@ packages:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
sift@17.1.3:
resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==}
sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
statuses@2.0.1: statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -397,6 +481,10 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
tr46@5.1.0:
resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==}
engines: {node: '>=18'}
ts-node@10.9.2: ts-node@10.9.2:
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true hasBin: true
@@ -438,6 +526,14 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
yallist@4.0.0: yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
@@ -460,6 +556,10 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
'@mongodb-js/saslprep@1.2.0':
dependencies:
sparse-bitfield: 3.0.3
'@redis/bloom@1.2.0(@redis/client@1.6.0)': '@redis/bloom@1.2.0(@redis/client@1.6.0)':
dependencies: dependencies:
'@redis/client': 1.6.0 '@redis/client': 1.6.0
@@ -544,6 +644,12 @@ snapshots:
'@types/node': 20.14.2 '@types/node': 20.14.2
'@types/send': 0.17.4 '@types/send': 0.17.4
'@types/webidl-conversions@7.0.3': {}
'@types/whatwg-url@11.0.5':
dependencies:
'@types/webidl-conversions': 7.0.3
accepts@1.3.8: accepts@1.3.8:
dependencies: dependencies:
mime-types: 2.1.35 mime-types: 2.1.35
@@ -576,6 +682,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
bson@6.10.3: {}
bytes@3.1.2: {} bytes@3.1.2: {}
call-bind@1.0.7: call-bind@1.0.7:
@@ -609,6 +717,10 @@ snapshots:
dependencies: dependencies:
ms: 2.0.0 ms: 2.0.0
debug@4.4.0:
dependencies:
ms: 2.1.3
define-data-property@1.1.4: define-data-property@1.1.4:
dependencies: dependencies:
es-define-property: 1.0.0 es-define-property: 1.0.0
@@ -731,10 +843,14 @@ snapshots:
ipaddr.js@1.9.1: {} ipaddr.js@1.9.1: {}
kareem@2.6.3: {}
make-error@1.3.6: {} make-error@1.3.6: {}
media-typer@0.3.0: {} media-typer@0.3.0: {}
memory-pager@1.5.0: {}
merge-descriptors@1.0.1: {} merge-descriptors@1.0.1: {}
methods@1.1.2: {} methods@1.1.2: {}
@@ -747,6 +863,44 @@ snapshots:
mime@1.6.0: {} mime@1.6.0: {}
mongodb-connection-string-url@3.0.2:
dependencies:
'@types/whatwg-url': 11.0.5
whatwg-url: 14.2.0
mongodb@6.14.2:
dependencies:
'@mongodb-js/saslprep': 1.2.0
bson: 6.10.3
mongodb-connection-string-url: 3.0.2
mongoose@8.12.1:
dependencies:
bson: 6.10.3
kareem: 2.6.3
mongodb: 6.14.2
mpath: 0.9.0
mquery: 5.0.0
ms: 2.1.3
sift: 17.1.3
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- gcp-metadata
- kerberos
- mongodb-client-encryption
- snappy
- socks
- supports-color
mpath@0.9.0: {}
mquery@5.0.0:
dependencies:
debug: 4.4.0
transitivePeerDependencies:
- supports-color
ms@2.0.0: {} ms@2.0.0: {}
ms@2.1.3: {} ms@2.1.3: {}
@@ -770,6 +924,8 @@ snapshots:
forwarded: 0.2.0 forwarded: 0.2.0
ipaddr.js: 1.9.1 ipaddr.js: 1.9.1
punycode@2.3.1: {}
qs@6.11.0: qs@6.11.0:
dependencies: dependencies:
side-channel: 1.0.6 side-channel: 1.0.6
@@ -841,10 +997,20 @@ snapshots:
get-intrinsic: 1.2.4 get-intrinsic: 1.2.4
object-inspect: 1.13.1 object-inspect: 1.13.1
sift@17.1.3: {}
sparse-bitfield@3.0.3:
dependencies:
memory-pager: 1.5.0
statuses@2.0.1: {} statuses@2.0.1: {}
toidentifier@1.0.1: {} toidentifier@1.0.1: {}
tr46@5.1.0:
dependencies:
punycode: 2.3.1
ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5): ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5):
dependencies: dependencies:
'@cspotcode/source-map-support': 0.8.1 '@cspotcode/source-map-support': 0.8.1
@@ -880,6 +1046,13 @@ snapshots:
vary@1.1.2: {} vary@1.1.2: {}
webidl-conversions@7.0.0: {}
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.0
webidl-conversions: 7.0.0
yallist@4.0.0: {} yallist@4.0.0: {}
yn@3.1.1: {} yn@3.1.1: {}

637
producer/src/controller.ts Normal file
View File

@@ -0,0 +1,637 @@
import { DomainWhitelistModel } from "./shared/schema/shields/DomainWhitelistSchema";
import { AddressBlacklistModel } from "./shared/schema/shields/AddressBlacklistSchema";
import { BotTrafficOptionModel } from "./shared/schema/shields/BotTrafficOptionSchema";
const BOT_PATTERNS = [
"Googlebot\\/",
"Googlebot-Mobile",
"Googlebot-Image",
"Googlebot-News",
"Googlebot-Video",
"AdsBot-Google([^-]|$)",
"AdsBot-Google-Mobile",
"Feedfetcher-Google",
"Mediapartners-Google",
"Mediapartners \\(Googlebot\\)",
"APIs-Google",
"Google-InspectionTool",
"Storebot-Google",
"GoogleOther",
"bingbot",
"Slurp",
"[wW]get",
"LinkedInBot",
"Python-urllib",
"python-requests",
"aiohttp",
"httpx",
"libwww-perl",
"httpunit",
"Nutch",
"Go-http-client",
"phpcrawl",
"msnbot",
"jyxobot",
"FAST-WebCrawler",
"FAST Enterprise Crawler",
"BIGLOTRON",
"Teoma",
"convera",
"seekbot",
"Gigabot",
"Gigablast",
"exabot",
"ia_archiver",
"GingerCrawler",
"webmon ",
"HTTrack",
"grub\\.org",
"UsineNouvelleCrawler",
"antibot",
"netresearchserver",
"speedy",
"fluffy",
"findlink",
"msrbot",
"panscient",
"yacybot",
"AISearchBot",
"ips-agent",
"tagoobot",
"MJ12bot",
"woriobot",
"yanga",
"buzzbot",
"mlbot",
"yandex\\.com\\/bots",
"purebot",
"Linguee Bot",
"CyberPatrol",
"voilabot",
"Baiduspider",
"citeseerxbot",
"spbot",
"twengabot",
"postrank",
"Turnitin",
"scribdbot",
"page2rss",
"sitebot",
"linkdex",
"Adidxbot",
"ezooms",
"dotbot",
"Mail\\.RU_Bot",
"discobot",
"heritrix",
"findthatfile",
"europarchive\\.org",
"NerdByNature\\.Bot",
"(sistrix|SISTRIX) [cC]rawler",
"Ahrefs(Bot|SiteAudit)",
"fuelbot",
"CrunchBot",
"IndeedBot",
"mappydata",
"woobot",
"ZoominfoBot",
"PrivacyAwareBot",
"Multiviewbot",
"SWIMGBot",
"Grobbot",
"eright",
"Apercite",
"semanticbot",
"Aboundex",
"domaincrawler",
"wbsearchbot",
"summify",
"CCBot",
"edisterbot",
"SeznamBot",
"ec2linkfinder",
"gslfbot",
"aiHitBot",
"intelium_bot",
"facebookexternalhit",
"Yeti",
"RetrevoPageAnalyzer",
"lb-spider",
"Sogou",
"lssbot",
"careerbot",
"wotbox",
"wocbot",
"ichiro",
"DuckDuckBot",
"lssrocketcrawler",
"drupact",
"webcompanycrawler",
"acoonbot",
"openindexspider",
"gnam gnam spider",
"web-archive-net\\.com\\.bot",
"backlinkcrawler",
"coccoc",
"integromedb",
"content crawler spider",
"toplistbot",
"it2media-domain-crawler",
"ip-web-crawler\\.com",
"siteexplorer\\.info",
"elisabot",
"proximic",
"changedetection",
"arabot",
"WeSEE:Search",
"niki-bot",
"CrystalSemanticsBot",
"rogerbot",
"360Spider",
"psbot",
"InterfaxScanBot",
"CC Metadata Scaper",
"g00g1e\\.net",
"GrapeshotCrawler",
"urlappendbot",
"brainobot",
"fr-crawler",
"binlar",
"SimpleCrawler",
"Twitterbot",
"cXensebot",
"smtbot",
"bnf\\.fr_bot",
"A6-Indexer",
"ADmantX",
"Facebot",
"OrangeBot\\/",
"memorybot",
"AdvBot",
"MegaIndex",
"SemanticScholarBot",
"ltx71",
"nerdybot",
"xovibot",
"BUbiNG",
"Qwantify",
"archive\\.org_bot",
"Applebot",
"TweetmemeBot",
"crawler4j",
"findxbot",
"S[eE][mM]rushBot",
"yoozBot",
"lipperhey",
"Y!J",
"Domain Re-Animator Bot",
"AddThis",
"Screaming Frog SEO Spider",
"MetaURI",
"Scrapy",
"Livelap[bB]ot",
"OpenHoseBot",
"CapsuleChecker",
"collection@infegy\\.com",
"IstellaBot",
"DeuSu\\/",
"betaBot",
"Cliqzbot\\/",
"MojeekBot\\/",
"netEstate NE Crawler",
"SafeSearch microdata crawler",
"Gluten Free Crawler\\/",
"Sonic",
"Sysomos",
"Trove",
"deadlinkchecker",
"Slack-ImgProxy",
"Embedly",
"RankActiveLinkBot",
"iskanie",
"SafeDNSBot",
"SkypeUriPreview",
"Veoozbot",
"Slackbot",
"redditbot",
"datagnionbot",
"Google-Adwords-Instant",
"adbeat_bot",
"WhatsApp",
"contxbot",
"pinterest\\.com\\/bot",
"electricmonk",
"GarlikCrawler",
"BingPreview\\/",
"vebidoobot",
"FemtosearchBot",
"Yahoo Link Preview",
"MetaJobBot",
"DomainStatsBot",
"mindUpBot",
"Daum\\/",
"Jugendschutzprogramm-Crawler",
"Xenu Link Sleuth",
"Pcore-HTTP",
"moatbot",
"KosmioBot",
"[pP]ingdom",
"AppInsights",
"PhantomJS",
"Gowikibot",
"PiplBot",
"Discordbot",
"TelegramBot",
"Jetslide",
"newsharecounts",
"James BOT",
"Bark[rR]owler",
"TinEye",
"SocialRankIOBot",
"trendictionbot",
"Ocarinabot",
"epicbot",
"Primalbot",
"DuckDuckGo-Favicons-Bot",
"GnowitNewsbot",
"Leikibot",
"LinkArchiver",
"YaK\\/",
"PaperLiBot",
"Digg Deeper",
"dcrawl",
"Snacktory",
"AndersPinkBot",
"Fyrebot",
"EveryoneSocialBot",
"Mediatoolkitbot",
"Luminator-robots",
"ExtLinksBot",
"SurveyBot",
"NING\\/",
"okhttp",
"Nuzzel",
"omgili",
"PocketParser",
"YisouSpider",
"um-LN",
"ToutiaoSpider",
"MuckRack",
"Jamie's Spider",
"AHC\\/",
"NetcraftSurveyAgent",
"Laserlikebot",
"^Apache-HttpClient",
"AppEngine-Google",
"Jetty",
"Upflow",
"Thinklab",
"Traackr\\.com",
"Twurly",
"Mastodon",
"http_get",
"DnyzBot",
"botify",
"007ac9 Crawler",
"BehloolBot",
"BrandVerity",
"check_http",
"BDCbot",
"ZumBot",
"EZID",
"ICC-Crawler",
"ArchiveBot",
"^LCC ",
"filterdb\\.iss\\.net\\/crawler",
"BLP_bbot",
"BomboraBot",
"Buck\\/",
"Companybook-Crawler",
"Genieo",
"magpie-crawler",
"MeltwaterNews",
"Moreover",
"newspaper\\/",
"ScoutJet",
"(^| )sentry\\/",
"StorygizeBot",
"UptimeRobot",
"OutclicksBot",
"seoscanners",
"Hatena",
"Google Web Preview",
"MauiBot",
"AlphaBot",
"SBL-BOT",
"IAS crawler",
"adscanner",
"Netvibes",
"acapbot",
"Baidu-YunGuanCe",
"bitlybot",
"blogmuraBot",
"Bot\\.AraTurka\\.com",
"bot-pge\\.chlooe\\.com",
"BoxcarBot",
"BTWebClient",
"ContextAd Bot",
"Digincore bot",
"Disqus",
"Feedly",
"Fetch\\/",
"Fever",
"Flamingo_SearchEngine",
"FlipboardProxy",
"g2reader-bot",
"G2 Web Services",
"imrbot",
"K7MLWCBot",
"Kemvibot",
"Landau-Media-Spider",
"linkapediabot",
"vkShare",
"Siteimprove\\.com",
"BLEXBot\\/",
"DareBoost",
"ZuperlistBot\\/",
"Miniflux\\/",
"Feedspot",
"Diffbot\\/",
"SEOkicks",
"tracemyfile",
"Nimbostratus-Bot",
"zgrab",
"PR-CY\\.RU",
"AdsTxtCrawler",
"Datafeedwatch",
"Zabbix",
"TangibleeBot",
"google-xrawler",
"axios",
"Amazon CloudFront",
"Pulsepoint",
"CloudFlare-AlwaysOnline",
"Cloudflare-Healthchecks",
"Cloudflare-Traffic-Manager",
"CloudFlare-Prefetch",
"Cloudflare-SSLDetector",
"https:\\/\\/developers\\.cloudflare\\.com\\/security-center\\/",
"Google-Structured-Data-Testing-Tool",
"WordupInfoSearch",
"WebDataStats",
"HttpUrlConnection",
"ZoomBot",
"VelenPublicWebCrawler",
"MoodleBot",
"jpg-newsbot",
"outbrain",
"W3C_Validator",
"Validator\\.nu",
"W3C-checklink",
"W3C-mobileOK",
"W3C_I18n-Checker",
"FeedValidator",
"W3C_CSS_Validator",
"W3C_Unicorn",
"Google-PhysicalWeb",
"Blackboard",
"ICBot\\/",
"BazQux",
"Twingly",
"Rivva",
"Experibot",
"awesomecrawler",
"Dataprovider\\.com",
"GroupHigh\\/",
"theoldreader\\.com",
"AnyEvent",
"Uptimebot\\.org",
"Nmap Scripting Engine",
"2ip\\.ru",
"Clickagy",
"Caliperbot",
"MBCrawler",
"online-webceo-bot",
"B2B Bot",
"AddSearchBot",
"Google Favicon",
"HubSpot",
"Chrome-Lighthouse",
"HeadlessChrome",
"CheckMarkNetwork\\/",
"www\\.uptime\\.com",
"Streamline3Bot\\/",
"serpstatbot\\/",
"MixnodeCache\\/",
"^curl",
"SimpleScraper",
"RSSingBot",
"Jooblebot",
"fedoraplanet",
"Friendica",
"NextCloud",
"Tiny Tiny RSS",
"RegionStuttgartBot",
"Bytespider",
"Datanyze",
"Google-Site-Verification",
"TrendsmapResolver",
"tweetedtimes",
"NTENTbot",
"Gwene",
"SimplePie",
"SearchAtlas",
"Superfeedr",
"feedbot",
"UT-Dorkbot",
"Amazonbot",
"SerendeputyBot",
"Eyeotabot",
"officestorebot",
"Neticle Crawler",
"SurdotlyBot",
"LinkisBot",
"AwarioSmartBot",
"AwarioRssBot",
"RyteBot",
"FreeWebMonitoring SiteChecker",
"AspiegelBot",
"NAVER Blog Rssbot",
"zenback bot",
"SentiBot",
"Domains Project\\/",
"Pandalytics",
"VKRobot",
"bidswitchbot",
"tigerbot",
"NIXStatsbot",
"Atom Feed Robot",
"[Cc]urebot",
"PagePeeker\\/",
"Vigil\\/",
"rssbot\\/",
"startmebot\\/",
"JobboerseBot",
"seewithkids",
"NINJA bot",
"Cutbot",
"BublupBot",
"BrandONbot",
"RidderBot",
"Taboolabot",
"Dubbotbot",
"FindITAnswersbot",
"infoobot",
"Refindbot",
"BlogTraffic\\/\\d\\.\\d+ Feed-Fetcher",
"SeobilityBot",
"Cincraw",
"Dragonbot",
"VoluumDSP-content-bot",
"FreshRSS",
"BitBot",
"^PHP-Curl-Class",
"Google-Certificates-Bridge",
"centurybot",
"Viber",
"e\\.ventures Investment Crawler",
"evc-batch",
"PetalBot",
"virustotal",
"(^| )PTST\\/",
"minicrawler",
"Cookiebot",
"trovitBot",
"seostar\\.co",
"IonCrawl",
"Uptime-Kuma",
"Seekport",
"FreshpingBot",
"Feedbin",
"CriteoBot",
"Snap URL Preview Service",
"Better Uptime Bot",
"RuxitSynthetic",
"Google-Read-Aloud",
"Valve\\/Steam",
"OdklBot\\/",
"GPTBot",
"ChatGPT-User",
"OAI-SearchBot",
"YandexRenderResourcesBot\\/",
"LightspeedSystemsCrawler",
"ev-crawler\\/",
"BitSightBot\\/",
"woorankreview\\/",
"Google-Safety",
"AwarioBot",
"DataForSeoBot",
"Linespider",
"WellKnownBot",
"A Patent Crawler",
"StractBot",
"search\\.marginalia\\.nu",
"YouBot",
"Nicecrawler",
"Neevabot",
"BrightEdge Crawler",
"SiteCheckerBotCrawler",
"TombaPublicWebCrawler",
"CrawlyProjectCrawler",
"KomodiaBot",
"KStandBot",
"CISPA Webcrawler",
"MTRobot",
"hyscore\\.io",
"AlexandriaOrgBot",
"2ip bot",
"Yellowbrandprotectionbot",
"SEOlizer",
"vuhuvBot",
"INETDEX-BOT",
"Synapse",
"t3versionsBot",
"deepnoc",
"Cocolyzebot",
"hypestat",
"ReverseEngineeringBot",
"sempi\\.tech",
"Iframely",
"MetaInspector",
"node-fetch",
"l9explore",
"python-opengraph",
"OpenGraphCheck",
"developers\\.google\\.com\\/\\+\\/web\\/snippet",
"SenutoBot",
"MaCoCu",
"NewsBlur",
"inoreader",
"NetSystemsResearch",
"PageThing",
"WordPress\\/",
"PhxBot",
"ImagesiftBot",
"Expanse",
"InternetMeasurement",
"^BW\\/",
"GeedoBot",
"Audisto Crawler",
"PerplexityBot\\/",
"[cC]laude[bB]ot",
"Monsidobot",
"GroupMeBot",
"Vercelbot",
"vercel-screenshot",
"facebookcatalog\\/",
"meta-externalagent\\/",
"meta-externalfetcher\\/",
"AcademicBotRTU",
"KeybaseBot",
"Lemmy",
"CookieHubScan",
"Hydrozen\\.io",
"HTTP Banner Detection",
"SummalyBot",
"MicrosoftPreview\\/",
"GeedoProductSearch",
"TikTokSpider"
]
function isBot(userAgent: string) {
for (const pattern of BOT_PATTERNS) {
const regexp = new RegExp(pattern);
const result = userAgent.match(regexp);
if (result != null) return true;
}
return false;
}
export async function isAllowedToLog(project_id: string, website: string, ip: string, userAgent: string) {
const blacklistData = await AddressBlacklistModel.find({ project_id }, { address: 1 });
for (const blacklistedData of blacklistData) {
if (blacklistedData.address == ip) return false;
}
const botOptions = await BotTrafficOptionModel.findOne({ project_id }, { block: 1 });
if (botOptions && botOptions.block) {
const isbot = isBot(userAgent);
if (isbot) return false;
}
const whitelist = await DomainWhitelistModel.findOne({ project_id }, { domains: 1 });
if (!whitelist) return true;
if (!whitelist.domains) return true;
if (whitelist.domains.length == 0) return true;
const allowedDomains = whitelist.domains;
for (const allowedDomain of allowedDomains) {
const regexpDomain = new RegExp(allowedDomain.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'));
const result = website.match(regexpDomain);
if (result != null) return true;
}
return false;
}

View File

@@ -2,6 +2,7 @@ import { Router, json } from "express";
import { createSessionHash, getIPFromRequest } from "./utils"; import { createSessionHash, getIPFromRequest } from "./utils";
import { requireEnv } from "./shared/utils/requireEnv"; import { requireEnv } from "./shared/utils/requireEnv";
import { RedisStreamService } from "./shared/services/RedisStreamService"; import { RedisStreamService } from "./shared/services/RedisStreamService";
import { isAllowedToLog } from "./controller";
const router = Router(); const router = Router();
@@ -14,6 +15,10 @@ router.post('/keep_alive', json(jsonOptions), async (req, res) => {
try { try {
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent); const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent);
const allowed = await isAllowedToLog(req.body.pid, req.body.website, ip, req.body.userAgent);
if (!allowed) return res.sendStatus(400);
await RedisStreamService.addToStream(streamName, { await RedisStreamService.addToStream(streamName, {
...req.body, _type: 'keep_alive', sessionHash, ip, ...req.body, _type: 'keep_alive', sessionHash, ip,
instant: req.body.instant + '', instant: req.body.instant + '',
@@ -32,6 +37,9 @@ router.post('/metrics/push', json(jsonOptions), async (req, res) => {
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent); const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent);
const allowed = await isAllowedToLog(req.body.pid, req.body.website, ip, req.body.userAgent);
if (!allowed) return res.sendStatus(400);
const { type } = req.body; const { type } = req.body;
if (type === 0) { if (type === 0) {

View File

@@ -14,17 +14,31 @@ const jsonOptions = { limit: '25kb', type: allowAnyType }
const streamName = requireEnv('STREAM_NAME'); const streamName = requireEnv('STREAM_NAME');
import DeprecatedRouter from "./deprecated"; import DeprecatedRouter from "./deprecated";
import { isAllowedToLog } from "./controller";
import { connectDatabase } from "./shared/services/DatabaseService";
app.use('/v1', DeprecatedRouter); app.use('/v1', DeprecatedRouter);
app.post('/event', express.json(jsonOptions), async (req, res) => { app.post('/event', express.json(jsonOptions), async (req, res) => {
try { try {
const startTime = Date.now();
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent); const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent);
const flowHash = createFlowSessionHash(req.body.pid, ip, req.body.userAgent); const flowHash = createFlowSessionHash(req.body.pid, ip, req.body.userAgent);
const allowed = await isAllowedToLog(req.body.pid, req.body.website, ip, req.body.userAgent);
if (!allowed) return res.sendStatus(400);
await RedisStreamService.addToStream(streamName, { await RedisStreamService.addToStream(streamName, {
...req.body, _type: 'event', sessionHash, ip, flowHash, ...req.body, _type: 'event', sessionHash, ip, flowHash,
timestamp: Date.now() timestamp: Date.now()
}); });
const duration = Date.now() - startTime;
await RedisStreamService.METRICS_PRODUCER_onProcess(process.env.NODE_APP_INSTANCE, duration);
return res.sendStatus(200); return res.sendStatus(200);
} catch (ex: any) { } catch (ex: any) {
return res.status(500).json({ error: ex.message }); return res.status(500).json({ error: ex.message });
@@ -33,10 +47,22 @@ app.post('/event', express.json(jsonOptions), async (req, res) => {
app.post('/visit', express.json(jsonOptions), async (req, res) => { app.post('/visit', express.json(jsonOptions), async (req, res) => {
try { try {
const startTime = Date.now();
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent); const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent);
const flowHash = createFlowSessionHash(req.body.pid, ip, req.body.userAgent); const flowHash = createFlowSessionHash(req.body.pid, ip, req.body.userAgent);
const allowed = await isAllowedToLog(req.body.pid, req.body.website, ip, req.body.userAgent);
if (!allowed) return res.sendStatus(400);
await RedisStreamService.addToStream(streamName, { ...req.body, _type: 'visit', sessionHash, ip, flowHash, timestamp: Date.now() }); await RedisStreamService.addToStream(streamName, { ...req.body, _type: 'visit', sessionHash, ip, flowHash, timestamp: Date.now() });
const duration = Date.now() - startTime;
await RedisStreamService.METRICS_PRODUCER_onProcess(process.env.NODE_APP_INSTANCE, duration);
return res.sendStatus(200); return res.sendStatus(200);
} catch (ex: any) { } catch (ex: any) {
return res.status(500).json({ error: ex.message }); return res.status(500).json({ error: ex.message });
@@ -45,14 +71,26 @@ app.post('/visit', express.json(jsonOptions), async (req, res) => {
app.post('/keep_alive', express.json(jsonOptions), async (req, res) => { app.post('/keep_alive', express.json(jsonOptions), async (req, res) => {
try { try {
const startTime = Date.now();
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent); const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent);
const flowHash = createFlowSessionHash(req.body.pid, ip, req.body.userAgent); const flowHash = createFlowSessionHash(req.body.pid, ip, req.body.userAgent);
const allowed = await isAllowedToLog(req.body.pid, req.body.website, ip, req.body.userAgent);
if (!allowed) return res.sendStatus(400);
await RedisStreamService.addToStream(streamName, { await RedisStreamService.addToStream(streamName, {
...req.body, _type: 'keep_alive', sessionHash, ip, ...req.body, _type: 'keep_alive', sessionHash, ip,
instant: req.body.instant + '', instant: req.body.instant + '',
flowHash, timestamp: Date.now() flowHash, timestamp: Date.now()
}); });
const duration = Date.now() - startTime;
await RedisStreamService.METRICS_PRODUCER_onProcess(process.env.NODE_APP_INSTANCE, duration);
return res.sendStatus(200); return res.sendStatus(200);
} catch (ex: any) { } catch (ex: any) {
return res.status(500).json({ error: ex.message }); return res.status(500).json({ error: ex.message });
@@ -61,6 +99,7 @@ app.post('/keep_alive', express.json(jsonOptions), async (req, res) => {
async function main() { async function main() {
const PORT = requireEnv("PORT"); const PORT = requireEnv("PORT");
await connectDatabase(process.env.MONGO_CONNECTION_STRING);
await RedisStreamService.connect(); await RedisStreamService.connect();
app.listen(PORT, () => console.log(`Listening on port ${PORT}`)); app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
} }

View File

@@ -0,0 +1,87 @@
// 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';
// 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 MODE = DeployHelper.getMode();
// const SKIP_BUILD = DeployHelper.getArgAt(0) == '--no-build';
// console.log('Deploying consumer in mode:', MODE);
// setTimeout(() => { main(); }, 3000);
// async function main() {
// 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`);
// }
// console.log('Creting 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' })
// }
// 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();
// 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 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('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('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`);
// ssh.dispose();
// }

View File

@@ -0,0 +1,23 @@
import { SharedHelper } from "../helpers/shared-helper";
import path from "node:path";
const helper = new SharedHelper(path.join(__dirname, '../../payments/src/shared'))
helper.clear();
helper.create('utils');
helper.copy('utils/requireEnv.ts');
helper.create('services');
helper.copy('services/DatabaseService.ts');
helper.copy('services/EmailService.ts');
helper.create('schema');
helper.copy('schema/UserSchema.ts');
helper.create('schema/project');
helper.copy('schema/project/ProjectSchema.ts');
helper.create('data');
helper.copy('data/PLANS.ts');

View File

@@ -4,7 +4,7 @@ import path from 'path';
import child from 'child_process'; import child from 'child_process';
import { createZip } from '../helpers/zip-helper'; import { createZip } from '../helpers/zip-helper';
import { DeployHelper } from '../helpers/deploy-helper'; import { DeployHelper } from '../helpers/deploy-helper';
import { REMOTE_HOST_TESTMODE } from '../.config'; import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config';
const TMP_PATH = path.join(__dirname, '../../tmp'); const TMP_PATH = path.join(__dirname, '../../tmp');
const LOCAL_PATH = path.join(__dirname, '../../producer'); const LOCAL_PATH = path.join(__dirname, '../../producer');
@@ -37,7 +37,9 @@ async function main() {
if (MODE === 'testmode') { if (MODE === 'testmode') {
const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8');
const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1]; const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1];
const devContent = ecosystemContent.replace(REDIS_URL, `redis://${REMOTE_HOST_TESTMODE}`); 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' }); archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' });
} else { } else {
archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' })

View File

@@ -11,3 +11,12 @@ helper.copy('utils/requireEnv.ts');
helper.create('services'); helper.create('services');
helper.copy('services/RedisStreamService.ts'); helper.copy('services/RedisStreamService.ts');
helper.copy('services/DatabaseService.ts');
helper.create('schema');
helper.create('schema/shields');
helper.copy('schema/shields/AddressBlacklistSchema.ts');
helper.copy('schema/shields/BotTrafficOptionSchema.ts');
helper.copy('schema/shields/CountryBlacklistSchema.ts');
helper.copy('schema/shields/DomainWhitelistSchema.ts');
helper.copy('schema/shields/PageBlacklistSchema.ts');

201
shared_global/data/PLANS.ts Normal file
View File

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

View File

@@ -0,0 +1,18 @@
import { model, Schema, Types } from 'mongoose';
export type TAddressBlacklistSchema = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
address: string,
description: string,
created_at: Date
}
const AddressBlacklistSchema = new Schema<TAddressBlacklistSchema>({
project_id: { type: Types.ObjectId, index: 1 },
address: { type: String, required: true },
description: { type: String },
created_at: { type: Date, default: () => Date.now() },
});
export const AddressBlacklistModel = model<TAddressBlacklistSchema>('address_blacklists', AddressBlacklistSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TBotTrafficOptionSchema = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
block: boolean,
created_at: Date
}
const BotTrafficOptionSchema = new Schema<TBotTrafficOptionSchema>({
project_id: { type: Types.ObjectId, index: 1 },
block: { type: Boolean, required: true },
created_at: { type: Date, default: () => Date.now() },
});
export const BotTrafficOptionModel = model<TBotTrafficOptionSchema>('bot_traffic_options', BotTrafficOptionSchema);

View File

@@ -0,0 +1,18 @@
import { model, Schema, Types } from 'mongoose';
export type TCountryBlacklistSchema = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
country: string,
description: string,
created_at: Date
}
const CountryBlacklistSchema = new Schema<TCountryBlacklistSchema>({
project_id: { type: Types.ObjectId, index: 1 },
country: { type: String, required: true },
description: { type: String },
created_at: { type: Date, default: () => Date.now() },
});
export const CountryBlacklistModel = model<TCountryBlacklistSchema>('country_blacklists', CountryBlacklistSchema);

View File

@@ -0,0 +1,16 @@
import { model, Schema, Types } from 'mongoose';
export type TDomainWhitelistSchema = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
domains: string[],
created_at: Date
}
const DomainWhitelistSchema = new Schema<TDomainWhitelistSchema>({
project_id: { type: Types.ObjectId, index: 1 },
domains: [{ type: String, required: true }],
created_at: { type: Date, default: () => Date.now() },
});
export const DomainWhitelistModel = model<TDomainWhitelistSchema>('domain_whitelists', DomainWhitelistSchema);

View File

@@ -9,7 +9,8 @@ const templateMap = {
limit_90: '/limit/90', limit_90: '/limit/90',
limit_max: '/limit/max', limit_max: '/limit/max',
invite_project: '/invite', invite_project: '/invite',
invite_project_noaccount: '/invite/noaccount' invite_project_noaccount: '/invite/noaccount',
brevolist_add: '/brevolist/add'
} as const; } as const;
export type EmailTemplate = keyof typeof templateMap; export type EmailTemplate = keyof typeof templateMap;
@@ -27,6 +28,7 @@ type EmailData =
| { template: 'limit_max', data: { target: string, projectName: string } } | { template: 'limit_max', data: { target: string, projectName: string } }
| { template: 'invite_project', data: { target: string, projectName: string, link: string } } | { template: 'invite_project', data: { target: string, projectName: string, link: string } }
| { template: 'invite_project_noaccount', data: { target: string, projectName: string, link: string } } | { template: 'invite_project_noaccount', data: { target: string, projectName: string, link: string } }
| { template: 'brevolist_add', data: { email: string } }
export class EmailService { export class EmailService {
static getEmailServerInfo<T extends EmailTemplate>(template: T, data: Extract<EmailData, { template: T }>['data']): EmailServerInfo { static getEmailServerInfo<T extends EmailTemplate>(template: T, data: Extract<EmailData, { template: T }>['data']): EmailServerInfo {

View File

@@ -26,6 +26,7 @@ export class RedisStreamService {
private static METRICS_MAX_ENTRIES = 1000; private static METRICS_MAX_ENTRIES = 1000;
private static METRICS_MAX_ENTRIES_PRODUCER = 1000;
static async METRICS_onProcess(id: string, time: number) { static async METRICS_onProcess(id: string, time: number) {
const key = `___dev_metrics`; const key = `___dev_metrics`;
@@ -39,6 +40,18 @@ export class RedisStreamService {
return data.map(e => e.split(':')) as [string, string][]; return data.map(e => e.split(':')) as [string, string][];
} }
static async METRICS_PRODUCER_onProcess(id: string, time: number) {
const key = `___dev_metrics_producer`;
await this.client.lPush(key, `${id}:${time.toString()}`);
await this.client.lTrim(key, 0, this.METRICS_MAX_ENTRIES_PRODUCER - 1);
}
static async METRICS_PRODUCER_get() {
const key = `___dev_metrics_producer`;
const data = await this.client.lRange(key, 0, -1);
return data.map(e => e.split(':')) as [string, string][];
}
static async connect() { static async connect() {
await this.client.connect(); await this.client.connect();