refactoring

This commit is contained in:
Emily
2025-03-06 10:55:46 +01:00
parent 63fa3995c5
commit 942d074f99
28 changed files with 253 additions and 83 deletions

View File

@@ -135,7 +135,7 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
type: 'bubble', type: 'bubble',
stack: 'combined', stack: 'combined',
borderColor: ["#fbbf24"] borderColor: ["#fbbf24"]
}, }
], ],
}); });
@@ -367,6 +367,11 @@ const legendClasses = ref<string[]>([
{{ (currentTooltipData as any)[tooltipNameIndex[index]] }} {{ (currentTooltipData as any)[tooltipNameIndex[index]] }}
</div> </div>
</div> </div>
<div class="mt-3 font-normal flex flex-col text-[.9rem] dark:text-lyx-text-dark text-lyx-lightmode-text-dark"
v-if="(currentTooltipData as any).sessions > (currentTooltipData as any).visits">
<div> Unique visitors is greater than visits. </div>
<div> This can indicate bot traffic. </div>
</div>
<!-- <div class="bg-lyx-background-lighter h-[2px] w-full my-2"> </div> --> <!-- <div class="bg-lyx-background-lighter h-[2px] w-full my-2"> </div> -->
</LyxUiCard> </LyxUiCard>
</div> </div>

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
const emit = defineEmits(['success', 'cancel'])
</script>
<template>
<UModal :ui="{
strategy: 'override',
overlay: {
background: 'bg-lyx-background/85'
},
background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
ring: 'border-solid border-[1px] border-[#262626]'
}">
<div class="h-full flex flex-col gap-2 p-4">
<div class="flex flex-col gap-3">
<div class="font-medium">
Are you sure to logout ?
</div>
<div class="flex justify-end gap-2">
<LyxUiButton type="secondary" @click="emit('cancel')">
Cancel
</LyxUiButton>
<LyxUiButton @click="emit('success')" type="danger">
Confirm
</LyxUiButton>
</div>
</div>
</div>
</UModal>
</template>

View File

@@ -107,7 +107,7 @@ async function confirmSnapshot() {
Cancel Cancel
</LyxUiButton> </LyxUiButton>
<LyxUiButton @click="confirmSnapshot()" type="primary" class="w-full text-center" <LyxUiButton @click="confirmSnapshot()" type="primary" class="w-full text-center"
:disabled="snapshotName.length == 0"> :disabled="snapshotName.trim().length == 0">
Confirm Confirm
</LyxUiButton> </LyxUiButton>
</div> </div>

View File

@@ -13,12 +13,42 @@ const props = defineProps<{
}[] }[]
}>(); }>();
function acceptInvite(project_id: string) { async function acceptInvite(project_id: string) {
try {
await $fetch('/api/project/members/accept', {
method: 'POST',
body: JSON.stringify({ project_id }),
headers: useComputedHeaders({
custom: {
'Content-Type': 'application/json'
}
}).value
});
emit('success');
} catch (ex) {
console.error(ex);
alert('Error accepting invite');
emit('cancel');
}
} }
function declineInvite(project_id: string) { async function declineInvite(project_id: string) {
try {
await $fetch('/api/project/members/decline', {
method: 'POST',
body: JSON.stringify({ project_id }),
headers: useComputedHeaders({
custom: {
'Content-Type': 'application/json'
}
}).value
});
emit('success');
} catch (ex) {
console.error(ex);
alert('Error accepting invite');
emit('cancel');
}
} }
</script> </script>
@@ -32,9 +62,9 @@ function declineInvite(project_id: string) {
background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light', background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
ring: 'border-solid border-[1px] border-[#262626]' ring: 'border-solid border-[1px] border-[#262626]'
}"> }">
<div class="h-full flex flex-col gap-8 p-4"> <div class="h-full flex flex-col gap-8 p-6">
<div class="flex flex-col gap-2" v-for="invite of [...invites, ...invites, ...invites]"> <div class="flex flex-col gap-2" v-for="invite of invites">
<div class="dark:text-lyx-text text-lyx-lightmode-text"> <div class="dark:text-lyx-text text-lyx-lightmode-text">
You are invited to join You are invited to join

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { DialogInviteManager } from '#components'; import { DialogConfirmLogout, DialogInviteManager } from '#components';
import CreateSnapshot from '../dialog/CreateSnapshot.vue'; import CreateSnapshot from '../dialog/CreateSnapshot.vue';
export type Entry = { export type Entry = {
@@ -94,12 +94,25 @@ async function generatePDF() {
const { setToken } = useAccessToken(); const { setToken } = useAccessToken();
const router = useRouter(); const router = useRouter();
const { actions } = useProject();
const modal = useModal();
function onLogout() { function onLogout() {
console.log('LOGOUT') modal.open(DialogConfirmLogout, {
setToken(''); onSuccess() {
setLoggedUser(undefined); modal.close();
router.push('/login'); console.log('LOGOUT');
setToken('');
setLoggedUser(undefined);
router.push('/login');
},
onCancel() {
modal.close();
}
})
} }
const { data: maxProjects } = useFetch("/api/user/max_projects", { const { data: maxProjects } = useFetch("/api/user/max_projects", {
@@ -111,8 +124,6 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
}); });
const modal = useModal();
function openPendingInvites() { function openPendingInvites() {
if (!pendingInvites.value) return; if (!pendingInvites.value) return;
if (pendingInvites.value.length == 0) return; if (pendingInvites.value.length == 0) return;
@@ -120,14 +131,16 @@ function openPendingInvites() {
console.log(pendingInvites); console.log(pendingInvites);
modal.open(DialogInviteManager, { modal.open(DialogInviteManager, {
invites: pendingInvites.value.map(e => { invites: pendingInvites.value.map(e => {
return { project_id: e._id, project_name: e.project_name } return { project_id: e.project_id, project_name: e.project_name }
}), }),
onSuccess: () => { onSuccess: () => {
modal.close(); modal.close();
actions.refreshProjectsList();
refreshInvites(); refreshInvites();
}, },
onCancel: () => { onCancel: () => {
modal.close(); modal.close();
actions.refreshProjectsList();
refreshInvites(); refreshInvites();
}, },
}); });

View File

@@ -4,6 +4,8 @@ import type { SettingsTemplateEntry } from './Template.vue';
const { project, actions, projectList, isGuest, projectId } = useProject(); const { project, actions, projectList, isGuest, projectId } = useProject();
const { createErrorAlert, createAlert } = useAlert();
const entries: SettingsTemplateEntry[] = [ const entries: SettingsTemplateEntry[] = [
{ id: 'pname', title: 'Name', text: 'Project name' }, { id: 'pname', title: 'Name', text: 'Project name' },
{ id: 'api', title: 'ApiKeys', text: 'Manage your authorization token' }, { id: 'api', title: 'ApiKeys', text: 'Manage your authorization token' },
@@ -37,7 +39,7 @@ async function createApiKey() {
apiKeys.value.push(res); apiKeys.value.push(res);
newApiKeyName.value = ''; newApiKeyName.value = '';
} catch (ex: any) { } catch (ex: any) {
alert(ex.message); createErrorAlert('Error', ex.message, 10000);
} }
} }
@@ -53,7 +55,7 @@ async function deleteApiKey(api_id: string) {
newApiKeyName.value = ''; newApiKeyName.value = '';
await updateApiKeys(); await updateApiKeys();
} catch (ex: any) { } catch (ex: any) {
alert(ex.message); createErrorAlert('Error', ex.message, 10000);
} }
} }
@@ -116,14 +118,12 @@ async function deleteProject() {
} catch (ex: any) { } catch (ex: any) {
alert(ex.message); createErrorAlert('Error', ex.message);
} }
} }
const { createAlert } = useAlert()
function copyScript() { function copyScript() {
if (!navigator.clipboard) alert('You can\'t copy in HTTP'); if (!navigator.clipboard) alert('You can\'t copy in HTTP');
@@ -172,7 +172,7 @@ function copyProjectId() {
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName" <LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName"
v-model="newApiKeyName"> v-model="newApiKeyName">
</LyxUiInput> </LyxUiInput>
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3" <LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length.trim() < 3"
type="primary"> type="primary">
<i class="far fa-plus"></i> <i class="far fa-plus"></i>
</LyxUiButton> </LyxUiButton>

View File

@@ -116,7 +116,7 @@ const { showDrawer } = useDrawer();
</script> </script>
<template> <template>
<div class="relative"> <div class="relative pb-[6rem]">
<div v-if="invoicesPending || planPending" <div v-if="invoicesPending || planPending"
class="backdrop-blur-[1px] z-[20] mt-20 w-full h-full flex items-center justify-center font-bold"> class="backdrop-blur-[1px] z-[20] mt-20 w-full h-full flex items-center justify-center font-bold">

View File

@@ -71,7 +71,6 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
} }
const allTime: DefaultSnapshot = { const allTime: DefaultSnapshot = {
project_id, project_id,
_id: '___allTime' as any, _id: '___allTime' as any,
@@ -83,8 +82,42 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
} }
const last30Days: DefaultSnapshot = {
project_id,
_id: '___last30days' as any,
name: 'Last 30 days',
from: fns.startOfDay(fns.subDays(Date.now(), 30)),
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
color: '#BC5090',
default: true
}
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek, allTime] const last60Days: DefaultSnapshot = {
project_id,
_id: '___last60days' as any,
name: 'Last 60 days',
from: fns.startOfDay(fns.subDays(Date.now(), 60)),
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
color: '#BC5090',
default: true
}
const last90Days: DefaultSnapshot = {
project_id,
_id: '___last90days' as any,
name: 'Last 90 days',
from: fns.startOfDay(fns.subDays(Date.now(), 90)),
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
color: '#BC5090',
default: true
}
const snapshotList = [
lastDay, today, lastMonth, currentMonth,
lastWeek, currentWeek, allTime,
last30Days, last60Days, last90Days
]
return snapshotList; return snapshotList;

View File

@@ -34,10 +34,19 @@ function createAlert(title: string, text: string, icon: string, ms: number) {
}, 250) }, 250)
} }
function createSuccessAlert(title: string, text: string, ms?: number) {
return createAlert(title, text, 'far fa-circle-check', ms ?? 5000);
}
function createErrorAlert(title: string, text: string, ms?: number) {
return createAlert(title, text, 'far fa-triangle-exclamation', ms ?? 5000);
}
function closeAlert(id: number) { function closeAlert(id: number) {
alerts.value = alerts.value.filter(e => e.id != id); alerts.value = alerts.value.filter(e => e.id != id);
} }
export function useAlert() { export function useAlert() {
return { alerts, createAlert, closeAlert } return { alerts, createAlert, closeAlert, createSuccessAlert, createErrorAlert }
} }

View File

@@ -33,7 +33,10 @@ const guestProjectList = computed(() => {
return guestProjectsRequest.data.value; return guestProjectsRequest.data.value;
}) })
const refreshProjectsList = () => projectsRequest.refresh(); const refreshProjectsList = () => {
projectsRequest.refresh();
guestProjectsRequest.refresh();
}
const activeProjectId = ref<string | undefined>(); const activeProjectId = ref<string | undefined>();

View File

@@ -19,6 +19,8 @@ const router = useRouter();
const { token, setToken } = useAccessToken(); const { token, setToken } = useAccessToken();
const { createErrorAlert } = useAlert();
async function handleOnSuccess(response: any) { async function handleOnSuccess(response: any) {
try { try {
@@ -97,7 +99,7 @@ function goBackToEmailLogin() {
async function signInSelfhosted() { async function signInSelfhosted() {
try { try {
const result = await $fetch(`/api/auth/no_auth`, { const result: any = await $fetch(`/api/auth/no_auth`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value, password: password.value }) body: JSON.stringify({ email: email.value, password: password.value })
@@ -124,7 +126,7 @@ async function signInSelfhosted() {
} }
} catch (ex: any) { } catch (ex: any) {
alert('Error during login.' + ex.message); createErrorAlert('Error', 'Error during login.' + ex.message);
} }
} }
@@ -137,7 +139,7 @@ async function signInWithCredentials() {
body: JSON.stringify({ email: email.value, password: password.value }) body: JSON.stringify({ email: email.value, password: password.value })
}) })
if (result.error) return alert(result.message); if (result.error) return createErrorAlert('Error', result.message);
setToken(result.access_token); setToken(result.access_token);
@@ -156,8 +158,8 @@ async function signInWithCredentials() {
} }
} catch (ex) { } catch (ex: any) {
alert('Something went wrong.'); createErrorAlert('Error', 'Something went wrong.' + ex.message);
} }
} }
@@ -258,7 +260,7 @@ async function signInWithCredentials() {
<div v-if="isNoAuth" @click="loginWithoutAuth" <div v-if="isNoAuth"
class="flex text-[1.3rem] flex-col gap-4 items-center px-8 py-3 relative z-[2]"> class="flex text-[1.3rem] flex-col gap-4 items-center px-8 py-3 relative z-[2]">
<div class="flex flex-col gap-4 z-[100] w-[20vw] min-w-[20rem]"> <div class="flex flex-col gap-4 z-[100] w-[20vw] min-w-[20rem]">
<LyxUiInput class="px-3 py-2" placeholder="Email" v-model="email"></LyxUiInput> <LyxUiInput class="px-3 py-2" placeholder="Email" v-model="email"></LyxUiInput>

View File

@@ -22,6 +22,8 @@ const showAddMember = ref<boolean>(false);
const addMemberEmail = ref<string>(""); const addMemberEmail = ref<string>("");
const { createErrorAlert } = useAlert();
async function kickMember(email: string) { async function kickMember(email: string) {
const sure = confirm('Are you sure to kick ' + email + ' ?'); const sure = confirm('Are you sure to kick ' + email + ' ?');
if (!sure) return; if (!sure) return;
@@ -34,7 +36,7 @@ async function kickMember(email: string) {
}), }),
body: JSON.stringify({ email }), body: JSON.stringify({ email }),
onResponseError({ request, response, options }) { onResponseError({ request, response, options }) {
alert(response.statusText); createErrorAlert('Error', response.statusText);
} }
}); });
@@ -58,7 +60,7 @@ async function addMember() {
}), }),
body: JSON.stringify({ email: addMemberEmail.value }), body: JSON.stringify({ email: addMemberEmail.value }),
onResponseError({ request, response, options }) { onResponseError({ request, response, options }) {
alert(response.statusText); createErrorAlert('Error', response.statusText);
} }
}); });
@@ -101,12 +103,25 @@ function permissionToString(permission: TPermission) {
} }
return result.join(''); return result.join('');
} }
async function leaveProject() {
try {
await $fetch('/api/project/members/leave', {
headers: useComputedHeaders({}).value
});
location.reload();
} catch (ex: any) {
alert(ex.message);
}
}
</script> </script>
<template> <template>
<div class="p-6 pt-10"> <div class="p-6 pt-10">
<div class="flex flex-col gap-8"> <div v-if="!isGuest" class="flex flex-col gap-8">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
@@ -144,7 +159,9 @@ function permissionToString(permission: TPermission) {
<div v-if="e.row.role !== 'OWNER' && !isGuest"> <div v-if="e.row.role !== 'OWNER' && !isGuest">
<LyxUiButton class="!px-2" type="secondary" <LyxUiButton class="!px-2" type="secondary"
@click="openPermissionManagerDialog(e.row.id.toString())"> @click="openPermissionManagerDialog(e.row.id.toString())">
<i class="far fa-gear"></i> <UTooltip text="Manage permissions">
<i class="far fa-gear"></i>
</UTooltip>
</LyxUiButton> </LyxUiButton>
</div> </div>
@@ -181,6 +198,12 @@ function permissionToString(permission: TPermission) {
</div> </div>
<div v-if="isGuest" class="flex flex-col gap-8 mt-[10vh]">
<div class="flex flex-col gap-4 items-center">
<div class="text-[1.2rem]"> Leave this project </div>
<LyxUiButton @click="leaveProject()" type="primary"> Leave </LyxUiButton>
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -23,7 +23,7 @@ onMounted(() => {
async function createProject() { async function createProject() {
if (projectName.value.length < 2) return; if (projectName.value.trim().length < 2) return;
Lit.event('create_project'); Lit.event('create_project');
@@ -34,7 +34,7 @@ async function createProject() {
await $fetch('/api/project/create', { await $fetch('/api/project/create', {
method: 'POST', method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }), ...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ name: projectName.value }) body: JSON.stringify({ name: projectName.value.trim() })
}); });
await actions.refreshProjectsList(); await actions.refreshProjectsList();
@@ -89,7 +89,7 @@ async function createProject() {
<div> <div>
<LyxUiButton type="primary" @click="createProject()" :disabled="projectName.length < 2"> <LyxUiButton type="primary" @click="createProject()" :disabled="projectName.trim().length < 2">
Create Create
</LyxUiButton> </LyxUiButton>

View File

@@ -15,7 +15,7 @@ const items = [
</script> </script>
<template> <template>
<div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars !pb-[10rem]"> <div class="lg:px-10 h-full lg:py-8 overflow-hidden hide-scrollbars">
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div> <div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>

View File

@@ -13,23 +13,28 @@ export default defineEventHandler(async event => {
const body = await readBody(event); const body = await readBody(event);
if (body.name.length == 0) return setResponseStatus(event, 400, 'name is required'); const data = await getRequestData(event, [], ['OWNER']);
if (body.name.length < 3) return setResponseStatus(event, 400, 'name too short');
if (body.name.length > 32) return setResponseStatus(event, 400, 'name too long');
const data = await getRequestDataOld(event, { allowGuests: false, allowLitlyx: false, });
if (!data) return; if (!data) return;
if (!body.name) return setResponseStatus(event, 400, 'body is required');
if (body.name.trim().length == 0) return setResponseStatus(event, 400, 'name is required');
if (body.name.trim().length < 3) return setResponseStatus(event, 400, 'name too short');
if (body.name.trim().length > 32) return setResponseStatus(event, 400, 'name too long');
const { project_id } = data; const { project_id } = data;
const sameName = await ApiSettingsModel.exists({ project_id, apiName: body.name.trim() });
if (sameName) return setResponseStatus(event, 400, 'An api key with the same name exists');
const key = generateApiKey(); const key = generateApiKey();
const keyNumbers = await ApiSettingsModel.countDocuments({ project_id }); const keyNumbers = await ApiSettingsModel.countDocuments({ project_id });
if (keyNumbers >= 5) return setResponseStatus(event, 400, 'Api key limit reached'); if (keyNumbers >= 5) return setResponseStatus(event, 400, 'Api key limit reached');
const newApiSettings = await ApiSettingsModel.create({ project_id, apiKey: key, apiName: body.name, created_at: Date.now(), usage: 0 }); const newApiSettings = await ApiSettingsModel.create({ project_id, apiKey: key, apiName: body.name.trim(), created_at: Date.now(), usage: 0 });
return newApiSettings.toJSON(); return newApiSettings.toJSON();

View File

@@ -1,16 +1,18 @@
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 } = data; const { project } = data;
const { name } = await readBody(event); const { name } = await readBody(event);
if (name.length == 0) return setResponseStatus(event, 400, 'name is required'); if (name.trim()) return setResponseStatus(event, 400, 'name is required');
if (name.trim().length < 2) return setResponseStatus(event, 400, 'name too short');
if (name.trim().length > 32) return setResponseStatus(event, 400, 'name too long');
project.name = name; project.name = name.trim();
await project.save(); await project.save();
return { ok: true }; return { ok: true };

View File

@@ -8,7 +8,7 @@ export default defineEventHandler(async event => {
const body = await readBody(event); const body = await readBody(event);
const newProjectName = body.name; const newProjectName = body.name.trim();
if (!newProjectName) return setResponseStatus(event, 400, 'ProjectName too short'); if (!newProjectName) return setResponseStatus(event, 400, 'ProjectName too short');
if (newProjectName.length < 2) return setResponseStatus(event, 400, 'ProjectName too short'); if (newProjectName.length < 2) return setResponseStatus(event, 400, 'ProjectName too short');

View File

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

View File

@@ -10,6 +10,8 @@ export default defineEventHandler(async event => {
const { project_id } = body; const { project_id } = body;
if (!project_id) return setResponseStatus(event, 400, 'project_id is required'); if (!project_id) return setResponseStatus(event, 400, 'project_id is required');
console.log({ project_id, user_id: data.user.id });
const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id }); const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id });
if (!member) return setResponseStatus(event, 400, 'member not found'); if (!member) return setResponseStatus(event, 400, 'member not found');

View File

@@ -9,12 +9,16 @@ export default defineEventHandler(async event => {
const data = await getRequestData(event, [], ['OWNER']); const data = await getRequestData(event, [], ['OWNER']);
if (!data) return; if (!data) return;
const { project_id, project } = data; const { project_id, project, user } = 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 && targetUser._id.toString() === project.owner.toString()) {
return setResponseStatus(event, 400, 'You cannot invite yourself');
}
const link = `http://127.0.0.1:3000/accept_invite?project_id=${project_id.toString()}`; const link = `http://127.0.0.1:3000/accept_invite?project_id=${project_id.toString()}`;

View File

@@ -4,7 +4,7 @@ import { TeamMemberModel } from "@schema/TeamMemberSchema";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const data = await getRequestData(event, []); const data = await getRequestData(event, [], []);
if (!data) return; if (!data) return;
const { project_id, user } = data; const { project_id, user } = data;

View File

@@ -14,7 +14,7 @@ export type MemberWithPermissions = {
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const data = await getRequestData(event); const data = await getRequestData(event, [], ['OWNER']);
if (!data) return; if (!data) return;
const { project_id, project, user } = data; const { project_id, project, user } = data;

View File

@@ -11,7 +11,7 @@ export default defineEventHandler(async event => {
const { name: newSnapshotName, from, to, color: snapshotColor } = body; const { name: newSnapshotName, from, to, color: snapshotColor } = body;
if (!newSnapshotName) return setResponseStatus(event, 400, 'SnapshotName too short'); if (!newSnapshotName) return setResponseStatus(event, 400, 'SnapshotName too short');
if (newSnapshotName.length == 0) return setResponseStatus(event, 400, 'SnapshotName too short'); if (newSnapshotName.trim().length == 0) return setResponseStatus(event, 400, 'SnapshotName too short');
if (!from) return setResponseStatus(event, 400, 'from is required'); if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required'); if (!to) return setResponseStatus(event, 400, 'to is required');
@@ -26,7 +26,7 @@ export default defineEventHandler(async event => {
const newSnapshot = await ProjectSnapshotModel.create({ const newSnapshot = await ProjectSnapshotModel.create({
name: newSnapshotName, name: newSnapshotName.trim(),
from: new Date(from), from: new Date(from),
to: new Date(to), to: new Date(to),
color: snapshotColor, color: snapshotColor,

View File

@@ -12,17 +12,18 @@ export default defineEventHandler(async event => {
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}:${domain}`; const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}:${domain}`;
const cacheExp = 20; const cacheExp = 20;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { // return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const timelineData = await executeAdvancedTimelineAggregation({ const timelineData = await executeAdvancedTimelineAggregation({
projectId: project_id, projectId: project_id,
model: VisitModel, model: VisitModel,
from, to, slice, timeOffset, domain from, to, slice, timeOffset, domain,
}); debug: true
return timelineData;
}); });
return timelineData;
// });

View File

@@ -80,10 +80,11 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
range: { range: {
step: 1, step: 1,
unit: granularity, unit: granularity,
bounds: [ bounds: 'full'
new Date(new Date(options.from).getTime() - (timeOffset * 1000 * 60)), // [
new Date(new Date(options.to).getTime() - (timeOffset * 1000 * 60) + 1), // new Date(new Date(options.from).getTime() - (timeOffset * 1000 * 60)),
] // new Date(new Date(options.to).getTime() - (timeOffset * 1000 * 60) + 1),
// ]
} }
} }
}, },

View File

@@ -90,6 +90,8 @@ app.post('/send/welcome', express.json(), async (req, res) => {
app.post('/send/purchase', express.json(), async (req, res) => { app.post('/send/purchase', express.json(), async (req, res) => {
try { try {
console.log('PURCHASE EMAIL DISABLED')
return;
const { target, projectName } = req.body; const { target, projectName } = req.body;
const ok = await EmailService.sendPurchaseEmail(target, projectName); const ok = await EmailService.sendPurchaseEmail(target, projectName);
res.json({ ok }); res.json({ ok });

View File

@@ -1,6 +1,6 @@
import { model, Schema, Types } from 'mongoose'; import { model, Schema, Types } from 'mongoose';
export type TeamMemberRole = 'ADMIN' | 'GUEST'; export type TeamMemberRole = 'OWNER' | 'GUEST' | 'MANAGER';
export type TPermission = { export type TPermission = {

View File

@@ -53,17 +53,16 @@ class DateService {
} }
canUseSliceFromDays(days: number, slice: Slice): [false, string] | [true, number] { canUseSliceFromDays(days: number, slice: Slice): [false, string] | [true, number] {
// HOUR - 3 DAYS - 72 SAMPLES
// 3 Days
if (slice === 'hour' && (days > 3)) return [false, 'Date gap too big for this slice']; if (slice === 'hour' && (days > 3)) return [false, 'Date gap too big for this slice'];
// 3 Weeks // DAY - 2 MONTHS - 62 SAMPLES
if (slice === 'day' && (days > 31)) return [false, 'Date gap too big for this slice']; if (slice === 'day' && (days > 31 * 2)) return [false, 'Date gap too big for this slice'];
// 3 Years // MONTH - 4 YEARS - 60 SAMPLES
if (slice === 'month' && (days > 365 * 3)) return [false, 'Date gap too big for this slice']; if (slice === 'month' && (days > 365 * 4)) return [false, 'Date gap too big for this slice'];
// 2 days // DAY - 2 DAYS - 2 SAMPLES
if (slice === 'day' && (days < 2)) return [false, 'Date gap too small for this slice']; if (slice === 'day' && (days < 2)) return [false, 'Date gap too small for this slice'];
// 2 month // MONTH - 2 MONTHS - 2 SAMPLES
if (slice === 'month' && (days < 31 * 2)) return [false, 'Date gap too small for this slice']; if (slice === 'month' && (days < 31 * 2)) return [false, 'Date gap too small for this slice'];
return [true, days] return [true, days]