mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
refactoring
This commit is contained in:
@@ -112,7 +112,7 @@ async function process_visit(data: Record<string, string>, sessionHash: string)
|
|||||||
|
|
||||||
async function process_keep_alive(data: Record<string, string>, sessionHash: string) {
|
async function process_keep_alive(data: Record<string, string>, sessionHash: string) {
|
||||||
|
|
||||||
const { pid, instant, flowHash, timestamp } = data;
|
const { pid, instant, flowHash, timestamp, website } = data;
|
||||||
|
|
||||||
const existingSession = await SessionModel.findOne({ project_id: pid, session: sessionHash }, { _id: 1 });
|
const existingSession = await SessionModel.findOne({ project_id: pid, session: sessionHash }, { _id: 1 });
|
||||||
if (!existingSession) {
|
if (!existingSession) {
|
||||||
@@ -123,13 +123,15 @@ async function process_keep_alive(data: Record<string, string>, sessionHash: str
|
|||||||
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
||||||
$inc: { duration: 0 },
|
$inc: { duration: 0 },
|
||||||
flowHash,
|
flowHash,
|
||||||
updated_at: Date.now()
|
website,
|
||||||
|
updated_at: new Date(parseInt(timestamp))
|
||||||
}, { upsert: true });
|
}, { upsert: true });
|
||||||
} else {
|
} else {
|
||||||
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
||||||
$inc: { duration: 1 },
|
$inc: { duration: 1 },
|
||||||
flowHash,
|
flowHash,
|
||||||
updated_at: Date.now()
|
website,
|
||||||
|
updated_at: new Date(parseInt(timestamp))
|
||||||
}, { upsert: true });
|
}, { upsert: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@ async function process_keep_alive(data: Record<string, string>, sessionHash: str
|
|||||||
|
|
||||||
async function process_event(data: Record<string, string>, sessionHash: string) {
|
async function process_event(data: Record<string, string>, sessionHash: string) {
|
||||||
|
|
||||||
const { name, metadata, pid, flowHash, timestamp } = data;
|
const { name, metadata, pid, flowHash, timestamp, website } = data;
|
||||||
|
|
||||||
let metadataObject;
|
let metadataObject;
|
||||||
try {
|
try {
|
||||||
@@ -149,6 +151,7 @@ async function process_event(data: Record<string, string>, sessionHash: string)
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
EventModel.create({
|
EventModel.create({
|
||||||
project_id: pid, name, flowHash, metadata: metadataObject, session: sessionHash,
|
project_id: pid, name, flowHash, metadata: metadataObject, session: sessionHash,
|
||||||
|
website,
|
||||||
created_at: new Date(parseInt(timestamp))
|
created_at: new Date(parseInt(timestamp))
|
||||||
}),
|
}),
|
||||||
ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'events': 1 } }, { upsert: true }),
|
ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'events': 1 } }, { upsert: true }),
|
||||||
|
|||||||
@@ -79,13 +79,13 @@ function reloadPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center mt-10">
|
<div class="flex items-center justify-center mt-10 w-full px-10">
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
|
|
||||||
<div class="flex gap-6 xl:flex-row flex-col">
|
<div class="flex gap-6 xl:flex-row flex-col">
|
||||||
|
|
||||||
<div class="h-full w-full">
|
<div class="h-full w-full">
|
||||||
<CardTitled class="h-full w-full xl:min-w-[500px] xl:h-[35rem]" title="Quick setup tutorial"
|
<CardTitled class="h-full w-full xl:min-w-[400px] xl:h-[35rem]" title="Quick setup tutorial"
|
||||||
sub="Quickly Set Up Litlyx in 30 Seconds!">
|
sub="Quickly Set Up Litlyx in 30 Seconds!">
|
||||||
|
|
||||||
<div class="flex items-center justify-center h-full w-full">
|
<div class="flex items-center justify-center h-full w-full">
|
||||||
@@ -133,6 +133,28 @@ function reloadPage() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<CardTitled class="w-full h-full" title="Wordpress + Elementor"
|
||||||
|
sub="Our WordPress plugin is coming soon!.">
|
||||||
|
<template #header>
|
||||||
|
<LyxUiButton @click="Lit.event('no_visit_goto_docs')" type="secondary"
|
||||||
|
to="https://docs.litlyx.com">
|
||||||
|
Visit documentation
|
||||||
|
</LyxUiButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-end">
|
||||||
|
<div class="justify-center w-full hidden xl:flex gap-3">
|
||||||
|
<a href="#">
|
||||||
|
<img class="cursor-pointer" :src="'tech-icons/wpel.png'" alt="Litlyx-Wordpress-Elementor">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</CardTitled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitled class="w-full h-full" title="Modules"
|
<CardTitled class="w-full h-full" title="Modules"
|
||||||
|
|||||||
@@ -88,12 +88,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
const filter = ref<string>('{}');
|
const filter = ref<string>('{}');
|
||||||
|
|
||||||
const { data: projectsInfo, pending: pendingProjects } = await useFetch<{ count: number, projects: TAdminProject[] }>(
|
const { data: projectsInfo, pending: pendingProjects } = useFetch<{ count: number, projects: TAdminProject[] }>(
|
||||||
() => `/api/admin/projects?page=${page.value - 1}&limit=${limit.value}&sortQuery=${order.value}&filterQuery=${filter.value}&filterFrom=${selected.value.start.toISOString()}&filterTo=${selected.value.end.toISOString()}`,
|
() => `/api/admin/projects?page=${page.value - 1}&limit=${limit.value}&sortQuery=${order.value}&filterQuery=${filter.value}&filterFrom=${selected.value.start.toISOString()}&filterTo=${selected.value.end.toISOString()}`,
|
||||||
signHeaders()
|
signHeaders()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: metrics, pending: pendingMetrics } = useFetch(
|
||||||
|
() => `/api/admin/metrics?filterFrom=${selected.value.start.toISOString()}&filterTo=${selected.value.end.toISOString()}`,
|
||||||
|
signHeaders()
|
||||||
|
);
|
||||||
|
|
||||||
const { uiMenu } = useSelectMenuStyle();
|
const { uiMenu } = useSelectMenuStyle();
|
||||||
|
|
||||||
@@ -165,6 +168,23 @@ const { uiMenu } = useSelectMenuStyle();
|
|||||||
</UPopover>
|
</UPopover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="w-[80%]">
|
||||||
|
<div v-if="pendingMetrics"> Loading... </div>
|
||||||
|
<div class="flex gap-10 flex-wrap" v-if="!pendingMetrics && metrics">
|
||||||
|
<div> Projects: {{ metrics.totalProjects }} ({{ metrics.premiumProjects }} premium) </div>
|
||||||
|
<div>
|
||||||
|
Total visits: {{ formatNumberK(metrics.totalVisits) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Active: {{ metrics.totalProjects - metrics.deadProjects }} |
|
||||||
|
Dead: {{ metrics.deadProjects }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Total events: {{ formatNumberK(metrics.totalEvents) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -198,11 +198,16 @@ const selectLabelsAvailable = computed<{ label: string, value: Slice, disabled:
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectedSlice = computed<Slice>(() => {
|
const selectedSlice = computed<Slice>(() => {
|
||||||
|
console.log({ available: selectLabelsAvailable.value })
|
||||||
const targetValue = selectLabelsAvailable.value[selectedLabelIndex.value];
|
const targetValue = selectLabelsAvailable.value[selectedLabelIndex.value];
|
||||||
if (!targetValue) return 'day';
|
if (!targetValue) return 'day';
|
||||||
if (targetValue.disabled) {
|
if (targetValue.disabled) {
|
||||||
selectedLabelIndex.value = selectLabelsAvailable.value.findIndex(e => !e.disabled);
|
selectedLabelIndex.value = selectLabelsAvailable.value.findIndex(e => !e.disabled);
|
||||||
}
|
}
|
||||||
|
if (selectedLabelIndex.value === -1) {
|
||||||
|
console.error('ERROR CANNOT FIND CORRECT SLICE')
|
||||||
|
return 'day';
|
||||||
|
}
|
||||||
return selectLabelsAvailable.value[selectedLabelIndex.value].value
|
return selectLabelsAvailable.value[selectedLabelIndex.value].value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()
|
|||||||
|
|
||||||
const chartSlice = computed(() => {
|
const chartSlice = computed(() => {
|
||||||
if (snapshotDuration.value <= 3) return 'hour' as Slice;
|
if (snapshotDuration.value <= 3) return 'hour' as Slice;
|
||||||
if (snapshotDuration.value <= 32) return 'day' as Slice;
|
if (snapshotDuration.value <= 31 * 3) return 'day' as Slice;
|
||||||
return 'month' as Slice;
|
return 'month' as Slice;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
56
dashboard/components/dialog/InviteManager.vue
Normal file
56
dashboard/components/dialog/InviteManager.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import Accept_invite from '~/pages/accept_invite.vue';
|
||||||
|
|
||||||
|
|
||||||
|
const { createAlert } = useAlert();
|
||||||
|
const { close } = useModal()
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'cancel'])
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
invites: {
|
||||||
|
project_name: string, project_id: string
|
||||||
|
}[]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function acceptInvite(project_id: string) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function declineInvite(project_id: string) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</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-8 p-4">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2" v-for="invite of [...invites, ...invites, ...invites]">
|
||||||
|
|
||||||
|
<div class="dark:text-lyx-text text-lyx-lightmode-text">
|
||||||
|
You are invited to join
|
||||||
|
<span class="font-semibold">{{ invite.project_name }}</span>.
|
||||||
|
Do you accept this invitation?
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<LyxUiButton @click="acceptInvite(invite.project_id)" type="primary"> Accept </LyxUiButton>
|
||||||
|
<LyxUiButton @click="declineInvite(invite.project_id)" type="danger"> Decline </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
110
dashboard/components/dialog/PermissionManager.vue
Normal file
110
dashboard/components/dialog/PermissionManager.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useSelectMenuStyle } from '~/composables/ui/useSelectMenuStyle';
|
||||||
|
import type { TTeamMember } from '~/shared/schema/TeamMemberSchema';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'cancel'])
|
||||||
|
|
||||||
|
const props = defineProps<{ member_id: string }>();
|
||||||
|
|
||||||
|
const { domainList, domain, setActiveDomain, refreshDomains, refreshingDomains } = useDomain();
|
||||||
|
|
||||||
|
const { data: member } = useFetch<TTeamMember>(`/api/project/members/get?member_id=${props.member_id}`, {
|
||||||
|
headers: useComputedHeaders({})
|
||||||
|
})
|
||||||
|
|
||||||
|
const { createAlert } = useAlert()
|
||||||
|
|
||||||
|
async function save(member_id: string) {
|
||||||
|
if (!member.value) return;
|
||||||
|
const res = await $fetch('/api/project/members/edit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: useComputedHeaders({ custom: { 'Content-Type': 'application/json' } }).value,
|
||||||
|
body: JSON.stringify({
|
||||||
|
member_id,
|
||||||
|
webAnalytics: member.value.permission.webAnalytics,
|
||||||
|
events: member.value.permission.events,
|
||||||
|
ai: member.value.permission.ai,
|
||||||
|
domains: member.value.permission.domains
|
||||||
|
})
|
||||||
|
});
|
||||||
|
createAlert('Saved', 'Permission saved successfully', 'fas fa-check', 2500);
|
||||||
|
emit('success')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal :ui="{
|
||||||
|
strategy: 'override',
|
||||||
|
overlay: {
|
||||||
|
background: 'bg-lyx-background/85'
|
||||||
|
},
|
||||||
|
background: 'bg-lyx-lightmode-widget dark:bg-lyx-widget',
|
||||||
|
ring: 'border-solid border-[1px] border-[#262626]'
|
||||||
|
}">
|
||||||
|
<div class="p-8">
|
||||||
|
<div v-if="member" class="manage flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1"> Allowed domains </div>
|
||||||
|
<div class="mb-1">
|
||||||
|
<USelectMenu v-model="member.permission.domains" :options="domainList" multiple
|
||||||
|
value-attribute="_id">
|
||||||
|
<template #option="{ option, active, selected }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'"
|
||||||
|
alt="Litlyx logo">
|
||||||
|
</div>
|
||||||
|
<div> {{ option._id }} </div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #label="e">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'"
|
||||||
|
alt="Litlyx logo">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
member.permission.domains.length > 2 ?
|
||||||
|
`${member.permission.domains.length} domains` :
|
||||||
|
(member.permission.domains.map(e => e).join(' & ') || 'No domains')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</USelectMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UCheckbox v-model="member.permission.webAnalytics"></UCheckbox>
|
||||||
|
<div> Allow web analytics page </div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UCheckbox v-model="member.permission.events"></UCheckbox>
|
||||||
|
<div> Allow events page </div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UCheckbox v-model="member.permission.ai"></UCheckbox>
|
||||||
|
<div> Allow AI page </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex gap-2 justify-end mt-8">
|
||||||
|
<LyxUiButton v-if="member?.permission" @click="save(member._id.toString())" type="primary">
|
||||||
|
Save
|
||||||
|
</LyxUiButton>
|
||||||
|
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -5,6 +5,7 @@ import { DialogFeedback, DialogHelp } from '#components';
|
|||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const selfhosted = useSelfhosted();
|
const selfhosted = useSelfhosted();
|
||||||
|
|
||||||
|
const { domain } = useDomain();
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const isDark = computed({
|
const isDark = computed({
|
||||||
@@ -16,23 +17,24 @@ const isDark = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const {safeSnapshotDates} = useSnapshot();
|
const { safeSnapshotDates } = useSnapshot();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full overflow-y-auto hide-scrollbars h-[4rem] border-solid border-[#D9D9E0] dark:border-[#202020] border-b-[1px] bg-lyx-lightmode-background dark:bg-lyx-background flex dark:shadow-[1px_0_10px_#000000]">
|
class="w-full hide-scrollbars relative h-[4rem] border-solid border-[#D9D9E0] dark:border-[#202020] border-b-[1px] bg-lyx-lightmode-background dark:bg-lyx-background dark:shadow-[1px_0_10px_#000000]">
|
||||||
|
|
||||||
|
<div class="absolute flex h-full w-full">
|
||||||
<div class="flex items-center px-6">
|
<div class="flex items-center px-6">
|
||||||
<SelectorDomainSelector></SelectorDomainSelector>
|
<SelectorDomainSelector></SelectorDomainSelector>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden lg:flex pl-[12rem] items-center popping text-[.9rem] dark:text-lyx-text-dark">
|
<div class="hidden lg:flex items-center popping text-[.9rem] dark:text-lyx-text-dark">
|
||||||
Timeframe:
|
Timeframe:
|
||||||
{{new Date(safeSnapshotDates.from).toLocaleDateString()}}
|
{{ new Date(safeSnapshotDates.from).toLocaleDateString() }}
|
||||||
to
|
to
|
||||||
{{new Date(safeSnapshotDates.to).toLocaleDateString()}}
|
{{ new Date(safeSnapshotDates.to).toLocaleDateString() }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -62,4 +64,6 @@ const {safeSnapshotDates} = useSnapshot();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { DialogInviteManager } from '#components';
|
||||||
import CreateSnapshot from '../dialog/CreateSnapshot.vue';
|
import CreateSnapshot from '../dialog/CreateSnapshot.vue';
|
||||||
|
|
||||||
export type Entry = {
|
export type Entry = {
|
||||||
@@ -27,6 +28,10 @@ type Props = {
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const { data: pendingInvites, refresh: refreshInvites } = useFetch('/api/project/members/pending', {
|
||||||
|
headers: useComputedHeaders({})
|
||||||
|
});
|
||||||
|
|
||||||
const { userRoles, setLoggedUser } = useLoggedUser();
|
const { userRoles, setLoggedUser } = useLoggedUser();
|
||||||
const { projectList } = useProject();
|
const { projectList } = useProject();
|
||||||
|
|
||||||
@@ -106,6 +111,28 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
function openPendingInvites() {
|
||||||
|
if (!pendingInvites.value) return;
|
||||||
|
if (pendingInvites.value.length == 0) return;
|
||||||
|
|
||||||
|
console.log(pendingInvites);
|
||||||
|
modal.open(DialogInviteManager, {
|
||||||
|
invites: pendingInvites.value.map(e => {
|
||||||
|
return { project_id: e._id, project_name: e.project_name }
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
modal.close();
|
||||||
|
refreshInvites();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
modal.close();
|
||||||
|
refreshInvites();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -249,7 +276,6 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
<div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full"></div>
|
<div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full"></div>
|
||||||
|
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
|
|
||||||
<div v-for="section of sections" class="flex flex-col gap-1 h-full pb-6">
|
<div v-for="section of sections" class="flex flex-col gap-1 h-full pb-6">
|
||||||
|
|
||||||
<div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }">
|
<div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }">
|
||||||
@@ -283,6 +309,18 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
|
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
|
|
||||||
|
<div v-if="pendingInvites && pendingInvites.length > 0" @click="openPendingInvites()"
|
||||||
|
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex flex-col justify-center cursor-pointer">
|
||||||
|
<div class="poppins font-medium dark:text-lyx-text text-lyx-lightmode-text">
|
||||||
|
Pending invitation
|
||||||
|
</div>
|
||||||
|
<div class="poppins dark:text-lyx-text-dark text-lyx-lightmode-text-dark">
|
||||||
|
You have {{ pendingInvites.length }}
|
||||||
|
pending invitation{{ pendingInvites.length != 1 ? 's' : '' }}
|
||||||
|
awaiting your response
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full px-4 mb-3"></div>
|
<div class="bg-lyx-lightmode-widget dark:bg-[#202020] h-[1px] w-full px-4 mb-3"></div>
|
||||||
|
|
||||||
<div class="flex justify-end px-2">
|
<div class="flex justify-end px-2">
|
||||||
@@ -296,7 +334,8 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UTooltip text="Logout" :popper="{ arrow: true, placement: 'top' }">
|
<UTooltip text="Logout" :popper="{ arrow: true, placement: 'top' }">
|
||||||
<div @click="onLogout()" class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark">
|
<div @click="onLogout()"
|
||||||
|
class="cursor-pointer hover:text-lyx-lightmode-text text-lyx-lightmode-text-dark dark:hover:text-lyx-text dark:text-lyx-text-dark">
|
||||||
<i class="far fa-arrow-right-from-bracket scale-x-[-100%]"></i>
|
<i class="far fa-arrow-right-from-bracket scale-x-[-100%]"></i>
|
||||||
</div>
|
</div>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function onChange(e: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex gap-2 absolute">
|
<div class="flex gap-2">
|
||||||
<USelectMenu :uiMenu="{
|
<USelectMenu :uiMenu="{
|
||||||
select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
|
select: 'bg-lyx-lightmode-widget-light !ring-lyx-lightmode-widget dark:!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter dark:!ring-lyx-widget-lighter',
|
||||||
base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget w-max',
|
base: '!bg-lyx-lightmode-widget dark:!bg-lyx-widget w-max',
|
||||||
@@ -18,7 +18,7 @@ function onChange(e: string) {
|
|||||||
},
|
},
|
||||||
input: 'z-[999] !bg-lyx-lightmode-widget dark:!bg-lyx-widget-light'
|
input: 'z-[999] !bg-lyx-lightmode-widget dark:!bg-lyx-widget-light'
|
||||||
}" class="w-full" searchable searchable-placeholder="Search domain..." v-if="domainList" @change="onChange"
|
}" class="w-full" searchable searchable-placeholder="Search domain..." v-if="domainList" @change="onChange"
|
||||||
:value="domain" value-attribute="_id" :options="domainList">
|
:value="domain" :loading="refreshingDomains" value-attribute="_id" :options="domainList">
|
||||||
|
|
||||||
<template #option="{ option, active, selected }">
|
<template #option="{ option, active, selected }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -35,7 +35,7 @@ function onChange(e: string) {
|
|||||||
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
<img class="h-5 bg-black rounded-full" :src="'/logo_32.png'" alt="Litlyx logo">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ domain || '-' }}
|
{{ refreshingDomains ? 'Loading...' : (domain || '-') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ function isProjectMine(owner?: string) {
|
|||||||
|
|
||||||
function onChange(e: TProject) {
|
function onChange(e: TProject) {
|
||||||
actions.setActiveProject(e._id.toString());
|
actions.setActiveProject(e._id.toString());
|
||||||
setActiveDomain('All domains');
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { SettingsTemplateEntry } from './Template.vue';
|
|
||||||
|
|
||||||
const { projectId, isGuest } = useProject();
|
|
||||||
|
|
||||||
definePageMeta({ layout: 'dashboard' });
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ key: 'me', label: '' },
|
|
||||||
{ key: 'email', label: 'Email' },
|
|
||||||
{ key: 'name', label: 'Name' },
|
|
||||||
{ key: 'role', label: 'Role' },
|
|
||||||
{ key: 'action', label: 'Actions' },
|
|
||||||
// { key: 'pending', label: 'Pending' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const { data: members, refresh: refreshMembers } = useFetch('/api/project/members/list', {
|
|
||||||
headers: useComputedHeaders({ useSnapshotDates: false })
|
|
||||||
});
|
|
||||||
|
|
||||||
const showAddMember = ref<boolean>(false);
|
|
||||||
|
|
||||||
const addMemberEmail = ref<string>("");
|
|
||||||
|
|
||||||
async function kickMember(email: string) {
|
|
||||||
|
|
||||||
const sure = confirm('Are you sure to kick ' + email + ' ?');
|
|
||||||
if (!sure) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
await $fetch('/api/project/members/kick', {
|
|
||||||
method: 'POST',
|
|
||||||
...signHeaders({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-pid': projectId.value ?? ''
|
|
||||||
}),
|
|
||||||
body: JSON.stringify({ email }),
|
|
||||||
onResponseError({ request, response, options }) {
|
|
||||||
alert(response.statusText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshMembers();
|
|
||||||
|
|
||||||
} catch (ex: any) { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addMember() {
|
|
||||||
|
|
||||||
if (addMemberEmail.value.length === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
showAddMember.value = false;
|
|
||||||
|
|
||||||
await $fetch('/api/project/members/add', {
|
|
||||||
method: 'POST',
|
|
||||||
...signHeaders({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-pid': projectId.value ?? ''
|
|
||||||
}),
|
|
||||||
body: JSON.stringify({ email: addMemberEmail.value }),
|
|
||||||
onResponseError({ request, response, options }) {
|
|
||||||
alert(response.statusText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addMemberEmail.value = '';
|
|
||||||
|
|
||||||
refreshMembers();
|
|
||||||
|
|
||||||
} catch (ex: any) { }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries: SettingsTemplateEntry[] = [
|
|
||||||
{ id: 'add', title: 'Add member', text: 'Add new member to project' },
|
|
||||||
{ id: 'members', title: 'Members', text: 'Manage members of current project' },
|
|
||||||
]
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
<SettingsTemplate :entries="entries">
|
|
||||||
<template #add>
|
|
||||||
<div v-if="!isGuest" class="flex flex-col">
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<LyxUiInput class="px-4 py-1 w-full" placeholder="User email" v-model="addMemberEmail"></LyxUiInput>
|
|
||||||
<LyxUiButton @click="addMember" type="secondary"> Add </LyxUiButton>
|
|
||||||
</div>
|
|
||||||
<div class="poppins text-[.8rem] mt-2 text-lyx-text-darker">
|
|
||||||
User should have been registered to Litlyx
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isGuest" class="text-lyx-text-darker"> Guests cannot add members</div>
|
|
||||||
</template>
|
|
||||||
<template #members>
|
|
||||||
|
|
||||||
|
|
||||||
<UTable :rows="members || []" :columns="columns">
|
|
||||||
<template #me-data="e">
|
|
||||||
<i v-if="e.row.me" class="far fa-user"></i>
|
|
||||||
<i v-if="!e.row.me"></i>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action-data="e" v-if="!isGuest">
|
|
||||||
<div @click="kickMember(e.row.email)" v-if="e.row.role != 'OWNER'"
|
|
||||||
class="text-red-500 hover:bg-black/20 cursor-pointer outline outline-[1px] outline-red-500 px-3 py-1 rounded-lg text-center">
|
|
||||||
Kick
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</UTable>
|
|
||||||
</template>
|
|
||||||
</SettingsTemplate>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
@@ -16,26 +16,22 @@ function refreshDomains() {
|
|||||||
domainsRequest.refresh();
|
domainsRequest.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(domainsRequest.data, () => {
|
||||||
|
if (!domainsRequest.data.value) return;
|
||||||
|
setActiveDomain(domainList.value[0]._id);
|
||||||
|
});
|
||||||
|
|
||||||
const refreshingDomains = computed(() => domainsRequest.pending.value);
|
const refreshingDomains = computed(() => domainsRequest.pending.value);
|
||||||
|
|
||||||
const domainList = computed(() => {
|
const domainList = computed(() => {
|
||||||
return [
|
return (domainsRequest.data.value?.sort((a, b) => b.visits - a.visits) || []);
|
||||||
{
|
|
||||||
_id: 'All domains', visits: domainsRequest.data.value?.reduce((a, e) => a + e.visits, 0)
|
|
||||||
},
|
|
||||||
...(domainsRequest.data.value?.sort((a, b) => b.visits - a.visits) || [])
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const activeDomain = ref<string>();
|
const activeDomain = ref<string>();
|
||||||
|
|
||||||
const domain = computed(() => {
|
const domain = computed(() => {
|
||||||
if (activeDomain.value) return activeDomain.value;
|
return activeDomain.value;
|
||||||
if (!domainList.value) return;
|
|
||||||
if (domainList.value.length == 0) return;
|
|
||||||
setActiveDomain(domainList.value[0]._id);
|
|
||||||
return domainList.value[0]._id;
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function setActiveDomain(domain: string) {
|
function setActiveDomain(domain: string) {
|
||||||
|
|||||||
14
dashboard/composables/usePermission.ts
Normal file
14
dashboard/composables/usePermission.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { data: permission } = useFetch('/api/project/members/me', {
|
||||||
|
headers: useComputedHeaders({})
|
||||||
|
});
|
||||||
|
|
||||||
|
const canSeeWeb = computed(() => permission.value?.webAnalytics || false);
|
||||||
|
const canSeeEvents = computed(() => permission.value?.events || false);
|
||||||
|
const canSeeAi = computed(() => permission.value?.ai || false);
|
||||||
|
|
||||||
|
export function usePermission() {
|
||||||
|
return { permission, canSeeWeb, canSeeEvents, canSeeAi };
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ const sections: Section[] = [
|
|||||||
entries: [
|
entries: [
|
||||||
{ 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: '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 },
|
||||||
|
|||||||
34
dashboard/pages/accept_invite.vue
Normal file
34
dashboard/pages/accept_invite.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const project_id = route.query.project_id;
|
||||||
|
if (!project_id) throw Error('project_id is required');
|
||||||
|
const res = await $fetch('/api/project/members/accept', {
|
||||||
|
headers: useComputedHeaders({
|
||||||
|
custom: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).value,
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ project_id })
|
||||||
|
});
|
||||||
|
router.push('/');
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('ERROR');
|
||||||
|
console.error(ex);
|
||||||
|
alert('An error occurred');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
You will be redirected soon.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,165 +5,8 @@ import type { CItem } from '~/components/CustomTab.vue';
|
|||||||
definePageMeta({ layout: 'dashboard' });
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
|
|
||||||
const filterPremium = ref<boolean>(false);
|
|
||||||
const filterAppsumo = ref<boolean>(false);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const timeRange = ref<number>(9);
|
|
||||||
|
|
||||||
function setTimeRange(n: number) {
|
|
||||||
timeRange.value = n;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeRangeTimestamp = computed(() => {
|
|
||||||
if (timeRange.value == 1) return Date.now() - 1000 * 60 * 60 * 24;
|
|
||||||
if (timeRange.value == 2) return Date.now() - 1000 * 60 * 60 * 24 * 7;
|
|
||||||
if (timeRange.value == 3) return Date.now() - 1000 * 60 * 60 * 24 * 30;
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// const { data: projectsAggregatedResponseData } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
|
|
||||||
// const { data: counts } = await useFetch(() => `/api/admin/counts?from=${timeRangeTimestamp.value}`, signHeaders());
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// function onHideClicked() {
|
|
||||||
// isAdminHidden.value = true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// function isAppsumoType(type: number) {
|
|
||||||
// return type > 6000 && type < 6004
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const projectsAggregated = computed(() => {
|
|
||||||
|
|
||||||
// let pool = projectsAggregatedResponseData.value ? [...projectsAggregatedResponseData.value] : [];
|
|
||||||
|
|
||||||
// let shownPool: AdminProjectsList[] = [];
|
|
||||||
|
|
||||||
|
|
||||||
// for (const element of pool) {
|
|
||||||
|
|
||||||
// shownPool.push({ ...element, projects: [...element.projects] });
|
|
||||||
|
|
||||||
// if (filterAppsumo.value === true) {
|
|
||||||
// shownPool.forEach(e => {
|
|
||||||
// e.projects = e.projects.filter(project => {
|
|
||||||
// return isAppsumoType(project.premium_type)
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// shownPool = shownPool.filter(e => {
|
|
||||||
// return e.projects.length > 0;
|
|
||||||
// })
|
|
||||||
|
|
||||||
// } else if (filterPremium.value === true) {
|
|
||||||
// shownPool.forEach(e => {
|
|
||||||
// e.projects = e.projects.filter(project => {
|
|
||||||
// return project.premium === true;
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// shownPool = shownPool.filter(e => {
|
|
||||||
// return e.projects.length > 0;
|
|
||||||
// })
|
|
||||||
|
|
||||||
// } else {
|
|
||||||
// console.log('NO DATA')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// return shownPool.sort((a, b) => {
|
|
||||||
// const sumVisitsA = a.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
|
||||||
// const sumVisitsB = b.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
|
||||||
// return sumVisitsB - sumVisitsA;
|
|
||||||
// }).filter(e => {
|
|
||||||
// return new Date(e.created_at).getTime() >= timeRangeTimestamp.value
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const premiumCount = computed(() => {
|
|
||||||
// let premiums = 0;
|
|
||||||
// projectsAggregated.value?.forEach(e => {
|
|
||||||
// e.projects.forEach(p => {
|
|
||||||
// if (p.premium) premiums++;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// })
|
|
||||||
// return premiums;
|
|
||||||
// })
|
|
||||||
|
|
||||||
|
|
||||||
// const activeProjects = computed(() => {
|
|
||||||
// let actives = 0;
|
|
||||||
|
|
||||||
// projectsAggregated.value?.forEach(e => {
|
|
||||||
// e.projects.forEach(p => {
|
|
||||||
// if (!p.counts) return;
|
|
||||||
// if (!p.counts.updated_at) return;
|
|
||||||
// const updated_at = new Date(p.counts.updated_at).getTime();
|
|
||||||
// if (updated_at < Date.now() - 1000 * 60 * 60 * 24) return;
|
|
||||||
// actives++;
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// return actives;
|
|
||||||
// });
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const totalVisits = computed(() => {
|
|
||||||
// return projectsAggregated.value?.reduce((a, e) => {
|
|
||||||
// return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0), 0);
|
|
||||||
// }, 0) || 0;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const totalEvents = computed(() => {
|
|
||||||
// return projectsAggregated.value?.reduce((a, e) => {
|
|
||||||
// return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.events || 0), 0);
|
|
||||||
// }, 0) || 0;
|
|
||||||
// });
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const details = ref<any>();
|
const details = ref<any>();
|
||||||
const showDetails = ref<boolean>(false);
|
const showDetails = ref<boolean>(false);
|
||||||
async function getProjectDetails(project_id: string) {
|
|
||||||
details.value = await $fetch(`/api/admin/details?project_id=${project_id}`, signHeaders());
|
|
||||||
showDetails.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetCount(project_id: string) {
|
|
||||||
await $fetch(`/api/admin/reset_count?project_id=${project_id}`, signHeaders());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function dateDiffDays(a: string) {
|
|
||||||
return (Date.now() - new Date(a).getTime()) / (1000 * 60 * 60 * 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLogBg(last_logged_at?: string) {
|
|
||||||
|
|
||||||
const day = 1000 * 60 * 60 * 24;
|
|
||||||
const week = 1000 * 60 * 60 * 24 * 7;
|
|
||||||
|
|
||||||
const lastLoggedAtDate = new Date(last_logged_at || 0);
|
|
||||||
|
|
||||||
if (lastLoggedAtDate.getTime() > Date.now() - day) {
|
|
||||||
return 'bg-green-500'
|
|
||||||
} else if (lastLoggedAtDate.getTime() > Date.now() - week) {
|
|
||||||
return 'bg-yellow-500'
|
|
||||||
} else {
|
|
||||||
return 'bg-red-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const tabs: CItem[] = [
|
const tabs: CItem[] = [
|
||||||
{ label: 'Overview', slot: 'overview' },
|
{ label: 'Overview', slot: 'overview' },
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ definePageMeta({ layout: 'dashboard' });
|
|||||||
|
|
||||||
const selfhosted = useSelfhosted();
|
const selfhosted = useSelfhosted();
|
||||||
|
|
||||||
|
const { permission, canSeeAi } = usePermission();
|
||||||
|
|
||||||
const debugModeAi = ref<boolean>(false);
|
const debugModeAi = ref<boolean>(false);
|
||||||
|
|
||||||
const { userRoles } = useLoggedUser();
|
const { userRoles } = useLoggedUser();
|
||||||
@@ -253,7 +255,12 @@ async function clearAllChats() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-y-hidden">
|
|
||||||
|
<div v-if="!canSeeAi" class="h-full w-full flex mt-[20vh] justify-center">
|
||||||
|
<div> You need AI permission to view this page </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canSeeAi" class="w-full h-full overflow-y-hidden">
|
||||||
|
|
||||||
<div class="flex flex-row h-full overflow-y-hidden">
|
<div class="flex flex-row h-full overflow-y-hidden">
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import DateService, { type Slice } from '@services/DateService';
|
|||||||
|
|
||||||
definePageMeta({ layout: 'dashboard' });
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
|
const { permission, canSeeEvents } = usePermission();
|
||||||
|
|
||||||
const { snapshotDuration } = useSnapshot();
|
const { snapshotDuration } = useSnapshot();
|
||||||
|
|
||||||
@@ -30,7 +31,12 @@ const eventsData = await useFetch(`/api/data/count`, {
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
|
||||||
|
<div v-if="!canSeeEvents" class="h-full w-full flex mt-[20vh] justify-center">
|
||||||
|
<div> You need events permission to view this page </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canSeeEvents" class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
||||||
|
|
||||||
|
|
||||||
<LyxUiCard class="w-full flex justify-between items-center lg:flex-row flex-col gap-6 lg:gap-0">
|
<LyxUiCard class="w-full flex justify-between items-center lg:flex-row flex-col gap-6 lg:gap-0">
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const jwtLogin = computed(() => route.query.jwt_login as string);
|
|||||||
|
|
||||||
const { token, setToken } = useAccessToken();
|
const { token, setToken } = useAccessToken();
|
||||||
|
|
||||||
|
const { refreshingDomains } = useDomain();
|
||||||
|
const { permission, canSeeWeb, canSeeEvents } = usePermission();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
if (jwtLogin.value) {
|
if (jwtLogin.value) {
|
||||||
@@ -36,13 +39,22 @@ const selfhosted = useSelfhosted();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="dashboard w-full h-full overflow-y-auto overflow-x-hidden pb-[7rem] md:pt-4 lg:pt-0">
|
<div v-if="!canSeeWeb" class="h-full w-full flex mt-[20vh] justify-center">
|
||||||
|
<div> You need webAnalytics permission to view this page </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canSeeWeb && refreshingDomains">
|
||||||
|
<div class="w-full flex justify-center items-center mt-[20vh]">
|
||||||
|
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canSeeWeb && !refreshingDomains" class="dashboard w-full h-full overflow-y-auto overflow-x-hidden pb-[7rem] md:pt-4 lg:pt-0">
|
||||||
|
|
||||||
<div v-if="showDashboard">
|
<div v-if="showDashboard">
|
||||||
|
|
||||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||||
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
||||||
<!-- <BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -52,7 +64,10 @@ const selfhosted = useSelfhosted();
|
|||||||
|
|
||||||
|
|
||||||
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
<DashboardActionableChart v-if="canSeeWeb && canSeeEvents" :key="refreshKey"></DashboardActionableChart>
|
||||||
|
<LyxUiCard v-else class="flex justify-center w-full py-4">
|
||||||
|
You need events permission to view this widget
|
||||||
|
</LyxUiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +107,6 @@ const selfhosted = useSelfhosted();
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<FirstInteraction v-if="!justLogged" :refresh-interaction="firstInteraction.refresh"
|
<FirstInteraction v-if="!justLogged" :refresh-interaction="firstInteraction.refresh"
|
||||||
:first-interaction="(firstInteraction.data.value || false)"></FirstInteraction>
|
:first-interaction="(firstInteraction.data.value || false)"></FirstInteraction>
|
||||||
|
|
||||||
|
|||||||
186
dashboard/pages/members.vue
Normal file
186
dashboard/pages/members.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DialogPermissionManager } from '#components';
|
||||||
|
import type { TPermission } from '~/shared/schema/TeamMemberSchema';
|
||||||
|
|
||||||
|
const { projectId, isGuest } = useProject();
|
||||||
|
|
||||||
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: 'me', label: '' },
|
||||||
|
{ key: 'email', label: 'Email' },
|
||||||
|
{ key: 'permission', label: 'Permission' },
|
||||||
|
{ key: 'pending', label: 'Status' },
|
||||||
|
{ key: 'action', label: 'Actions' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const { data: members, refresh: refreshMembers } = useFetch('/api/project/members/list', {
|
||||||
|
headers: useComputedHeaders({ useSnapshotDates: false })
|
||||||
|
});
|
||||||
|
|
||||||
|
const showAddMember = ref<boolean>(false);
|
||||||
|
|
||||||
|
const addMemberEmail = ref<string>("");
|
||||||
|
|
||||||
|
async function kickMember(email: string) {
|
||||||
|
const sure = confirm('Are you sure to kick ' + email + ' ?');
|
||||||
|
if (!sure) return;
|
||||||
|
try {
|
||||||
|
await $fetch('/api/project/members/kick', {
|
||||||
|
method: 'POST',
|
||||||
|
...signHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-pid': projectId.value ?? ''
|
||||||
|
}),
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
onResponseError({ request, response, options }) {
|
||||||
|
alert(response.statusText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshMembers();
|
||||||
|
} catch (ex: any) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMember() {
|
||||||
|
|
||||||
|
if (addMemberEmail.value.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
showAddMember.value = false;
|
||||||
|
|
||||||
|
await $fetch('/api/project/members/add', {
|
||||||
|
method: 'POST',
|
||||||
|
...signHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-pid': projectId.value ?? ''
|
||||||
|
}),
|
||||||
|
body: JSON.stringify({ email: addMemberEmail.value }),
|
||||||
|
onResponseError({ request, response, options }) {
|
||||||
|
alert(response.statusText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addMemberEmail.value = '';
|
||||||
|
|
||||||
|
refreshMembers();
|
||||||
|
|
||||||
|
} catch (ex: any) { }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
function openPermissionManagerDialog(member_id: string) {
|
||||||
|
modal.open(DialogPermissionManager, {
|
||||||
|
preventClose: true,
|
||||||
|
member_id,
|
||||||
|
onSuccess: () => {
|
||||||
|
modal.close();
|
||||||
|
refreshMembers();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
modal.close();
|
||||||
|
refreshMembers();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function permissionToString(permission: TPermission) {
|
||||||
|
const result: string[] = [];
|
||||||
|
if (permission.webAnalytics) result.push('w');
|
||||||
|
if (permission.events) result.push('e');
|
||||||
|
if (permission.ai) result.push('a');
|
||||||
|
if (permission.domains.includes('All domains')) {
|
||||||
|
result.push('+');
|
||||||
|
} else {
|
||||||
|
result.push(permission.domains.length.toString());
|
||||||
|
}
|
||||||
|
return result.join('');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6 pt-10">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<LyxUiInput class="px-4 py-1 w-full" placeholder="Add a new member" v-model="addMemberEmail">
|
||||||
|
</LyxUiInput>
|
||||||
|
<LyxUiButton @click="addMember" type="secondary"> Add </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
<div class="poppins text-[.8rem] mt-2 text-lyx-text-darker">
|
||||||
|
User should have been registered to Litlyx
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UTable :rows="members || []" :columns="columns">
|
||||||
|
|
||||||
|
<template #me-data="e">
|
||||||
|
<i v-if="e.row.me" class="far fa-user text-lyx-lightmode-text dark:text-lyx-text"></i>
|
||||||
|
<i v-if="!e.row.me"></i>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #email-data="e">
|
||||||
|
<div class="text-lyx-lightmode-text dark:text-lyx-text">
|
||||||
|
{{ e.row.email }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #pending-data="e">
|
||||||
|
<div class="text-lyx-lightmode-text dark:text-lyx-text">
|
||||||
|
{{ e.row.pending ? 'Pending' : 'Ok' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #permission-data="e">
|
||||||
|
<div class="text-lyx-lightmode-text dark:text-lyx-text flex gap-2">
|
||||||
|
<div v-if="e.row.role !== 'OWNER' && !isGuest">
|
||||||
|
<LyxUiButton class="!px-2" type="secondary"
|
||||||
|
@click="openPermissionManagerDialog(e.row.id.toString())">
|
||||||
|
<i class="far fa-gear"></i>
|
||||||
|
</LyxUiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<UBadge variant="outline" size="sm" v-if="e.row.permission.webAnalytics"
|
||||||
|
label="Analytics"> </UBadge>
|
||||||
|
<UBadge variant="outline" size="sm" v-if="e.row.permission.events" label="Events">
|
||||||
|
</UBadge>
|
||||||
|
<UBadge variant="outline" size="sm" v-if="e.row.permission.ai" label="AI"> </UBadge>
|
||||||
|
<UBadge variant="outline" color="blue" size="sm"
|
||||||
|
v-if="e.row.permission.domains.includes('All domains')" label="All domains">
|
||||||
|
</UBadge>
|
||||||
|
|
||||||
|
<UBadge variant="outline" size="sm" color="blue"
|
||||||
|
v-if="!e.row.permission.domains.includes('All domains')"
|
||||||
|
v-for="domain of e.row.permission.domains" :label="domain"> </UBadge>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<template #action-data="e" v-if="!isGuest">
|
||||||
|
<div @click="kickMember(e.row.email)" v-if="e.row.role != 'OWNER'"
|
||||||
|
class="text-red-500 hover:bg-black/20 cursor-pointer outline outline-[1px] outline-red-500 px-3 py-1 rounded-lg text-center">
|
||||||
|
Kick
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</UTable>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -7,7 +7,6 @@ const selfhosted = useSelfhosted();
|
|||||||
const items = [
|
const items = [
|
||||||
{ label: 'General', slot: 'general', tab: 'general' },
|
{ label: 'General', slot: 'general', tab: 'general' },
|
||||||
{ label: 'Domains', slot: 'domains', tab: 'domains' },
|
{ label: 'Domains', slot: 'domains', tab: 'domains' },
|
||||||
{ label: 'Members', slot: 'members', tab: 'members' },
|
|
||||||
{ label: 'Billing', slot: 'billing', tab: 'billing' },
|
{ label: 'Billing', slot: 'billing', tab: 'billing' },
|
||||||
{ label: 'Codes', slot: 'codes', tab: 'codes' },
|
{ label: 'Codes', slot: 'codes', tab: 'codes' },
|
||||||
{ label: 'Account', slot: 'account', tab: 'account' }
|
{ label: 'Account', slot: 'account', tab: 'account' }
|
||||||
@@ -27,9 +26,6 @@ const items = [
|
|||||||
<template #domains>
|
<template #domains>
|
||||||
<SettingsData :key="refreshKey"></SettingsData>
|
<SettingsData :key="refreshKey"></SettingsData>
|
||||||
</template>
|
</template>
|
||||||
<template #members>
|
|
||||||
<SettingsMembers :key="refreshKey"></SettingsMembers>
|
|
||||||
</template>
|
|
||||||
<template #billing>
|
<template #billing>
|
||||||
<SettingsBilling v-if="!selfhosted" :key="refreshKey"></SettingsBilling>
|
<SettingsBilling v-if="!selfhosted" :key="refreshKey"></SettingsBilling>
|
||||||
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"
|
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"
|
||||||
|
|||||||
15
dashboard/pages/test_links.vue
Normal file
15
dashboard/pages/test_links.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const { data: links } = useFetch('/api/project/links/list', {
|
||||||
|
headers: useComputedHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="link of links">
|
||||||
|
{{ link }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
BIN
dashboard/public/tech-icons/wpel.png
Normal file
BIN
dashboard/public/tech-icons/wpel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
dashboard/public/yt.png
Normal file
BIN
dashboard/public/yt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
36
dashboard/server/api/admin/metrics.ts
Normal file
36
dashboard/server/api/admin/metrics.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||||
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
const userData = getRequestUser(event);
|
||||||
|
if (!userData?.logged) return;
|
||||||
|
if (!userData.user.roles.includes('ADMIN')) return;
|
||||||
|
|
||||||
|
const { filterFrom, filterTo } = getQuery(event);
|
||||||
|
|
||||||
|
|
||||||
|
const matchQuery = {
|
||||||
|
created_at: {
|
||||||
|
$gte: new Date(filterFrom as string),
|
||||||
|
$lte: new Date(filterTo as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalProjects = await ProjectModel.countDocuments({ ...matchQuery });
|
||||||
|
const premiumProjects = await ProjectModel.countDocuments({ ...matchQuery, premium: true });
|
||||||
|
|
||||||
|
const deadProjects = await ProjectModel.countDocuments({ ...matchQuery });
|
||||||
|
|
||||||
|
const totalUsers = await UserModel.countDocuments({ ...matchQuery });
|
||||||
|
|
||||||
|
const totalVisits = 0;
|
||||||
|
|
||||||
|
const totalEvents = await EventModel.countDocuments({ ...matchQuery });
|
||||||
|
|
||||||
|
|
||||||
|
return { totalProjects, premiumProjects, deadProjects, totalUsers, totalVisits, totalEvents }
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestDataOld(event);
|
const data = await getRequestData(event, [], ['AI']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type OpenAI from "openai";
|
|||||||
import { getChartsInMessage } from "~/server/services/AiService";
|
import { getChartsInMessage } from "~/server/services/AiService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestDataOld(event);
|
const data = await getRequestData(event, [], ['AI']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const isAdmin = data.user.user.roles.includes('ADMIN');
|
const isAdmin = data.user.user.roles.includes('ADMIN');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestDataOld(event);
|
const data = await getRequestData(event, [], ['AI']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function getAiChatRemainings(project_id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestDataOld(event);
|
const data = await getRequestData(event, [], ['AI']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid } = data;
|
const { pid } = data;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestDataOld(event);
|
const data = await getRequestData(event, [], ['AI']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
|
|||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const data = await getRequestDataOld(event);
|
const data = await getRequestData(event, [], ['AI']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid } = data;
|
const { pid } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit, domain } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'DOMAIN', 'RANGE', 'SCHEMA']);
|
const data = await getRequestData(event, ['DOMAIN', 'RANGE', 'SCHEMA'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { schemaName, pid, from, to, model, project_id, domain } = data;
|
const { schemaName, pid, from, to, model, project_id, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit, domain } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit, domain } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'DOMAIN', 'RANGE']);
|
const data = await getRequestData(event, ['DOMAIN', 'RANGE'], ['EVENTS']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit, domain } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit, domain } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'RANGE', 'GUEST', 'DOMAIN']);
|
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit, domain } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Redis } from "~/server/services/CacheService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['OFFSET', 'RANGE', 'GUEST', 'DOMAIN']);
|
const data = await getRequestData(event, ['OFFSET', 'RANGE', 'DOMAIN'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, project_id, limit, domain } = data;
|
const { pid, from, to, project_id, limit, domain } = data;
|
||||||
|
|||||||
@@ -1,18 +1,45 @@
|
|||||||
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST']);
|
const data = await getRequestData(event, []);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id, project, user } = data;
|
||||||
|
|
||||||
const result = await VisitModel.aggregate([
|
const result: { _id: string, visits: number }[] = await VisitModel.aggregate([
|
||||||
{ $match: { project_id, } },
|
{ $match: { project_id, } },
|
||||||
{ $group: { _id: "$website", visits: { $sum: 1 } } },
|
{ $group: { _id: "$website", visits: { $sum: 1 } } },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return result as { _id: string, visits: number }[];
|
const isOwner = user.id === project.owner.toString();
|
||||||
|
if (isOwner) return [
|
||||||
|
{
|
||||||
|
_id: 'All domains',
|
||||||
|
visits: result.reduce((a, e) => a + e.visits, 0)
|
||||||
|
},
|
||||||
|
...result
|
||||||
|
]
|
||||||
|
|
||||||
|
const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id, pending: false });
|
||||||
|
if (!member) return setResponseStatus(event, 400, 'Not a member');
|
||||||
|
if (!member.permission) return setResponseStatus(event, 400, 'No permission');
|
||||||
|
|
||||||
|
if (member.permission.domains.includes('All domains')) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
_id: 'All domains',
|
||||||
|
visits: result.reduce((a, e) => a + e.visits, 0)
|
||||||
|
},
|
||||||
|
...result
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.filter(e => {
|
||||||
|
return member.permission.domains.includes(e._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
18
dashboard/server/api/project/links/list.ts
Normal file
18
dashboard/server/api/project/links/list.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
import { ProjectLinkModel } from "~/shared/schema/project/ProjectLinkSchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project_id, project } = data;
|
||||||
|
|
||||||
|
const owner = await UserModel.findById(project.owner);
|
||||||
|
if (!owner) return setResponseStatus(event, 400, 'No owner');
|
||||||
|
|
||||||
|
const links = await ProjectLinkModel.find({ project_id });
|
||||||
|
return links;
|
||||||
|
|
||||||
|
});
|
||||||
21
dashboard/server/api/project/members/accept.post.ts
Normal file
21
dashboard/server/api/project/members/accept.post.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, [], []);
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
const { project_id } = body;
|
||||||
|
if (!project_id) return setResponseStatus(event, 400, 'project_id is required');
|
||||||
|
|
||||||
|
const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id });
|
||||||
|
if (!member) return setResponseStatus(event, 400, 'member not found');
|
||||||
|
|
||||||
|
member.pending = false;
|
||||||
|
await member.save();
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,21 +1,51 @@
|
|||||||
|
|
||||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||||
import { UserModel } from "@schema/UserSchema";
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
import { EmailServiceHelper } from "~/server/services/EmailServiceHelper";
|
||||||
|
import { EmailService } from "~/shared/services/EmailService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id, project } = data;
|
||||||
|
|
||||||
const { email } = await readBody(event);
|
const { email } = await readBody(event);
|
||||||
|
|
||||||
const targetUser = await UserModel.findOne({ email });
|
const targetUser = await UserModel.findOne({ email });
|
||||||
if (!targetUser) return setResponseStatus(event, 400, 'No user with this email');
|
|
||||||
|
|
||||||
|
|
||||||
|
const link = `http://127.0.0.1:3000/accept_invite?project_id=${project_id.toString()}`;
|
||||||
|
|
||||||
|
if (!targetUser) {
|
||||||
|
|
||||||
|
const exist = await TeamMemberModel.exists({ project_id, email });
|
||||||
|
if (exist) return setResponseStatus(event, 400, 'Member already invited');
|
||||||
|
|
||||||
|
await TeamMemberModel.create({
|
||||||
|
project_id,
|
||||||
|
email,
|
||||||
|
pending: true,
|
||||||
|
role: 'GUEST'
|
||||||
|
});
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
const emailData = EmailService.getEmailServerInfo('invite_project_noaccount', {
|
||||||
|
target: email,
|
||||||
|
projectName: project.name,
|
||||||
|
link
|
||||||
|
});
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const exist = await TeamMemberModel.exists({ project_id, user_id: targetUser.id });
|
||||||
|
if (exist) return setResponseStatus(event, 400, 'Member already invited');
|
||||||
|
|
||||||
await TeamMemberModel.create({
|
await TeamMemberModel.create({
|
||||||
project_id,
|
project_id,
|
||||||
user_id: targetUser.id,
|
user_id: targetUser.id,
|
||||||
@@ -23,6 +53,20 @@ export default defineEventHandler(async event => {
|
|||||||
role: 'GUEST'
|
role: 'GUEST'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
const emailData = EmailService.getEmailServerInfo('invite_project', {
|
||||||
|
|
||||||
|
target: email,
|
||||||
|
projectName: project.name,
|
||||||
|
link
|
||||||
|
});
|
||||||
|
EmailServiceHelper.sendEmail(emailData);
|
||||||
|
});
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
18
dashboard/server/api/project/members/decline.post.ts
Normal file
18
dashboard/server/api/project/members/decline.post.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, [], []);
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
const { project_id } = body;
|
||||||
|
if (!project_id) return setResponseStatus(event, 400, 'project_id is required');
|
||||||
|
|
||||||
|
const member = await TeamMemberModel.deleteOne({ project_id, user_id: data.user.id });
|
||||||
|
if (!member) return setResponseStatus(event, 400, 'member not found');
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
|
||||||
|
});
|
||||||
25
dashboard/server/api/project/members/edit.post.ts
Normal file
25
dashboard/server/api/project/members/edit.post.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
const { member_id, webAnalytics, events, ai, domains } = body;
|
||||||
|
|
||||||
|
if (!member_id) return setResponseStatus(event, 400, 'permission_id is required');
|
||||||
|
|
||||||
|
const edited = await TeamMemberModel.updateOne({ _id: member_id }, {
|
||||||
|
permission: {
|
||||||
|
webAnalytics,
|
||||||
|
events,
|
||||||
|
ai,
|
||||||
|
domains
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: edited.modifiedCount == 1 }
|
||||||
|
|
||||||
|
});
|
||||||
23
dashboard/server/api/project/members/get.ts
Normal file
23
dashboard/server/api/project/members/get.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
import { TeamMemberModel, TPermission, TTeamMember } from "@schema/TeamMemberSchema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
const data = await getRequestData(event, []);
|
||||||
|
if (!data) return;
|
||||||
|
const { member_id } = getQuery(event);
|
||||||
|
const member = await TeamMemberModel.findById(member_id);
|
||||||
|
if (!member) return setResponseStatus(event, 400, 'Cannot get member');
|
||||||
|
|
||||||
|
const resultPermission: TPermission = {
|
||||||
|
ai: false,
|
||||||
|
domains: [],
|
||||||
|
events: false,
|
||||||
|
webAnalytics: false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission: resultPermission,
|
||||||
|
...member.toJSON() as any
|
||||||
|
} as TTeamMember
|
||||||
|
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import { UserModel } from "@schema/UserSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
const data = await getRequestData(event, []);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id, user } = data;
|
const { project_id, user } = data;
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
import { TeamMemberModel, TeamMemberRole, TPermission, TTeamMember } from "@schema/TeamMemberSchema";
|
||||||
import { UserModel } from "@schema/UserSchema";
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
|
||||||
|
export type MemberWithPermissions = {
|
||||||
|
id: string | null,
|
||||||
|
email: string,
|
||||||
|
name: string,
|
||||||
|
role: TeamMemberRole,
|
||||||
|
pending: boolean,
|
||||||
|
me: boolean,
|
||||||
|
permission: TPermission
|
||||||
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestDataOld(event, { requireSchema: false });
|
const data = await getRequestData(event);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id, project, user } = data;
|
const { project_id, project, user } = data;
|
||||||
@@ -15,25 +24,42 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
const members = await TeamMemberModel.find({ project_id });
|
const members = await TeamMemberModel.find({ project_id });
|
||||||
|
|
||||||
const result: { email: string, name: string, role: string, pending: boolean, me: boolean }[] = [];
|
const result: MemberWithPermissions[] = [];
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
|
id: null,
|
||||||
email: owner.email,
|
email: owner.email,
|
||||||
name: owner.name,
|
name: owner.name,
|
||||||
role: 'OWNER',
|
role: 'OWNER',
|
||||||
pending: false,
|
pending: false,
|
||||||
me: user.id === owner.id
|
me: user.id === owner.id,
|
||||||
|
permission: {
|
||||||
|
webAnalytics: true,
|
||||||
|
events: true,
|
||||||
|
ai: true,
|
||||||
|
domains: ['All domains']
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
const userMember = await UserModel.findById(member.user_id);
|
const userMember = await UserModel.findById(member.user_id);
|
||||||
if (!userMember) continue;
|
if (!userMember) continue;
|
||||||
|
|
||||||
|
const permission: TPermission = {
|
||||||
|
webAnalytics: member.permission?.webAnalytics || false,
|
||||||
|
events: member.permission?.events || false,
|
||||||
|
ai: member.permission?.ai || false,
|
||||||
|
domains: member.permission?.domains || []
|
||||||
|
}
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
|
id: member.id,
|
||||||
email: userMember.email,
|
email: userMember.email,
|
||||||
name: userMember.name,
|
name: userMember.name,
|
||||||
role: member.role,
|
role: member.role,
|
||||||
pending: member.pending,
|
pending: member.pending,
|
||||||
me: user.id === userMember.id
|
me: user.id === userMember.id,
|
||||||
|
permission
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
dashboard/server/api/project/members/me.ts
Normal file
39
dashboard/server/api/project/members/me.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||||
|
import { TeamMemberModel, TeamMemberRole, TPermission, TTeamMember } from "@schema/TeamMemberSchema";
|
||||||
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event, []);
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const { project_id, project, user } = data;
|
||||||
|
|
||||||
|
const owner = await UserModel.findById(project.owner, { _id: 1 });
|
||||||
|
|
||||||
|
if (owner && owner._id.toString() === user.id) return {
|
||||||
|
ai: true,
|
||||||
|
domains: ['All domains'],
|
||||||
|
events: true,
|
||||||
|
webAnalytics: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await TeamMemberModel.findOne({ project_id, user_id: user.id });
|
||||||
|
|
||||||
|
if (!member) return {
|
||||||
|
ai: true,
|
||||||
|
domains: ['All domains'],
|
||||||
|
events: true,
|
||||||
|
webAnalytics: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ai: false,
|
||||||
|
domains: [],
|
||||||
|
events: false,
|
||||||
|
webAnalytics: false,
|
||||||
|
...(member.permission as any),
|
||||||
|
} as TPermission
|
||||||
|
|
||||||
|
});
|
||||||
44
dashboard/server/api/project/members/pending.ts
Normal file
44
dashboard/server/api/project/members/pending.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const data = await getRequestData(event);
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const members = await TeamMemberModel.aggregate([
|
||||||
|
{
|
||||||
|
$match:
|
||||||
|
{
|
||||||
|
$or: [
|
||||||
|
{ user_id: new Types.ObjectId(data.user.id) },
|
||||||
|
{ email: data.user.user.email }
|
||||||
|
],
|
||||||
|
pending: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'projects',
|
||||||
|
as: 'project',
|
||||||
|
foreignField: '_id',
|
||||||
|
localField: 'project_id',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addFields: {
|
||||||
|
project_name: { $arrayElemAt: ["$project.name", 0] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
project: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return members;
|
||||||
|
|
||||||
|
});
|
||||||
@@ -9,16 +9,16 @@ import { checkSliceValidity } from "~/server/services/TimelineService";
|
|||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
|
||||||
const data = await getRequestDataOld(event, { requireSchema: false, requireSlice: true });
|
const data = await getRequestData(event, ['SLICE', 'RANGE', 'DOMAIN'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id } = data;
|
const { pid, from, to, slice, project_id, domain } = data;
|
||||||
|
|
||||||
|
|
||||||
const cacheKey = `timeline:bouncing_rate:${pid}:${slice}:${from}:${to}`;
|
const cacheKey = `timeline:bouncing_rate:${pid}:${slice}:${from}:${to}`;
|
||||||
const cacheExp = 60 * 60; //1 hour
|
const cacheExp = 60 * 60; //1 hour
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async (noStore, updateExp) => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
|
|
||||||
const [sliceValid, errorOrDays] = checkSliceValidity(from, to, slice);
|
const [sliceValid, errorOrDays] = checkSliceValidity(from, to, slice);
|
||||||
if (!sliceValid) throw Error(errorOrDays);
|
if (!sliceValid) throw Error(errorOrDays);
|
||||||
@@ -36,7 +36,8 @@ export default defineEventHandler(async event => {
|
|||||||
created_at: {
|
created_at: {
|
||||||
$gte: DateService.startOfSlice(date, slice),
|
$gte: DateService.startOfSlice(date, slice),
|
||||||
$lte: DateService.endOfSlice(date, slice)
|
$lte: DateService.endOfSlice(date, slice)
|
||||||
}
|
},
|
||||||
|
website: domain
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ $group: { _id: "$session", count: { $sum: 1, } } },
|
{ $group: { _id: "$session", count: { $sum: 1, } } },
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { executeTimelineAggregation } from "~/server/services/TimelineService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['SLICE', 'GUEST', 'DOMAIN', 'RANGE', 'OFFSET']);
|
const data = await getRequestData(event, ['SLICE', 'DOMAIN', 'RANGE', 'OFFSET'], ['EVENTS']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
|
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { executeAdvancedTimelineAggregation } from "~/server/services/TimelineSe
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['GUEST', 'RANGE', 'SLICE', 'DOMAIN']);
|
const data = await getRequestData(event, ['RANGE', 'SLICE', 'DOMAIN'], ['EVENTS']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { from, to, slice, project_id, timeOffset, domain } = data;
|
const { from, to, slice, project_id, timeOffset, domain } = data;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { executeTimelineAggregation } from "~/server/services/TimelineService";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['SLICE', 'GUEST', 'DOMAIN', 'RANGE', 'OFFSET']);
|
const data = await getRequestData(event, ['SLICE', 'DOMAIN', 'RANGE', 'OFFSET'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
|
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 }
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['SLICE', 'GUEST', 'DOMAIN', 'RANGE']);
|
const data = await getRequestData(event, ['SLICE', 'DOMAIN', 'RANGE'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
|
const { pid, from, to, slice, project_id, domain } = data;
|
||||||
|
|
||||||
const cacheKey = `timeline:sessions_duration:${pid}:${slice}:${from}:${to}:${domain}`;
|
const cacheKey = `timeline:sessions_duration:${pid}:${slice}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { executeAdvancedTimelineAggregation } from "~/server/services/TimelineSe
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, ['SLICE', 'GUEST', 'DOMAIN', 'RANGE', 'OFFSET']);
|
const data = await getRequestData(event, ['SLICE', 'DOMAIN', 'RANGE', 'OFFSET'], ['WEB']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
|
const { pid, from, to, slice, project_id, timeOffset, domain } = data;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { EventHandlerRequest, H3Event } from 'h3'
|
|||||||
import { allowedModels, TModelName } from "../services/DataService";
|
import { allowedModels, TModelName } from "../services/DataService";
|
||||||
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
|
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
|
||||||
import { Model, Types } from "mongoose";
|
import { Model, Types } from "mongoose";
|
||||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
import { TeamMemberModel, TPermission } from "@schema/TeamMemberSchema";
|
||||||
import { Slice } from "@services/DateService";
|
import { Slice } from "@services/DateService";
|
||||||
import { ADMIN_EMAILS } from "~/shared/data/ADMINS";
|
import { ADMIN_EMAILS } from "~/shared/data/ADMINS";
|
||||||
|
|
||||||
@@ -32,7 +32,22 @@ export type GetRequestDataOptions = {
|
|||||||
/** @default false */ requireOffset?: boolean,
|
/** @default false */ requireOffset?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RequestDataScope = 'GUEST' | 'SCHEMA' | 'ANON' | 'SLICE' | 'RANGE' | 'OFFSET' | 'DOMAIN';
|
export type RequestDataScope = 'SCHEMA' | 'ANON' | 'SLICE' | 'RANGE' | 'OFFSET' | 'DOMAIN';
|
||||||
|
export type RequestDataPermissions = 'WEB' | 'EVENTS' | 'AI' | 'OWNER';
|
||||||
|
|
||||||
|
async function getAccessPermission(user_id: string, project: TProject): Promise<TPermission> {
|
||||||
|
if (!project) return { ai: false, domains: [], events: false, webAnalytics: false }
|
||||||
|
|
||||||
|
//TODO: Create table with admins
|
||||||
|
if (user_id === '66520c90f381ec1e9284938b') return { ai: true, domains: ['All domains'], events: true, webAnalytics: true }
|
||||||
|
|
||||||
|
const owner = project.owner.toString();
|
||||||
|
const project_id = project._id;
|
||||||
|
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 });
|
||||||
|
if (!member) return { ai: false, domains: [], events: false, webAnalytics: false }
|
||||||
|
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) {
|
||||||
if (!project) return [false, 'NONE'];
|
if (!project) return [false, 'NONE'];
|
||||||
@@ -48,10 +63,9 @@ async function hasAccessToProject(user_id: string, project: TProject) {
|
|||||||
return [false, 'NONE'];
|
return [false, 'NONE'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRequestData(event: H3Event<EventHandlerRequest>, required_scopes: RequestDataScope[] = []) {
|
export async function getRequestData(event: H3Event<EventHandlerRequest>, required_scopes: RequestDataScope[] = [], required_permissions: RequestDataPermissions[] = []) {
|
||||||
|
|
||||||
const requireSchema = required_scopes.includes('SCHEMA');
|
const requireSchema = required_scopes.includes('SCHEMA');
|
||||||
const allowGuests = required_scopes.includes('GUEST');
|
|
||||||
const allowAnon = required_scopes.includes('ANON');
|
const allowAnon = required_scopes.includes('ANON');
|
||||||
const requireSlice = required_scopes.includes('SLICE');
|
const requireSlice = required_scopes.includes('SLICE');
|
||||||
const requireRange = required_scopes.includes('RANGE');
|
const requireRange = required_scopes.includes('RANGE');
|
||||||
@@ -61,7 +75,10 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, requir
|
|||||||
const pid = getHeader(event, 'x-pid');
|
const pid = getHeader(event, 'x-pid');
|
||||||
if (!pid) return setResponseStatus(event, 400, 'x-pid is required');
|
if (!pid) return setResponseStatus(event, 400, 'x-pid is required');
|
||||||
|
|
||||||
let domain: any = getHeader(event, 'x-domain');
|
const originalDomain = getHeader(event, 'x-domain')?.toString();
|
||||||
|
|
||||||
|
let domain: any = originalDomain;
|
||||||
|
|
||||||
if (requireDomain) {
|
if (requireDomain) {
|
||||||
if (domain == null || domain == undefined || domain.length == 0) return setResponseStatus(event, 400, 'x-domain is required');
|
if (domain == null || domain == undefined || domain.length == 0) return setResponseStatus(event, 400, 'x-domain is required');
|
||||||
}
|
}
|
||||||
@@ -108,16 +125,40 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, requir
|
|||||||
const project = await ProjectModel.findById(project_id);
|
const project = await ProjectModel.findById(project_id);
|
||||||
if (!project) return setResponseStatus(event, 400, 'project not found');
|
if (!project) return setResponseStatus(event, 400, 'project not found');
|
||||||
|
|
||||||
if (!allowAnon) {
|
|
||||||
const [hasAccess, role] = await hasAccessToProject(user.id, project);
|
if (user.id != project.owner.toString()) {
|
||||||
if (!hasAccess) return setResponseStatus(event, 400, 'no access to project');
|
if (required_permissions.includes('OWNER')) return setResponseStatus(event, 403, 'ADMIN permission required');
|
||||||
if (role === 'GUEST' && !allowGuests) return setResponseStatus(event, 403, 'only owner can access this');
|
const hasAccess = await TeamMemberModel.findOne({ project_id, user_id: user.id });
|
||||||
|
if (!hasAccess) return setResponseStatus(event, 403, 'No permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (required_permissions.length > 0 || requireDomain) {
|
||||||
|
|
||||||
|
const permission = await getAccessPermission(user.id, project);
|
||||||
|
|
||||||
|
if (required_permissions.includes('WEB') && permission.webAnalytics === false) {
|
||||||
|
return setResponseStatus(event, 403, 'WEB permission required');
|
||||||
|
}
|
||||||
|
if (required_permissions.includes('EVENTS') && permission.events === false) {
|
||||||
|
return setResponseStatus(event, 403, 'EVENTS permission required');
|
||||||
|
}
|
||||||
|
if (required_permissions.includes('AI') && permission.ai === false) {
|
||||||
|
return setResponseStatus(event, 403, 'AI permission required');
|
||||||
|
}
|
||||||
|
if (requireDomain && originalDomain) {
|
||||||
|
if (!permission.domains.includes('All domains') && !permission.domains.includes(originalDomain)) {
|
||||||
|
return setResponseStatus(event, 403, 'DOMAIN permission required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: from as string,
|
from: from as string,
|
||||||
to: to as string,
|
to: to as string,
|
||||||
domain: domain as string,
|
domain: domain as string,
|
||||||
|
originalDomain: originalDomain as string,
|
||||||
pid, project_id, project, user, limit, slice, schemaName, model, timeOffset: offset
|
pid, project_id, project, user, limit, slice, schemaName, model, timeOffset: offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile": "tsc"
|
"compile": "tsc",
|
||||||
|
"workspace:deploy": "ts-node ../scripts/email/deploy.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Emily",
|
"author": "Emily",
|
||||||
|
|||||||
1737
email/pnpm-lock.yaml
generated
1737
email/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,64 @@
|
|||||||
import { TransactionalEmailsApi, SendSmtpEmail } from '@getbrevo/brevo';
|
import { TransactionalEmailsApi, SendSmtpEmail, ContactsApi } from '@getbrevo/brevo';
|
||||||
import * as TEMPLATE from './templates'
|
import * as TEMPLATE from './templates'
|
||||||
|
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
|
|
||||||
private static apiInstance = new TransactionalEmailsApi();
|
private static apiInstance = new TransactionalEmailsApi();
|
||||||
|
private static apiContacts = new ContactsApi();
|
||||||
|
|
||||||
static init(apiKey: string) {
|
static init(apiKey: string) {
|
||||||
this.apiInstance.setApiKey(0, apiKey);
|
this.apiInstance.setApiKey(0, apiKey);
|
||||||
|
this.apiContacts.setApiKey(0, apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendInviteEmail(target: string, projectName: string, link: string) {
|
||||||
|
try {
|
||||||
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
|
sendSmtpEmail.subject = "⚡ Invite";
|
||||||
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
|
|
||||||
|
sendSmtpEmail.htmlContent = TEMPLATE.PROJECT_INVITE_EMAIL
|
||||||
|
.replace(/\[Project Name\]/, projectName)
|
||||||
|
.replace(/\[Link\]/, link)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('ERROR SENDING EMAIL', ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendInviteEmailNoAccount(target: string, projectName: string, link: string) {
|
||||||
|
try {
|
||||||
|
const sendSmtpEmail = new SendSmtpEmail();
|
||||||
|
sendSmtpEmail.subject = "⚡ Invite - No account";
|
||||||
|
sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
|
||||||
|
sendSmtpEmail.to = [{ "email": target }];
|
||||||
|
|
||||||
|
sendSmtpEmail.htmlContent = TEMPLATE.PROJECT_INVITE_EMAIL_NO_ACCOUNT
|
||||||
|
.replace(/\[Project Name\]/, projectName)
|
||||||
|
.replace(/\[Link\]/, link)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('ERROR SENDING EMAIL', ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createContact(email: string) {
|
||||||
|
try {
|
||||||
|
await this.apiContacts.createContact({ email });
|
||||||
|
await this.apiContacts.addContactToList(12, { emails: [email] })
|
||||||
|
} catch (ex) {
|
||||||
|
console.error('ERROR ADDING CONTACT', ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async sendLimitEmail50(target: string, projectName: string) {
|
static async sendLimitEmail50(target: string, projectName: string) {
|
||||||
|
|||||||
@@ -38,6 +38,36 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/send/invite', express.json(), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { target, projectName, link } = req.body;
|
||||||
|
const ok = await EmailService.sendInviteEmail(target, projectName, link);
|
||||||
|
res.json({ ok });
|
||||||
|
} catch (ex) {
|
||||||
|
res.status(500).json({ error: ex.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/send/invite/noaccount', express.json(), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { target, projectName, link } = req.body;
|
||||||
|
const ok = await EmailService.sendInviteEmailNoAccount(target, projectName, link);
|
||||||
|
res.json({ ok });
|
||||||
|
} catch (ex) {
|
||||||
|
res.status(500).json({ error: ex.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/brevolist/add', express.json(), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
const ok = await EmailService.createContact(email);
|
||||||
|
res.json({ ok });
|
||||||
|
} catch (ex) {
|
||||||
|
res.status(500).json({ error: ex.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/send/confirm', express.json(), async (req, res) => {
|
app.post('/send/confirm', express.json(), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { target, link } = req.body;
|
const { target, link } = req.body;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { ANOMALY_VISITS_EVENTS_EMAIL } from '../../templates/AnomalyUsageEmail';
|
|||||||
import { ANOMALY_DOMAIN_EMAIL } from '../../templates/AnomalyDomainEmail';
|
import { ANOMALY_DOMAIN_EMAIL } from '../../templates/AnomalyDomainEmail';
|
||||||
import { CONFIRM_EMAIL } from '../../templates/ConfirmEmail';
|
import { CONFIRM_EMAIL } from '../../templates/ConfirmEmail';
|
||||||
import { RESET_PASSWORD_EMAIL } from '../../templates/ResetPasswordEmail';
|
import { RESET_PASSWORD_EMAIL } from '../../templates/ResetPasswordEmail';
|
||||||
|
import { PROJECT_INVITE_EMAIL } from '../../templates/ProjectInviteEmail';
|
||||||
|
import { PROJECT_INVITE_EMAIL_NO_ACCOUNT } from '../../templates/ProjectInviteEmailNoAccount';
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -19,5 +20,7 @@ export {
|
|||||||
ANOMALY_VISITS_EVENTS_EMAIL,
|
ANOMALY_VISITS_EVENTS_EMAIL,
|
||||||
ANOMALY_DOMAIN_EMAIL,
|
ANOMALY_DOMAIN_EMAIL,
|
||||||
CONFIRM_EMAIL,
|
CONFIRM_EMAIL,
|
||||||
RESET_PASSWORD_EMAIL
|
RESET_PASSWORD_EMAIL,
|
||||||
|
PROJECT_INVITE_EMAIL,
|
||||||
|
PROJECT_INVITE_EMAIL_NO_ACCOUNT
|
||||||
}
|
}
|
||||||
21
email/templates/ProjectInviteEmail.ts
Normal file
21
email/templates/ProjectInviteEmail.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export const PROJECT_INVITE_EMAIL = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Thank You for Upgrading Your Litlyx Plan!</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
|
||||||
|
<!-- Email Content -->
|
||||||
|
|
||||||
|
<p>Invited by <strong>[Project Name]</strong> on <strong>Litlyx</strong></p>
|
||||||
|
|
||||||
|
<p>Best regards,</p>
|
||||||
|
|
||||||
|
<p>Antonio,</p>
|
||||||
|
<p>CEO @ Litlyx</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
21
email/templates/ProjectInviteEmailNoAccount.ts
Normal file
21
email/templates/ProjectInviteEmailNoAccount.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export const PROJECT_INVITE_EMAIL_NO_ACCOUNT = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Thank You for Upgrading Your Litlyx Plan!</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
|
||||||
|
<!-- Email Content -->
|
||||||
|
|
||||||
|
<p>Invited by <strong>[Project Name]</strong> on <strong>Litlyx</strong> NO_ACCOUNT</p>
|
||||||
|
|
||||||
|
<p>Best regards,</p>
|
||||||
|
|
||||||
|
<p>Antonio,</p>
|
||||||
|
<p>CEO @ Litlyx</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
@@ -19,7 +19,7 @@ async function main() {
|
|||||||
if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true });
|
if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true });
|
||||||
fs.ensureDirSync(TMP_PATH);
|
fs.ensureDirSync(TMP_PATH);
|
||||||
|
|
||||||
console.log('Creting zip file');
|
console.log('Creating zip file');
|
||||||
const archive = createZip(TMP_PATH + '/' + ZIP_NAME);
|
const archive = createZip(TMP_PATH + '/' + ZIP_NAME);
|
||||||
archive.directory(LOCAL_PATH + '/dist', '/dist');
|
archive.directory(LOCAL_PATH + '/dist', '/dist');
|
||||||
archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' })
|
archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' })
|
||||||
|
|||||||
@@ -2,11 +2,21 @@ import { model, Schema, Types } from 'mongoose';
|
|||||||
|
|
||||||
export type TeamMemberRole = 'ADMIN' | 'GUEST';
|
export type TeamMemberRole = 'ADMIN' | 'GUEST';
|
||||||
|
|
||||||
|
|
||||||
|
export type TPermission = {
|
||||||
|
webAnalytics: boolean,
|
||||||
|
events: boolean,
|
||||||
|
ai: boolean,
|
||||||
|
domains: string[],
|
||||||
|
}
|
||||||
|
|
||||||
export type TTeamMember = {
|
export type TTeamMember = {
|
||||||
_id: Schema.Types.ObjectId,
|
_id: Schema.Types.ObjectId,
|
||||||
project_id: Schema.Types.ObjectId,
|
project_id: Schema.Types.ObjectId,
|
||||||
user_id: Schema.Types.ObjectId,
|
user_id?: Schema.Types.ObjectId,
|
||||||
|
email?: string,
|
||||||
role: TeamMemberRole,
|
role: TeamMemberRole,
|
||||||
|
permission: TPermission,
|
||||||
pending: boolean,
|
pending: boolean,
|
||||||
created_at: Date,
|
created_at: Date,
|
||||||
}
|
}
|
||||||
@@ -14,7 +24,9 @@ export type TTeamMember = {
|
|||||||
const TeamMemberSchema = new Schema<TTeamMember>({
|
const TeamMemberSchema = new Schema<TTeamMember>({
|
||||||
project_id: { type: Types.ObjectId, index: true },
|
project_id: { type: Types.ObjectId, index: true },
|
||||||
user_id: { type: Types.ObjectId, index: true },
|
user_id: { type: Types.ObjectId, index: true },
|
||||||
|
email: { type: String, index: true },
|
||||||
role: { type: String, required: true },
|
role: { type: String, required: true },
|
||||||
|
permission: { type: Schema.Types.Mixed },
|
||||||
pending: { type: Boolean, required: true },
|
pending: { type: Boolean, required: true },
|
||||||
created_at: { type: Date, required: true, default: () => Date.now() },
|
created_at: { type: Date, required: true, default: () => Date.now() },
|
||||||
});
|
});
|
||||||
|
|||||||
19
shared_global/schema/project/ProjectLinkSchema.ts
Normal file
19
shared_global/schema/project/ProjectLinkSchema.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
import { TPermission } from '../TeamMemberSchema';
|
||||||
|
|
||||||
|
export type TProjectLink = {
|
||||||
|
_id: Schema.Types.ObjectId,
|
||||||
|
project_id: Schema.Types.ObjectId,
|
||||||
|
link_id: string,
|
||||||
|
password: string,
|
||||||
|
permission: TPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectLinkSchema = new Schema<TProjectLink>({
|
||||||
|
project_id: { type: Types.ObjectId, index: true, unique: true },
|
||||||
|
link_id: { type: String, required: true },
|
||||||
|
password: { type: String },
|
||||||
|
permission: { type: Schema.Types.Mixed },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ProjectLinkModel = model<TProjectLink>('project_links', ProjectLinkSchema);
|
||||||
@@ -8,6 +8,8 @@ const templateMap = {
|
|||||||
limit_50: '/limit/50',
|
limit_50: '/limit/50',
|
||||||
limit_90: '/limit/90',
|
limit_90: '/limit/90',
|
||||||
limit_max: '/limit/max',
|
limit_max: '/limit/max',
|
||||||
|
invite_project: '/invite',
|
||||||
|
invite_project_noaccount: '/invite/noaccount'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type EmailTemplate = keyof typeof templateMap;
|
export type EmailTemplate = keyof typeof templateMap;
|
||||||
@@ -22,7 +24,9 @@ type EmailData =
|
|||||||
| { template: 'anomaly_visits_events', data: { target: string, projectName: string, data: any[] } }
|
| { template: 'anomaly_visits_events', data: { target: string, projectName: string, data: any[] } }
|
||||||
| { template: 'limit_50', data: { target: string, projectName: string } }
|
| { template: 'limit_50', data: { target: string, projectName: string } }
|
||||||
| { template: 'limit_90', data: { target: string, projectName: string } }
|
| { template: 'limit_90', data: { target: string, projectName: string } }
|
||||||
| { 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_noaccount', data: { target: string, projectName: string, link: 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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user