add domain wipe

This commit is contained in:
Emily
2024-11-11 16:54:02 +01:00
parent f06d7d78fc
commit 2929b229c4
11 changed files with 440 additions and 5 deletions

View File

@@ -67,6 +67,9 @@ const { visible } = usePricingDrawer();
</div>
</div>
<UModals />
<NuxtLayout>
<NuxtPage></NuxtPage>
</NuxtLayout>

View File

@@ -140,8 +140,8 @@ const pricingDrawer = usePricingDrawer();
<LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
<div class="flex items-center gap-2 justify-center">
<div><i class="fas fa-plus"></i></div>
<div> Create new project </div>
<div><i class="fas fa-plus text-[.7rem]"></i></div>
<div class="poppins"> New Project </div>
</div>
</LyxUiButton>

View File

@@ -0,0 +1,73 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel'])
const props = defineProps<{
buttonType: string,
message: string,
deleteData: { isAll: boolean, visits: boolean, sessions: boolean, events: boolean, domain: string }
}>();
const isDone = ref<boolean>(false);
async function deleteData() {
try {
if (props.deleteData.isAll) {
} else {
await $fetch('/api/settings/delete_domain', {
method: 'DELETE',
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'Content-Type': 'application/json' } }).value,
body: JSON.stringify({
domain: props.deleteData.domain,
visits: props.deleteData.visits,
sessions: props.deleteData.sessions,
events: props.deleteData.events,
})
})
}
} catch (ex) {
alert('Something went wrong');
console.error(ex);
}
isDone.value = true;
}
</script>
<template>
<UModal :ui="{
strategy: 'override',
overlay: {
background: 'bg-lyx-background/85'
},
background: 'bg-lyx-widget',
ring: 'border-solid border-[1px] border-[#262626]'
}">
<div class="h-full flex flex-col gap-2 p-4">
<div class="font-semibold text-[1.2rem]"> {{ isDone ? "Data Deletion Scheduled" : "Are you sure ?" }}</div>
<div v-if="!isDone">
{{ message }}
</div>
<div v-if="isDone">
Your data deletion request is being processed and will be reflected in your project dashboard within a
few minutes.
</div>
<div class="grow"></div>
<div v-if="!isDone" class="flex justify-end gap-2">
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
<LyxUiButton @click="deleteData()" :type="buttonType"> Confirm </LyxUiButton>
</div>
<div v-if="isDone" class="flex justify-end w-full">
<LyxUiButton type="secondary" @click="emit('success')"> Dismiss </LyxUiButton>
</div>
</div>
</UModal>
</template>

View File

@@ -0,0 +1,154 @@
<script lang="ts" setup>
import DeleteDomainData from '../dialog/DeleteDomainData.vue';
import type { SettingsTemplateEntry } from './Template.vue';
const entries: SettingsTemplateEntry[] = [
{ id: 'delete_dns', title: 'Delete domain data', text: 'Delete data of a specific domain from this project' },
{ id: 'delete_data', title: 'Delete project data', text: 'Delete all data from this project' },
]
const domains = useFetch('/api/settings/domains', {
headers: useComputedHeaders({ useSnapshotDates: false }),
transform: (e) => {
if (!e) return [];
return e.sort((a, b) => {
return a.count - b.count;
}).map(e => {
return { id: e._id, label: `${e._id} - ${e.count} visits` }
})
}
})
const selectedDomain = ref<{ id: string, label: string }>();
const selectedVisits = ref<boolean>(true);
const selectedSessions = ref<boolean>(true);
const selectedEvents = ref<boolean>(true);
const domainCounts = useFetch(() => `/api/settings/domain_counts?domain=${selectedDomain.value?.id}`, {
headers: useComputedHeaders({ useSnapshotDates: false }),
})
const { setToken } = useAccessToken();
const modal = useModal();
function openDeleteDomainDataDialog() {
modal.open(DeleteDomainData, {
preventClose: true,
deleteData: {
isAll: false,
domain: selectedDomain.value?.id as string,
visits: selectedVisits.value,
sessions: selectedSessions.value,
events: selectedEvents.value,
},
buttonType: 'primary',
message: 'This action is irreversable and will wipe all the data from the selected domain.',
onSuccess: () => {
modal.close()
},
onCancel: () => {
modal.close()
},
});
}
function openDeleteAllDomainDataDialog() {
modal.open(DeleteDomainData, {
preventClose: true,
deleteData: {
isAll: true,
domain: '',
visits: false,
sessions: false,
events: false,
},
buttonType: 'danger',
message: 'This action is irreversable and will wipe all the data from the entire project.',
onSuccess: () => {
modal.close()
},
onCancel: () => {
modal.close()
},
});
}
const visitsLabel = computed(() => {
if (domainCounts.pending.value === true) return 'Visits loading...';
if (domainCounts.data.value?.error === true) return 'Visits (too many to compute)';
return 'Visits ' + (domainCounts.data.value?.visits ?? '');
})
const eventsLabel = computed(() => {
if (domainCounts.pending.value === true) return 'Events loading...';
if (domainCounts.data.value?.error === true) return 'Events (too many to compute)';
return 'Events ' + (domainCounts.data.value?.events ?? '');
})
const sessionsLabel = computed(() => {
if (domainCounts.pending.value === true) return 'Sessions loading...';
if (domainCounts.data.value?.error === true) return 'Sessions (too many to compute)';
return 'Sessions ' + (domainCounts.data.value?.sessions ?? '');
})
</script>
<template>
<SettingsTemplate :entries="entries">
<template #delete_dns>
<div class="flex flex-col">
<!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> -->
<USelectMenu placeholder="Select a domain" :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget',
option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter'
}
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
<div class="text-[.9rem] text-lyx-text-dark"> Select data to delete </div>
<div class="flex flex-col gap-1">
<UCheckbox :ui="{ color: 'actionable-visits-color-checkbox' }" v-model="selectedVisits"
:label="visitsLabel" />
<UCheckbox :ui="{ color: 'actionable-sessions-color-checkbox' }" v-model="selectedSessions"
:label="sessionsLabel" />
<UCheckbox :ui="{ color: 'actionable-events-color-checkbox' }" v-model="selectedEvents"
:label="eventsLabel" />
</div>
<LyxUiButton class="mt-2" v-if="selectedVisits || selectedSessions || selectedEvents"
@click="openDeleteDomainDataDialog()" type="outline">
Delete data
</LyxUiButton>
<div class="text-lyx-text-dark">
This action will delete all data from the project creation date.
</div>
</div>
</div>
</template>
<template #delete_data>
<div
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]">
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
visits 0 events 0 sessions)</div>
<div @click="openDeleteAllDomainDataDialog()"
class="text-[#e95b61] poppins font-semibold cursor-pointer hover:text-black hover:bg-red-700 outline rounded-lg w-fit px-8 py-2 outline-[1px] outline-[#532b26] bg-[#291415]">
Delete all data
</div>
</div>
</template>
</SettingsTemplate>
</template>

View File

@@ -16,7 +16,7 @@ export type CustomDialogOptions = {
params?: any,
width?: string,
height?: string,
closable?: boolean
closable?: boolean,
}
function openDialogEx(component: Component, options?: CustomDialogOptions) {

View File

@@ -5,6 +5,7 @@ definePageMeta({ layout: 'dashboard' });
const items = [
{ label: 'General', slot: 'general' },
{ label: 'Data', slot: 'data' },
{ label: 'Members', slot: 'members' },
{ label: 'Billing', slot: 'billing' },
{ label: 'Codes', slot: 'codes' },
@@ -22,6 +23,9 @@ const items = [
<template #general>
<SettingsGeneral :key="refreshKey"></SettingsGeneral>
</template>
<template #data>
<SettingsData :key="refreshKey"></SettingsData>
</template>
<template #members>
<SettingsMembers :key="refreshKey"></SettingsMembers>
</template>

View File

@@ -0,0 +1,104 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { SessionModel } from "@schema/metrics/SessionSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Types } from "mongoose";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const { domain, visits, events, sessions } = await readBody(event);
taskDeleteDomain(project_id, domain, visits, events, sessions);
return { ok: true }
});
async function taskDeleteDomain(project_id: Types.ObjectId, domain: string, deleteVisits: boolean, deleteEvents: boolean, deleteSessions: boolean) {
console.log('Deletation started');
const start = Date.now();
const data = await VisitModel.aggregate([
{
$match: {
project_id,
website: domain
}
},
{
$group: {
_id: "$session",
count: { $sum: 1 }
}
},
{
$lookup: {
from: "events",
let: { sessionId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
{ $match: { project_id } },
{ $project: { _id: 1 } }
],
as: "events"
}
},
{
$lookup: {
from: "sessions",
let: { sessionId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
{ $match: { project_id } },
{ $project: { _id: 1 } }
],
as: "sessions"
}
},
{
$project: {
_id: 1,
count: 1,
"events._id": 1,
"sessions._id": 1
}
}
]) as { _id: string, events: { _id: string }[], sessions: { _id: string }[] }[]
if (deleteSessions === true) {
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString());
const batchSize = 1000;
for (let i = 0; i < sessions.length; i += batchSize) {
const batch = sessions.slice(i, i + batchSize);
await SessionModel.deleteMany({ _id: { $in: batch } });
}
}
if (deleteEvents === true) {
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString());
const batchSize = 1000;
for (let i = 0; i < sessions.length; i += batchSize) {
const batch = sessions.slice(i, i + batchSize);
await EventModel.deleteMany({ _id: { $in: batch } });
}
}
if (deleteVisits === true) {
await VisitModel.deleteMany({ project_id, website: domain })
}
const s = (Date.now() - start) / 1000;
console.log(`Deletation done in ${s.toFixed(2)} seconds`);
}

View File

@@ -0,0 +1,79 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const { domain } = getQuery(event);
try {
const resultData = await VisitModel.aggregate([
{
$match: {
project_id,
website: domain
}
},
{
$group: {
_id: "$session",
count: { $sum: 1 }
}
},
{
$lookup: {
from: "events",
let: { sessionId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
{ $match: { project_id } },
{ $project: { _id: 1 } }
],
as: "events"
}
},
{
$lookup: {
from: "sessions",
let: { sessionId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
{ $match: { project_id } },
{ $project: { _id: 1 } }
],
as: "sessions"
}
},
{
$project: {
_id: 1,
count: 1,
"events._id": 1,
"sessions._id": 1
}
}
], { maxTimeMS: 5000 }) as { _id: string, count: number, events: { _id: string }[], sessions: { _id: string }[] }[]
const visits = resultData.reduce((a, e) => a + e.count, 0);
const sessions = resultData.reduce((a, e) => {
const count = e.sessions.length;
return a + count;
}, 0);
const events = resultData.reduce((a, e) => {
const count = e.events.length;
return a + count;
}, 0);
return { visits, sessions, events, error: false, message: '' };
} catch (ex: any) {
return { error: true, message: ex.message.toString(), visits: -1, sessions: -1, events: -1 }
}
});

View File

@@ -0,0 +1,18 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const result = await VisitModel.aggregate([
{ $match: { project_id } },
{ $group: { _id: "$website", count: { $sum: 1 } } },
]);
return result as { _id: string, count: number }[];
});