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',
stack: 'combined',
borderColor: ["#fbbf24"]
},
}
],
});
@@ -367,6 +367,11 @@ const legendClasses = ref<string[]>([
{{ (currentTooltipData as any)[tooltipNameIndex[index]] }}
</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> -->
</LyxUiCard>
</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
</LyxUiButton>
<LyxUiButton @click="confirmSnapshot()" type="primary" class="w-full text-center"
:disabled="snapshotName.length == 0">
:disabled="snapshotName.trim().length == 0">
Confirm
</LyxUiButton>
</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>
@@ -32,9 +62,9 @@ function declineInvite(project_id: string) {
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="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">
You are invited to join

View File

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

View File

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

View File

@@ -116,7 +116,7 @@ const { showDrawer } = useDrawer();
</script>
<template>
<div class="relative">
<div class="relative pb-[6rem]">
<div v-if="invoicesPending || planPending"
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 = {
project_id,
_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;

View File

@@ -34,10 +34,19 @@ function createAlert(title: string, text: string, icon: string, ms: number) {
}, 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) {
alerts.value = alerts.value.filter(e => e.id != id);
}
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;
})
const refreshProjectsList = () => projectsRequest.refresh();
const refreshProjectsList = () => {
projectsRequest.refresh();
guestProjectsRequest.refresh();
}
const activeProjectId = ref<string | undefined>();

View File

@@ -19,6 +19,8 @@ const router = useRouter();
const { token, setToken } = useAccessToken();
const { createErrorAlert } = useAlert();
async function handleOnSuccess(response: any) {
try {
@@ -97,7 +99,7 @@ function goBackToEmailLogin() {
async function signInSelfhosted() {
try {
const result = await $fetch(`/api/auth/no_auth`, {
const result: any = await $fetch(`/api/auth/no_auth`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value, password: password.value })
@@ -124,7 +126,7 @@ async function signInSelfhosted() {
}
} 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 })
})
if (result.error) return alert(result.message);
if (result.error) return createErrorAlert('Error', result.message);
setToken(result.access_token);
@@ -156,8 +158,8 @@ async function signInWithCredentials() {
}
} catch (ex) {
alert('Something went wrong.');
} catch (ex: any) {
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]">
<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>

View File

@@ -22,6 +22,8 @@ const showAddMember = ref<boolean>(false);
const addMemberEmail = ref<string>("");
const { createErrorAlert } = useAlert();
async function kickMember(email: string) {
const sure = confirm('Are you sure to kick ' + email + ' ?');
if (!sure) return;
@@ -34,7 +36,7 @@ async function kickMember(email: string) {
}),
body: JSON.stringify({ email }),
onResponseError({ request, response, options }) {
alert(response.statusText);
createErrorAlert('Error', response.statusText);
}
});
@@ -58,7 +60,7 @@ async function addMember() {
}),
body: JSON.stringify({ email: addMemberEmail.value }),
onResponseError({ request, response, options }) {
alert(response.statusText);
createErrorAlert('Error', response.statusText);
}
});
@@ -101,12 +103,25 @@ function permissionToString(permission: TPermission) {
}
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>
<template>
<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 gap-4 items-center">
@@ -144,7 +159,9 @@ function permissionToString(permission: TPermission) {
<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>
<UTooltip text="Manage permissions">
<i class="far fa-gear"></i>
</UTooltip>
</LyxUiButton>
</div>
@@ -181,6 +198,12 @@ function permissionToString(permission: TPermission) {
</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>
</template>

View File

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

View File

@@ -15,7 +15,7 @@ const items = [
</script>
<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>

View File

@@ -13,23 +13,28 @@ export default defineEventHandler(async event => {
const body = await readBody(event);
if (body.name.length == 0) return setResponseStatus(event, 400, 'name is required');
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, });
const data = await getRequestData(event, [], ['OWNER']);
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 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 keyNumbers = await ApiSettingsModel.countDocuments({ project_id });
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();

View File

@@ -1,16 +1,18 @@
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;
const { project } = data;
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();
return { ok: true };

View File

@@ -8,7 +8,7 @@ export default defineEventHandler(async 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.length < 2) return setResponseStatus(event, 400, 'ProjectName too short');

View File

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

View File

@@ -10,6 +10,8 @@ export default defineEventHandler(async event => {
const { project_id } = body;
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 });
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']);
if (!data) return;
const { project_id, project } = data;
const { project_id, project, user } = data;
const { email } = await readBody(event);
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()}`;

View File

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

View File

@@ -14,7 +14,7 @@ export type MemberWithPermissions = {
export default defineEventHandler(async event => {
const data = await getRequestData(event);
const data = await getRequestData(event, [], ['OWNER']);
if (!data) return;
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;
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 (!to) return setResponseStatus(event, 400, 'to is required');
@@ -26,7 +26,7 @@ export default defineEventHandler(async event => {
const newSnapshot = await ProjectSnapshotModel.create({
name: newSnapshotName,
name: newSnapshotName.trim(),
from: new Date(from),
to: new Date(to),
color: snapshotColor,

View File

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

View File

@@ -80,10 +80,11 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
range: {
step: 1,
unit: granularity,
bounds: [
new Date(new Date(options.from).getTime() - (timeOffset * 1000 * 60)),
new Date(new Date(options.to).getTime() - (timeOffset * 1000 * 60) + 1),
]
bounds: 'full'
// [
// 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) => {
try {
console.log('PURCHASE EMAIL DISABLED')
return;
const { target, projectName } = req.body;
const ok = await EmailService.sendPurchaseEmail(target, projectName);
res.json({ ok });

View File

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

View File

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