mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
new selfhosted version
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const { AUTH_JWT_SECRET } = useRuntimeConfig();
|
||||
|
||||
function createJwt(data: Object, expiresIn?: string) {
|
||||
return jwt.sign(data, AUTH_JWT_SECRET, { expiresIn: expiresIn ?? '30d' });
|
||||
}
|
||||
|
||||
function readJwt(data: string) {
|
||||
try {
|
||||
return jwt.verify(data, AUTH_JWT_SECRET);
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type TUserJwt = {
|
||||
email: string,
|
||||
name: string
|
||||
}
|
||||
|
||||
export function readUserJwt(raw: string) {
|
||||
const data = readJwt(raw);
|
||||
return data as TUserJwt | undefined;
|
||||
}
|
||||
|
||||
export function createUserJwt(data: TUserJwt) {
|
||||
return createJwt(data);
|
||||
}
|
||||
|
||||
export function createRegisterJwt(email: string, hashedPassword: string) {
|
||||
return createJwt({ email, password: hashedPassword }, '7d');
|
||||
}
|
||||
|
||||
export function readRegisterJwt(raw: string) {
|
||||
const data = readJwt(raw);
|
||||
return data as { email: string, password: string } | undefined;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { AuthContext } from "./middleware/01-authorization";
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { hasAccessToProject } from "./utils/hasAccessToProject";
|
||||
|
||||
export async function getUserProjectFromId(project_id: string, user: AuthContext | undefined, allowGuest: boolean = true) {
|
||||
if (!project_id) return;
|
||||
|
||||
if (project_id === "6643cd08a1854e3b81722ab5") {
|
||||
return await ProjectModel.findOne({ _id: project_id });
|
||||
}
|
||||
|
||||
if (!user || !user.logged) return;
|
||||
|
||||
const project = await ProjectModel.findById(project_id);
|
||||
if (!project) return;
|
||||
|
||||
const [hasAccess, role] = await hasAccessToProject(user.id, project_id, project);
|
||||
if (!hasAccess) return;
|
||||
|
||||
if (role === 'GUEST' && !allowGuest) return false;
|
||||
|
||||
return project;
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
import winston from 'winston';
|
||||
|
||||
const { combine, timestamp, json, errors } = winston.format;
|
||||
|
||||
|
||||
const timestampFormat = () => { return new Date().toLocaleString('it-IT', { timeZone: 'Europe/Rome' }); }
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
format: combine(
|
||||
errors({ stack: true }),
|
||||
timestamp({
|
||||
format: timestampFormat
|
||||
}),
|
||||
json()
|
||||
),
|
||||
exceptionHandlers: [
|
||||
new winston.transports.File({ filename: 'winston-logs.ndjson' }),
|
||||
new winston.transports.File({ filename: 'winston-exceptions.ndjson' }),
|
||||
],
|
||||
rejectionHandlers: [
|
||||
new winston.transports.File({ filename: 'winston-logs.ndjson' }),
|
||||
new winston.transports.File({ filename: 'winston-rejections.ndjson' }),
|
||||
],
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
level: 'debug',
|
||||
format: combine(
|
||||
winston.format.colorize({ all: true }),
|
||||
errors({ stack: true }),
|
||||
timestamp({ format: timestampFormat }),
|
||||
winston.format.printf((info) => {
|
||||
if (info instanceof Error) {
|
||||
return `${info.timestamp} [${info.level}]: ${info.message}\n${info.stack}`;
|
||||
} else {
|
||||
return `${info.timestamp} [${info.level}]: ${info.message}`;
|
||||
}
|
||||
})
|
||||
),
|
||||
}),
|
||||
new winston.transports.File({ filename: 'winston-logs.ndjson' }),
|
||||
new winston.transports.File({
|
||||
level: 'debug',
|
||||
filename: 'winston-debug.ndjson'
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
import type OpenAI from 'openai'
|
||||
|
||||
|
||||
export type AIPlugin_TTool<T extends string> = (OpenAI.Chat.Completions.ChatCompletionTool & { function: { name: T } });
|
||||
|
||||
export type AIPlugin_TFunction = (...args: any[]) => any;
|
||||
|
||||
type AIPlugin_Constructor<Items extends string[]> = {
|
||||
[Key in Items[number]]: {
|
||||
tool: AIPlugin_TTool<Key>,
|
||||
handler: AIPlugin_TFunction
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AIPlugin<Items extends string[] = []> {
|
||||
constructor(public functions: AIPlugin_Constructor<Items>) { }
|
||||
|
||||
getTools() {
|
||||
const keys = Object.keys(this.functions) as Items;
|
||||
return keys.map((key: Items[number]) => { return this.functions[key].tool });
|
||||
}
|
||||
getHandlers() {
|
||||
const keys = Object.keys(this.functions) as Items;
|
||||
const result: Record<string, any> = {};
|
||||
keys.forEach((key: Items[number]) => {
|
||||
result[key] = this.functions[key].handler;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
|
||||
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
|
||||
import { AIPlugin } from "../Plugin";
|
||||
import { MAX_LOG_LIMIT_PERCENT } from "@data/broker/Limits";
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import StripeService from "~/server/services/StripeService";
|
||||
import { InvoiceData } from "~/server/api/pay/invoices";
|
||||
|
||||
export class AiBilling extends AIPlugin<[
|
||||
'getBillingInfo',
|
||||
'getLimits',
|
||||
'getInvoices'
|
||||
]> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
|
||||
'getInvoices': {
|
||||
handler: async (data: { project_id: string }) => {
|
||||
|
||||
const project = await ProjectModel.findOne({ _id: data.project_id });
|
||||
if (!project) return { error: 'Project not found' };
|
||||
const invoices = await StripeService.getInvoices(project.customer_id);
|
||||
if (!invoices) return [];
|
||||
|
||||
return invoices?.data.map(e => {
|
||||
const result: InvoiceData = {
|
||||
link: e.invoice_pdf || '',
|
||||
id: e.number || '',
|
||||
date: e.created * 1000,
|
||||
status: e.status || 'NO_STATUS',
|
||||
cost: e.amount_due
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
tool: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getInvoices',
|
||||
description: 'Gets the invoices of the user project',
|
||||
parameters: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'getBillingInfo': {
|
||||
handler: async (data: { project_id: string }) => {
|
||||
|
||||
const project = await ProjectModel.findOne({ _id: data.project_id });
|
||||
if (!project) return { error: 'Project not found' };
|
||||
|
||||
if (project.subscription_id === 'onetime') {
|
||||
|
||||
const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
|
||||
if (!projectLimits) return { error: 'Limits not found' }
|
||||
|
||||
const result = {
|
||||
premium: project.premium,
|
||||
premium_type: project.premium_type,
|
||||
billing_start_at: projectLimits.billing_start_at,
|
||||
billing_expire_at: projectLimits.billing_expire_at,
|
||||
limit: projectLimits.limit,
|
||||
count: projectLimits.events + projectLimits.visits,
|
||||
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : ('One time payment')
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const subscription = await StripeService.getSubscription(project.subscription_id);
|
||||
|
||||
const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
|
||||
if (!projectLimits) return { error: 'Limits not found' }
|
||||
|
||||
|
||||
const result = {
|
||||
premium: project.premium,
|
||||
premium_type: project.premium_type,
|
||||
billing_start_at: projectLimits.billing_start_at,
|
||||
billing_expire_at: projectLimits.billing_expire_at,
|
||||
limit: projectLimits.limit,
|
||||
count: projectLimits.events + projectLimits.visits,
|
||||
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : (subscription?.status ?? '?')
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
tool: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getBillingInfo',
|
||||
description: 'Gets the informations about the billing of the user project, limits, count, subscription_status, is premium, premium type, billing start at, billing expire at',
|
||||
parameters: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'getLimits': {
|
||||
handler: async (data: { project_id: string }) => {
|
||||
const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
|
||||
if (!projectLimits) return { error: 'Project limits not found' };
|
||||
const TOTAL_COUNT = projectLimits.events + projectLimits.visits;
|
||||
const COUNT_LIMIT = projectLimits.limit;
|
||||
return {
|
||||
total: TOTAL_COUNT,
|
||||
limit: COUNT_LIMIT,
|
||||
limited: TOTAL_COUNT > COUNT_LIMIT * MAX_LOG_LIMIT_PERCENT,
|
||||
percent: Math.round(100 / COUNT_LIMIT * TOTAL_COUNT)
|
||||
}
|
||||
},
|
||||
tool: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getLimits',
|
||||
description: 'Gets the informations about the limits of the user project',
|
||||
parameters: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const AiBillingInstance = new AiBilling();
|
||||
@@ -1,67 +0,0 @@
|
||||
|
||||
import { AIPlugin } from "../Plugin";
|
||||
|
||||
export class AiComposableChart extends AIPlugin<['createComposableChart']> {
|
||||
constructor() {
|
||||
super({
|
||||
'createComposableChart': {
|
||||
handler: (data: { labels: string, points: number[] }) => {
|
||||
return { ok: true };
|
||||
},
|
||||
tool: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'createComposableChart',
|
||||
description: 'Creates a chart based on the provided datasets',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
labels: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Labels for each data point in the chart'
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Title of the chart to let user understand what is displaying, not include dates'
|
||||
},
|
||||
datasets: {
|
||||
type: 'array',
|
||||
description: 'List of datasets',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
chartType: {
|
||||
type: 'string',
|
||||
enum: ['line', 'bar'],
|
||||
description: 'The type of chart to display the dataset, either "line" or "bar"'
|
||||
},
|
||||
points: {
|
||||
type: 'array',
|
||||
items: { type: 'number' },
|
||||
description: 'Numerical values for each data point in the chart'
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'Color used to represent the dataset in format "#RRGGBB"'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the dataset'
|
||||
}
|
||||
},
|
||||
required: ['points', 'color', 'chartType', 'name'],
|
||||
description: 'Data points and style information for the dataset'
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['labels', 'datasets', 'title']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const AiComposableChartInstance = new AiComposableChart();
|
||||
@@ -1,85 +0,0 @@
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { executeTimelineAggregation } from "~/server/services/TimelineService";
|
||||
import { Types } from "mongoose";
|
||||
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
|
||||
|
||||
|
||||
const getEventsCountTool: AIPlugin_TTool<'getEventsCount'> = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getEventsCount',
|
||||
description: 'Gets the number of events received on a date range, can also specify the event name and the metadata associated',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
name: { type: 'string', description: 'Name of the events to get' },
|
||||
metadata: { type: 'object', description: 'Metadata of events to get' },
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getEventsTimelineTool: AIPlugin_TTool<'getEventsTimeline'> = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getEventsTimeline',
|
||||
description: 'Gets an array of date and count for events received on a date range. Should be used to create charts.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
name: { type: 'string', description: 'Name of the events to get' },
|
||||
metadata: { type: 'object', description: 'Metadata of events to get' },
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AiEvents extends AIPlugin<['getEventsCount', 'getEventsTimeline']> {
|
||||
|
||||
constructor() {
|
||||
|
||||
super({
|
||||
'getEventsCount': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, name?: string, metadata?: string }) => {
|
||||
const query: any = {
|
||||
project_id: data.project_id,
|
||||
created_at: {
|
||||
$gt: new Date(data.from),
|
||||
$lt: new Date(data.to),
|
||||
}
|
||||
}
|
||||
if (data.metadata) query.metadata = data.metadata;
|
||||
if (data.name) query.name = data.name;
|
||||
const result = await EventModel.countDocuments(query);
|
||||
return { count: result };
|
||||
},
|
||||
tool: getEventsCountTool
|
||||
},
|
||||
'getEventsTimeline': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, time_offset: number, name?: string, metadata?: string }) => {
|
||||
|
||||
const timelineData = await executeTimelineAggregation({
|
||||
projectId: new Types.ObjectId(data.project_id),
|
||||
model: EventModel,
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
slice: 'day',
|
||||
timeOffset: data.time_offset
|
||||
});
|
||||
return { data: timelineData };
|
||||
},
|
||||
tool: getEventsTimelineTool
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const AiEventsInstance = new AiEvents();
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { executeTimelineAggregation } from "~/server/services/TimelineService";
|
||||
import { Types } from "mongoose";
|
||||
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
|
||||
const getSessionsCountsTool: AIPlugin_TTool<'getSessionsCount'> = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getSessionsCount',
|
||||
description: 'Gets the number of sessions (unique visitors) received on a date range',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
min_duration: { type: 'number', description: 'Minimum duration of the session' },
|
||||
max_duration: { type: 'number', description: 'Maximum duration of the session' },
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getSessionsTimelineTool: AIPlugin_TTool<'getSessionsTimeline'> = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getSessionsTimeline',
|
||||
description: 'Gets an array of date and count for events received on a date range. Should be used to create charts.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AiSessions extends AIPlugin<['getSessionsCount', 'getSessionsTimeline']> {
|
||||
|
||||
constructor() {
|
||||
|
||||
super({
|
||||
'getSessionsCount': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, min_duration?: number, max_duration?: number }) => {
|
||||
|
||||
const query: any = {
|
||||
project_id: data.project_id,
|
||||
created_at: {
|
||||
$gt: new Date(data.from),
|
||||
$lt: new Date(data.to),
|
||||
},
|
||||
duration: {
|
||||
$gte: data.min_duration || 0,
|
||||
$lte: data.max_duration || 999_999_999,
|
||||
}
|
||||
}
|
||||
|
||||
const result = await VisitModel.countDocuments(query);
|
||||
return { count: result };
|
||||
},
|
||||
tool: getSessionsCountsTool
|
||||
},
|
||||
'getSessionsTimeline': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, time_offset: number, website?: string, page?: string }) => {
|
||||
|
||||
const timelineData = await executeTimelineAggregation({
|
||||
projectId: new Types.ObjectId(data.project_id),
|
||||
model: SessionModel,
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
slice: 'day',
|
||||
timeOffset: data.time_offset
|
||||
});
|
||||
return { data: timelineData };
|
||||
},
|
||||
tool: getSessionsTimelineTool
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const AiSessionsInstance = new AiSessions();
|
||||
@@ -1,78 +0,0 @@
|
||||
|
||||
import { AIPlugin } from "../Plugin";
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { ProjectSnapshotModel } from "@schema/project/ProjectSnapshot";
|
||||
|
||||
export class AiSnapshot extends AIPlugin<[
|
||||
'getSnapshots',
|
||||
'createSnapshot',
|
||||
]> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
|
||||
'getSnapshots': {
|
||||
handler: async (data: { project_id: string }) => {
|
||||
const snapshots = await ProjectSnapshotModel.find({ project_id: data.project_id });
|
||||
return snapshots.map(e => e.toJSON());
|
||||
},
|
||||
tool: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getSnapshots',
|
||||
description: 'Gets the snapshots list',
|
||||
parameters: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'createSnapshot': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, color: string, name: string }) => {
|
||||
|
||||
if (!data.name) return { error: 'SnapshotName too short' }
|
||||
if (data.name.length == 0) return { error: 'SnapshotName too short' }
|
||||
|
||||
if (!data.from) return { error: 'from is required' }
|
||||
if (!data.to) return { error: 'to is required' }
|
||||
if (!data.color) return { error: 'color is required' }
|
||||
|
||||
const project = await ProjectModel.findById(data.project_id);
|
||||
if (!project) return { error: 'Project not found' }
|
||||
|
||||
|
||||
const newSnapshot = await ProjectSnapshotModel.create({
|
||||
name: data.name,
|
||||
from: new Date(data.from),
|
||||
to: new Date(data.to),
|
||||
color: data.color,
|
||||
project_id: data.project_id
|
||||
});
|
||||
|
||||
return newSnapshot.id;
|
||||
|
||||
|
||||
},
|
||||
tool: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'createSnapshot',
|
||||
description: 'Create a snapshot',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
color: { type: 'string', description: 'Color of the snapshot in HEX' },
|
||||
name: { type: 'string', description: 'Name of the snapshot' }
|
||||
},
|
||||
required: ['from', 'to', 'color', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const AiSnapshotInstance = new AiSnapshot();
|
||||
@@ -1,93 +0,0 @@
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { executeTimelineAggregation } from "~/server/services/TimelineService";
|
||||
import { Types } from "mongoose";
|
||||
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
|
||||
|
||||
const getVisitsCountsTool: AIPlugin_TTool<'getVisitsCount'> = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getVisitsCount',
|
||||
description: 'Gets the number of visits received on a date range',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
website: { type: 'string', description: 'The website of the visits' },
|
||||
page: { type: 'string', description: 'The page of the visit' },
|
||||
domain: { type: 'string', description: 'Used only to filter a specific domain' }
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getVisitsTimelineTool: AIPlugin_TTool<'getVisitsTimeline'> = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getVisitsTimeline',
|
||||
description: 'Gets an array of date and count for events received on a date range. Should be used to create charts.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'ISO string of start date' },
|
||||
to: { type: 'string', description: 'ISO string of end date' },
|
||||
website: { type: 'string', description: 'The website of the visits' },
|
||||
page: { type: 'string', description: 'The page of the visit' },
|
||||
domain: { type: 'string', description: 'Used only to filter a specific domain' }
|
||||
},
|
||||
required: ['from', 'to']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']> {
|
||||
|
||||
constructor() {
|
||||
|
||||
super({
|
||||
'getVisitsCount': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string, domain?: string }) => {
|
||||
|
||||
const query: any = {
|
||||
project_id: data.project_id,
|
||||
created_at: {
|
||||
$gt: new Date(data.from),
|
||||
$lt: new Date(data.to),
|
||||
},
|
||||
website: data.domain || { $ne: '_NODOMAIN_' }
|
||||
}
|
||||
|
||||
if (data.website) query.website = data.website;
|
||||
|
||||
if (data.page) query.page = data.page;
|
||||
|
||||
const result = await VisitModel.countDocuments(query);
|
||||
|
||||
return { count: result };
|
||||
|
||||
},
|
||||
tool: getVisitsCountsTool
|
||||
},
|
||||
'getVisitsTimeline': {
|
||||
handler: async (data: { project_id: string, from: string, to: string, time_offset: number, website?: string, page?: string, domain?: string }) => {
|
||||
|
||||
const timelineData = await executeTimelineAggregation({
|
||||
projectId: new Types.ObjectId(data.project_id),
|
||||
model: VisitModel,
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
slice: 'day',
|
||||
timeOffset: data.time_offset,
|
||||
domain: data.domain || { $ne: '_NODOMAIN_' } as any
|
||||
});
|
||||
return { data: timelineData };
|
||||
},
|
||||
tool: getVisitsTimelineTool
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const AiVisitsInstance = new AiVisits();
|
||||
21
dashboard/server/api/admin/aggregate_all.ts
Normal file
21
dashboard/server/api/admin/aggregate_all.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { eachDayOfInterval } from "date-fns";
|
||||
import { executeAggregation } from "~/server/services/AggregationService";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
|
||||
const dates = eachDayOfInterval({
|
||||
start: new Date('2025-09-01T00:00:00.000Z'),
|
||||
end: new Date('2025-09-25T00:00:00.000Z')
|
||||
});
|
||||
|
||||
for (const date of dates) {
|
||||
console.log(new Date().toLocaleTimeString('it-IT'), 'AGGREGATION', date);
|
||||
await executeAggregation(date);
|
||||
}
|
||||
|
||||
console.log('COMPLETED')
|
||||
|
||||
});
|
||||
7
dashboard/server/api/admin/aichats.ts
Normal file
7
dashboard/server/api/admin/aichats.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AiNewChatModel } from "~/shared/schema/ai/AiNewChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
const result = await AiNewChatModel.find({});
|
||||
return result;
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
|
||||
const queueRes = await fetch("http://94.130.182.52:3031/metrics/queue");
|
||||
const queue = await queueRes.json();
|
||||
const durationsRes = await fetch("http://94.130.182.52:3031/metrics/durations");
|
||||
const durations = await durationsRes.json();
|
||||
|
||||
return { queue, durations: durations }
|
||||
|
||||
|
||||
});
|
||||
@@ -1,24 +1,112 @@
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { EventModel } from "~/shared/schema/metrics/EventSchema";
|
||||
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
|
||||
import { PremiumModel } from "~/shared/schema/PremiumSchema";
|
||||
import { ProjectModel } from "~/shared/schema/project/ProjectSchema";
|
||||
|
||||
export type TAdminCounts = {
|
||||
projects: number;
|
||||
paid: number;
|
||||
appsumo: number;
|
||||
active: number;
|
||||
dead: number;
|
||||
visits: number;
|
||||
events: number;
|
||||
users: number;
|
||||
free_trial: number;
|
||||
free_trial_ended: number;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
|
||||
const { from } = getQuery(event);
|
||||
const projects = await ProjectModel.countDocuments({});
|
||||
|
||||
const date = new Date(parseInt(from as any));
|
||||
const users = await PremiumModel.countDocuments();
|
||||
|
||||
const projectsCount = await ProjectModel.countDocuments({
|
||||
created_at: { $gte: date }
|
||||
});
|
||||
const usersCount = await UserModel.countDocuments({
|
||||
created_at: { $gte: date }
|
||||
const premium = await PremiumModel.countDocuments({
|
||||
premium_type: {
|
||||
$in: [
|
||||
8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return { users: usersCount, projects: projectsCount }
|
||||
const free_trial = await PremiumModel.countDocuments({ premium_type: 7006 });
|
||||
const free_trial_ended = await PremiumModel.countDocuments({ premium_type: 7999 });
|
||||
|
||||
const appsumo = await PremiumModel.countDocuments({ premium_type: { $in: [6001, 6002, 6003, 6004] } });
|
||||
|
||||
const result = await ProjectModel.aggregate([
|
||||
{
|
||||
$lookup: {
|
||||
from: "visits",
|
||||
let: {
|
||||
projectId: "$_id"
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{
|
||||
$eq: [
|
||||
"$project_id",
|
||||
"$$projectId"
|
||||
]
|
||||
},
|
||||
{
|
||||
$gte: [
|
||||
"$created_at",
|
||||
{
|
||||
$dateSubtract: {
|
||||
startDate: "$$NOW",
|
||||
unit: "day",
|
||||
amount: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$limit: 1
|
||||
}
|
||||
],
|
||||
as: "recent_visit"
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
"recent_visit.0": {
|
||||
$exists: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$count: "active"
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
const visits = await VisitModel.estimatedDocumentCount();
|
||||
const events = await EventModel.estimatedDocumentCount();
|
||||
|
||||
const active = result.length == 0 ? 0 : result[0].active;
|
||||
|
||||
return {
|
||||
projects,
|
||||
users,
|
||||
paid: premium,
|
||||
appsumo,
|
||||
active,
|
||||
dead: projects - active,
|
||||
visits,
|
||||
events,
|
||||
free_trial,
|
||||
free_trial_ended
|
||||
} as TAdminCounts;
|
||||
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
|
||||
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import StripeService from '~/server/services/StripeService';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const { project_id } = getQuery(event);
|
||||
if (!project_id) return setResponseStatus(event, 400, 'ProjectId is required');
|
||||
|
||||
const project = await ProjectModel.findById(project_id);
|
||||
const limits = await ProjectLimitModel.findOne({ project_id });
|
||||
const counts = await ProjectCountModel.findOne({ project_id });
|
||||
const user = await UserModel.findOne({ project_id });
|
||||
|
||||
const subscription =
|
||||
project?.subscription_id ?
|
||||
await StripeService.getSubscription(project.subscription_id) : 'NONE';
|
||||
|
||||
const customer =
|
||||
project?.customer_id ?
|
||||
await StripeService.getCustomer(project.customer_id) : 'NONE';
|
||||
|
||||
return { project, limits, counts, user, subscription, customer }
|
||||
|
||||
});
|
||||
16
dashboard/server/api/admin/domains.ts
Normal file
16
dashboard/server/api/admin/domains.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Types } from "mongoose";
|
||||
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
|
||||
const { pid } = getQuery(event);
|
||||
|
||||
const start = performance.now();
|
||||
const domains = await VisitModel.distinct('website', { project_id: new Types.ObjectId(pid as string) });
|
||||
const end = performance.now();
|
||||
|
||||
return domains;
|
||||
|
||||
});
|
||||
@@ -1,31 +1,21 @@
|
||||
import { FeedbackModel, TFeedback } from "~/shared/schema/FeedbackSchema";
|
||||
|
||||
import { FeedbackModel } from '@schema/FeedbackSchema';
|
||||
export type PopulatedFeedback = Omit<TFeedback, 'user_id'> & {
|
||||
user_id?: { email?: string };
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const feedbacks = await FeedbackModel.aggregate([
|
||||
{
|
||||
$lookup: {
|
||||
from: 'users',
|
||||
localField: 'user_id',
|
||||
foreignField: '_id',
|
||||
as: 'user'
|
||||
}
|
||||
const feedbacks = await FeedbackModel.find({}, {}, {
|
||||
populate: {
|
||||
path: 'user_id',
|
||||
model: 'users',
|
||||
select: 'email'
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'projects',
|
||||
localField: 'project_id',
|
||||
foreignField: '_id',
|
||||
as: 'project'
|
||||
}
|
||||
},
|
||||
])
|
||||
lean: true
|
||||
});
|
||||
|
||||
return feedbacks;
|
||||
return feedbacks as any as PopulatedFeedback[];
|
||||
|
||||
});
|
||||
8
dashboard/server/api/admin/feedbacks_delete.ts
Normal file
8
dashboard/server/api/admin/feedbacks_delete.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { FeedbackModel, TFeedback } from "~/shared/schema/FeedbackSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
const { id } = getQuery(event);
|
||||
const deletation = await FeedbackModel.deleteOne({ _id: id });
|
||||
return deletation;
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
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 }
|
||||
|
||||
|
||||
});
|
||||
@@ -1,11 +1,8 @@
|
||||
|
||||
import { OnboardingModel } from '~/shared/schema/OnboardingSchema';
|
||||
import { OnboardingModel } from "~/shared/schema/OnboardingSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
|
||||
const analytics = await OnboardingModel.aggregate([
|
||||
{
|
||||
@@ -1,89 +0,0 @@
|
||||
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
|
||||
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
|
||||
import { TAdminProject } from "./projects";
|
||||
import { Types } from "mongoose";
|
||||
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
|
||||
|
||||
function addFieldsFromArray(data: { fieldName: string, projectedName: string, arrayName: string }[]) {
|
||||
const content: Record<string, any> = {};
|
||||
data.forEach(e => {
|
||||
content[e.projectedName] = {
|
||||
"$ifNull": [{ "$getField": { "field": e.fieldName, "input": { "$arrayElemAt": [`$${e.arrayName}`, 0] } } }, 0]
|
||||
}
|
||||
});
|
||||
return content;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const { pid } = getQuery(event);
|
||||
|
||||
const projects = await ProjectModel.aggregate([
|
||||
{
|
||||
$match: { _id: new Types.ObjectId(pid as string) }
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_limits",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
as: "limits"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_counts",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
as: "counts"
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: addFieldsFromArray([
|
||||
{ arrayName: 'counts', fieldName: 'visits', projectedName: 'visits' },
|
||||
{ arrayName: 'counts', fieldName: 'events', projectedName: 'events' },
|
||||
{ arrayName: 'counts', fieldName: 'session', projectedName: 'session' },
|
||||
{ arrayName: 'counts', fieldName: 'updated_at', projectedName: 'last_log_at' },
|
||||
]),
|
||||
},
|
||||
{
|
||||
$addFields: addFieldsFromArray([
|
||||
{ arrayName: 'limits', fieldName: 'visits', projectedName: 'limit_visits' },
|
||||
{ arrayName: 'limits', fieldName: 'events', projectedName: 'limit_events' },
|
||||
{ arrayName: 'limits', fieldName: 'limit', projectedName: 'limit_max' },
|
||||
{ arrayName: 'limits', fieldName: 'ai_messages', projectedName: 'limit_ai_messages' },
|
||||
{ arrayName: 'limits', fieldName: 'ai_limit', projectedName: 'limit_ai_max' },
|
||||
]),
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
limit_total: {
|
||||
$add: [
|
||||
{ $ifNull: ["$limit_visits", 0] },
|
||||
{ $ifNull: ["$limit_events", 0] }
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
{ $unset: 'counts' },
|
||||
{ $unset: 'limits' },
|
||||
]);
|
||||
|
||||
const domains = await VisitModel.aggregate([
|
||||
{
|
||||
$match: { project_id: new Types.ObjectId(pid as string) }
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$website',
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
return { domains, project: (projects[0] as TAdminProject) };
|
||||
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
|
||||
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
|
||||
|
||||
type ExtendedProject = {
|
||||
limits: TProjectLimit[],
|
||||
counts: [{
|
||||
events: number,
|
||||
visits: number,
|
||||
sessions: number
|
||||
}],
|
||||
visits: number,
|
||||
events: number,
|
||||
sessions: number,
|
||||
limit_visits: number,
|
||||
limit_events: number,
|
||||
limit_max: number,
|
||||
limit_ai_messages: number,
|
||||
limit_ai_max: number,
|
||||
limit_total: number,
|
||||
last_log_at: string
|
||||
}
|
||||
|
||||
export type TAdminProject = TProject & ExtendedProject;
|
||||
|
||||
function addFieldsFromArray(data: { fieldName: string, projectedName: string, arrayName: string }[]) {
|
||||
const content: Record<string, any> = {};
|
||||
data.forEach(e => {
|
||||
content[e.projectedName] = {
|
||||
"$ifNull": [{ "$getField": { "field": e.fieldName, "input": { "$arrayElemAt": [`$${e.arrayName}`, 0] } } }, 0]
|
||||
}
|
||||
});
|
||||
return content;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const { page, limit, sortQuery, filterQuery, filterFrom, filterTo } = getQuery(event);
|
||||
|
||||
const pageNumber = parseInt(page as string);
|
||||
const limitNumber = parseInt(limit as string);
|
||||
|
||||
const matchQuery = {
|
||||
...JSON.parse(filterQuery as string),
|
||||
created_at: {
|
||||
$gte: new Date(filterFrom as string),
|
||||
$lte: new Date(filterTo as string)
|
||||
}
|
||||
}
|
||||
|
||||
const count = await ProjectModel.countDocuments(matchQuery);
|
||||
|
||||
const projects = await ProjectModel.aggregate([
|
||||
{
|
||||
$match: matchQuery
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_limits",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
as: "limits"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_counts",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
as: "counts"
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: addFieldsFromArray([
|
||||
{ arrayName: 'counts', fieldName: 'visits', projectedName: 'visits' },
|
||||
{ arrayName: 'counts', fieldName: 'events', projectedName: 'events' },
|
||||
{ arrayName: 'counts', fieldName: 'session', projectedName: 'session' },
|
||||
{ arrayName: 'counts', fieldName: 'updated_at', projectedName: 'last_log_at' },
|
||||
]),
|
||||
},
|
||||
{
|
||||
$addFields: addFieldsFromArray([
|
||||
{ arrayName: 'limits', fieldName: 'visits', projectedName: 'limit_visits' },
|
||||
{ arrayName: 'limits', fieldName: 'events', projectedName: 'limit_events' },
|
||||
{ arrayName: 'limits', fieldName: 'limit', projectedName: 'limit_max' },
|
||||
{ arrayName: 'limits', fieldName: 'ai_messages', projectedName: 'limit_ai_messages' },
|
||||
{ arrayName: 'limits', fieldName: 'ai_limit', projectedName: 'limit_ai_max' },
|
||||
]),
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
limit_total: {
|
||||
$add: [
|
||||
{ $ifNull: ["$limit_visits", 0] },
|
||||
{ $ifNull: ["$limit_events", 0] }
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
{ $unset: 'counts' },
|
||||
{ $unset: 'limits' },
|
||||
{ $sort: JSON.parse(sortQuery as string) },
|
||||
{ $skip: pageNumber * limitNumber },
|
||||
{ $limit: limitNumber }
|
||||
]);
|
||||
|
||||
return {
|
||||
count,
|
||||
projects: projects as TAdminProject[]
|
||||
};
|
||||
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
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 { project_id } = getQuery(event);
|
||||
if (!project_id) return setResponseStatus(event, 400, 'ProjectId is required');
|
||||
|
||||
const events = await EventModel.countDocuments({ project_id });
|
||||
const visits = await VisitModel.countDocuments({ project_id });
|
||||
const sessions = await SessionModel.countDocuments({ project_id });
|
||||
|
||||
await ProjectCountModel.updateOne({ project_id, events, visits, sessions }, {}, { upsert: true });
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
89
dashboard/server/api/admin/shard/info.ts
Normal file
89
dashboard/server/api/admin/shard/info.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
|
||||
|
||||
import mongoose from "mongoose";
|
||||
|
||||
|
||||
async function executeAggregation(uuid: string) {
|
||||
const aggregation = [
|
||||
{ $match: { uuid: new mongoose.Types.UUID(uuid) } },
|
||||
{ $group: { _id: "$shard", chunkCount: { $sum: 1 } } },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
total: { $sum: "$chunkCount" },
|
||||
shards: { $push: { shard: "$_id", count: "$chunkCount" } }
|
||||
}
|
||||
},
|
||||
{ $unwind: "$shards" },
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
shard: "$shards.shard",
|
||||
chunkCount: "$shards.count",
|
||||
percent: { $round: [{ $multiply: [{ $divide: ["$shards.count", "$total"] }, 100] }, 2] }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const result = await mongoose.connection.useDb('config').collection('chunks').aggregate(aggregation).toArray();
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
async function getAllCollections() {
|
||||
const result = await mongoose.connection.useDb('config').collection('collections').find({}).toArray();
|
||||
return result.filter((e: any) => e._id.startsWith('SimpleMetrics'));
|
||||
}
|
||||
|
||||
async function getOperations() {
|
||||
try {
|
||||
|
||||
const db = mongoose.connection.db?.admin();
|
||||
if (!db) return [];
|
||||
const result = await db.command({
|
||||
aggregate: 1,
|
||||
pipeline: [
|
||||
{ $currentOp: { allUsers: true, localOps: false } },
|
||||
{ $match: { type: 'op', "originatingCommand.reshardCollection": { $regex: "^SimpleMetrics.*" } } }
|
||||
],
|
||||
cursor: {}
|
||||
});
|
||||
|
||||
return result.cursor.firstBatch
|
||||
} catch (ex) {
|
||||
console.error('Error fetching current ops:', ex);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getAdvancedInfo(collection: string) {
|
||||
try {
|
||||
const db = mongoose.connection.useDb('SimpleMetrics');
|
||||
const nativeDb = db.db;
|
||||
const stats = await nativeDb?.command({ collStats: collection });
|
||||
return stats;
|
||||
} catch (ex) {
|
||||
console.error("Error getting index info:", ex);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
|
||||
const collections = await getAllCollections();
|
||||
|
||||
const aggregations = await Promise.all(collections.map(async e => {
|
||||
const collName = e._id.toString().split('.')[1];
|
||||
const chunks = await executeAggregation(e.uuid.toString());
|
||||
const advanced = await getAdvancedInfo(collName);
|
||||
return { info: e, advanced, chunks }
|
||||
}))
|
||||
|
||||
const operations = await getOperations();
|
||||
|
||||
return { aggregations, operations };
|
||||
|
||||
|
||||
});
|
||||
@@ -1,48 +1,226 @@
|
||||
import { TProject } from "@schema/project/ProjectSchema";
|
||||
import { TUser, UserModel } from "@schema/UserSchema";
|
||||
import { ProjectModel } from "~/shared/schema/project/ProjectSchema";
|
||||
import { UserModel } from "~/shared/schema/UserSchema";
|
||||
import { parseNumberInt } from "~/utils/parseNumber";
|
||||
|
||||
export type TAdminUser = TUser & { _id: string, projects: TProject[] };
|
||||
export type TAdminUser = {
|
||||
email: string;
|
||||
premium_type: string;
|
||||
created_at: string;
|
||||
limit: number;
|
||||
visits: number;
|
||||
events: number;
|
||||
projects: { name: string, counts: [any], _id: string }[]
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const { page, limit, sortQuery, filterQuery, filterFrom, filterTo } = getQuery(event);
|
||||
const ctx = await getRequestContext(event, 'admin');
|
||||
|
||||
const pageNumber = parseInt(page as string);
|
||||
const limitNumber = parseInt(limit as string);
|
||||
const { page, limit, from, to, sort, search } = getQuery(event);
|
||||
|
||||
const matchQuery = {
|
||||
...JSON.parse(filterQuery as string),
|
||||
created_at: {
|
||||
$gte: new Date(filterFrom as string),
|
||||
$lte: new Date(filterTo as string)
|
||||
}
|
||||
const pageValue = parseNumberInt(page, 1);
|
||||
const limitValue = parseNumberInt(limit, 10);
|
||||
const skipValue = (pageValue - 1) * limitValue;
|
||||
|
||||
|
||||
const getSortQuery: () => any = () => {
|
||||
if (sort === 'usage-more') return { visits: -1 }
|
||||
if (sort === 'usage-less') return { visits: 1 }
|
||||
if (sort === 'newer') return { created_at: -1 }
|
||||
if (sort === 'older') return { created_at: 1 }
|
||||
return { created_at: -1 }
|
||||
}
|
||||
|
||||
|
||||
const count = await UserModel.countDocuments(matchQuery);
|
||||
|
||||
let users: any[] = [];
|
||||
|
||||
|
||||
const users = await UserModel.aggregate([
|
||||
{
|
||||
$match: matchQuery
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "projects",
|
||||
localField: "_id",
|
||||
foreignField: "owner",
|
||||
as: "projects"
|
||||
if (!search || search === '') {
|
||||
users = await UserModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
created_at: {
|
||||
$gte: new Date(from as string),
|
||||
$lte: new Date(to as string),
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "premiums",
|
||||
localField: "_id",
|
||||
foreignField: "user_id",
|
||||
as: "premiums"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "user_limits",
|
||||
localField: "_id",
|
||||
foreignField: "user_id",
|
||||
as: "limits"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "projects",
|
||||
let: {
|
||||
userId: "$_id"
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$eq: ["$$userId", "$owner"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_counts",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
as: "counts"
|
||||
}
|
||||
}
|
||||
],
|
||||
as: "projects"
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
email: "$email",
|
||||
created_at: "$created_at",
|
||||
premium_type: {
|
||||
$arrayElemAt: [
|
||||
"$premiums.premium_type",
|
||||
0
|
||||
]
|
||||
},
|
||||
limit: {
|
||||
$arrayElemAt: ["$limits.limit", 0]
|
||||
},
|
||||
visits: {
|
||||
$arrayElemAt: ["$limits.visits", 0]
|
||||
},
|
||||
events: {
|
||||
$arrayElemAt: ["$limits.events", 0]
|
||||
},
|
||||
projects: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: getSortQuery()
|
||||
},
|
||||
{
|
||||
$skip: skipValue
|
||||
},
|
||||
{
|
||||
$limit: limitValue
|
||||
}
|
||||
},
|
||||
{ $sort: JSON.parse(sortQuery as string) },
|
||||
{ $skip: pageNumber * limitNumber },
|
||||
{ $limit: limitNumber }
|
||||
]);
|
||||
])
|
||||
} else {
|
||||
|
||||
return { count, users: users as TAdminUser[] }
|
||||
const matchingProjects = await ProjectModel.find({
|
||||
name: { $regex: new RegExp(search as string) }
|
||||
}, { owner: 1 });
|
||||
|
||||
|
||||
|
||||
users = await UserModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ _id: { $in: matchingProjects.map(e => e.owner) } },
|
||||
{ name: { $regex: new RegExp(search as string) } },
|
||||
{ email: { $regex: new RegExp(search as string) } },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "premiums",
|
||||
localField: "_id",
|
||||
foreignField: "user_id",
|
||||
as: "premiums"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "user_limits",
|
||||
localField: "_id",
|
||||
foreignField: "user_id",
|
||||
as: "limits"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "projects",
|
||||
let: {
|
||||
userId: "$_id"
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$eq: ["$$userId", "$owner"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_counts",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
as: "counts"
|
||||
}
|
||||
}
|
||||
],
|
||||
as: "projects"
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
email: "$email",
|
||||
created_at: "$created_at",
|
||||
premium_type: {
|
||||
$arrayElemAt: [
|
||||
"$premiums.premium_type",
|
||||
0
|
||||
]
|
||||
},
|
||||
limit: {
|
||||
$arrayElemAt: ["$limits.limit", 0]
|
||||
},
|
||||
visits: {
|
||||
$arrayElemAt: ["$limits.visits", 0]
|
||||
},
|
||||
events: {
|
||||
$arrayElemAt: ["$limits.events", 0]
|
||||
},
|
||||
projects: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: getSortQuery()
|
||||
},
|
||||
{
|
||||
$skip: skipValue
|
||||
},
|
||||
{
|
||||
$limit: limitValue
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
|
||||
const count = await UserModel.countDocuments({
|
||||
created_at: {
|
||||
$gte: new Date(from as string),
|
||||
$lte: new Date(to as string),
|
||||
}
|
||||
});
|
||||
|
||||
return { users, count } as { users: TAdminUser[], count: number };
|
||||
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
|
||||
import { TUser, UserModel } from "@schema/UserSchema";
|
||||
import { TProjectLimit } from "~/shared/schema/project/ProjectsLimits";
|
||||
|
||||
export type TAdminUserProjectInfo = TUser & {
|
||||
projects: (TProject & {
|
||||
limits: TProjectLimit[],
|
||||
visits: number,
|
||||
events: number,
|
||||
sessions: number
|
||||
})[],
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return;
|
||||
if (!userData.user.roles.includes('ADMIN')) return;
|
||||
|
||||
const { page, limit, sortQuery } = getQuery(event);
|
||||
|
||||
const pageNumber = parseInt(page as string);
|
||||
const limitNumber = parseInt(limit as string);
|
||||
|
||||
const users = await UserModel.aggregate([
|
||||
{
|
||||
$lookup: {
|
||||
from: "projects",
|
||||
localField: "_id",
|
||||
foreignField: "owner",
|
||||
pipeline: [
|
||||
{
|
||||
$lookup: {
|
||||
from: "project_limits",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
as: "limits"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "visits",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
pipeline: [
|
||||
{
|
||||
$count: "total_visits"
|
||||
}
|
||||
],
|
||||
as: "visit_data"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "events",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
pipeline: [
|
||||
{
|
||||
$count: "total_events"
|
||||
}
|
||||
],
|
||||
as: "event_data"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "sessions",
|
||||
localField: "_id",
|
||||
foreignField: "project_id",
|
||||
pipeline: [
|
||||
{
|
||||
$count: "total_sessions"
|
||||
}
|
||||
],
|
||||
as: "session_data"
|
||||
}
|
||||
},
|
||||
{ $addFields: { visits: { $ifNull: [{ $arrayElemAt: ["$visit_data.total_visits", 0] }, 0] } } },
|
||||
{ $addFields: { events: { $ifNull: [{ $arrayElemAt: ["$event_data.total_events", 0] }, 0] } } },
|
||||
{ $addFields: { sessions: { $ifNull: [{ $arrayElemAt: ["$session_data.total_sessions", 0] }, 0] } }, },
|
||||
{ $unset: "visit_data" },
|
||||
{ $unset: "event_data" },
|
||||
{ $unset: "session_data" }
|
||||
|
||||
],
|
||||
as: "projects"
|
||||
},
|
||||
},
|
||||
{ $sort: JSON.parse(sortQuery as string) },
|
||||
{ $skip: pageNumber * limitNumber },
|
||||
{ $limit: limitNumber }
|
||||
]);
|
||||
|
||||
return users as TAdminUserProjectInfo[];
|
||||
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, [], ['AI']);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
if (!event.context.params) return;
|
||||
const chat_id = event.context.params['chat_id'];
|
||||
|
||||
const result = await AiChatModel.updateOne({ _id: chat_id, project_id }, { deleted: true });
|
||||
return result.modifiedCount > 0;
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
import type OpenAI from "openai";
|
||||
import { getChartsInMessage } from "~/server/services/AiService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event, [], ['AI']);
|
||||
if (!data) return;
|
||||
|
||||
const isAdmin = data.user.user.roles.includes('ADMIN');
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
if (!event.context.params) return;
|
||||
const chat_id = event.context.params['chat_id'];
|
||||
|
||||
const chat = await AiChatModel.findOne({ _id: chat_id, project_id });
|
||||
if (!chat) return;
|
||||
|
||||
return (chat.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[])
|
||||
.filter(e => isAdmin ? true : (e.role === 'assistant' || e.role === 'user'))
|
||||
.map(e => {
|
||||
const charts = getChartsInMessage(e);
|
||||
const content = e.content;
|
||||
return { ...e, charts }
|
||||
})
|
||||
.filter(e => {
|
||||
return isAdmin ? true : (e.charts.length > 0 || e.content);
|
||||
})
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event, [], ['AI']);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
if (!event.context.params) return;
|
||||
const chat_id = event.context.params['chat_id'];
|
||||
|
||||
const chat = await AiChatModel.findOne({ _id: chat_id, project_id }, { status: 1, completed: 1 });
|
||||
if (!chat) return;
|
||||
|
||||
return { status: chat.status, completed: chat.completed || false }
|
||||
});
|
||||
22
dashboard/server/api/ai/ask.post.ts
Normal file
22
dashboard/server/api/ai/ask.post.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AiService } from "~/server/services/ai/AiService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { user_email, pid } = ctx;
|
||||
|
||||
const query = getQuery(event);
|
||||
|
||||
const { message } = await readBody(event);
|
||||
if (!message) throw createError({ status: 400, message: 'message is required' });
|
||||
|
||||
const chat_id = await AiService.handleUserMessage({
|
||||
name: user_email.split('@')[0],
|
||||
pid,
|
||||
text: message,
|
||||
chat_id: query.chat_id?.toString()
|
||||
});
|
||||
|
||||
return { chat_id }
|
||||
|
||||
});
|
||||
16
dashboard/server/api/ai/chat.ts
Normal file
16
dashboard/server/api/ai/chat.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
import { AiNewChatModel } from "~/shared/schema/ai/AiNewChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id } = ctx;
|
||||
|
||||
const { id } = getQuery(event);
|
||||
|
||||
const chat = await AiNewChatModel.findOne({ _id: id, project_id });
|
||||
if (!chat) return;
|
||||
|
||||
return chat;
|
||||
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const chatList = await AiChatModel.find({ project_id, deleted: false }, { _id: 1, title: 1 }, { sort: { updated_at: 1 } });
|
||||
|
||||
return chatList.map(e => e.toJSON());
|
||||
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
|
||||
|
||||
export async function getAiChatRemainings(project_id: string) {
|
||||
const limits = await ProjectLimitModel.findOne({ project_id })
|
||||
if (!limits) return 0;
|
||||
const chatsRemaining = limits.ai_limit - limits.ai_messages;
|
||||
|
||||
if (isNaN(chatsRemaining)) return 0;
|
||||
return chatsRemaining;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event, [], ['AI']);
|
||||
if (!data) return;
|
||||
|
||||
const { pid } = data;
|
||||
|
||||
const chatsRemaining = await getAiChatRemainings(pid);
|
||||
return chatsRemaining;
|
||||
});
|
||||
@@ -1,13 +1,14 @@
|
||||
|
||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
import { AiNewChatModel } from "~/shared/schema/ai/AiNewChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, [], ['AI']);
|
||||
if (!data) return;
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id } = ctx;
|
||||
|
||||
const { project_id } = data;
|
||||
const chat = await AiNewChatModel.updateMany({ project_id }, { deleted: true });
|
||||
if (!chat) return;
|
||||
|
||||
return chat;
|
||||
|
||||
const result = await AiChatModel.updateMany({ project_id }, { deleted: true });
|
||||
return result.modifiedCount > 0;
|
||||
});
|
||||
16
dashboard/server/api/ai/delete_chat.ts
Normal file
16
dashboard/server/api/ai/delete_chat.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
import { AiNewChatModel } from "~/shared/schema/ai/AiNewChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id } = ctx;
|
||||
|
||||
const { chat_id } = getQuery(event);
|
||||
|
||||
const chat = await AiNewChatModel.updateOne({ _id: chat_id, project_id }, { deleted: true });
|
||||
if (!chat) return;
|
||||
|
||||
return chat;
|
||||
|
||||
});
|
||||
21
dashboard/server/api/ai/downvote_message.post.ts
Normal file
21
dashboard/server/api/ai/downvote_message.post.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
import { Types } from "mongoose";
|
||||
import { AiNewChatModel } from "~/shared/schema/ai/AiNewChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id } = ctx;
|
||||
|
||||
const { chat_id, message_index } = getQuery(event);
|
||||
|
||||
if (!chat_id) throw createError({ status: 400, message: 'chat_id is required' });
|
||||
if (!message_index) throw createError({ status: 400, message: 'message_index is required' });
|
||||
const index = parseInt(message_index as string);
|
||||
if (isNaN(index)) throw createError({ status: 400, message: 'message_index must be a number' });
|
||||
|
||||
const update = await AiNewChatModel.updateOne({ _id: new Types.ObjectId(chat_id as string), project_id }, { $set: { [`messages.${index}.downvoted`]: true } });
|
||||
|
||||
return update;
|
||||
|
||||
});
|
||||
9
dashboard/server/api/ai/insight.ts
Normal file
9
dashboard/server/api/ai/insight.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { AiService } from "~/server/services/ai/AiService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'permission:ai');
|
||||
const { project_id } = ctx;
|
||||
const res = await AiService.generateInsight(project_id.toString());
|
||||
return res;
|
||||
});
|
||||
12
dashboard/server/api/ai/list.ts
Normal file
12
dashboard/server/api/ai/list.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { AiNewChatModel } from "~/shared/schema/ai/AiNewChatSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id } = ctx;
|
||||
|
||||
const chats = await AiNewChatModel.find({ project_id, deleted: false }, { _id: 1, title: 1, created_at: 1, updated_at: 1, status: 1 });
|
||||
|
||||
return chats;
|
||||
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import { sendMessageOnChat, updateChatStatus } from "~/server/services/AiService";
|
||||
import { getAiChatRemainings } from "./chats_remaining";
|
||||
import { ProjectLimitModel } from "@schema/project/ProjectsLimits";
|
||||
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestData(event, [], ['AI']);
|
||||
if (!data) return;
|
||||
|
||||
const { pid } = data;
|
||||
|
||||
const { text, chat_id, timeOffset } = await readBody(event);
|
||||
if (!text) return setResponseStatus(event, 400, 'text parameter missing');
|
||||
|
||||
const chatsRemaining = await getAiChatRemainings(pid);
|
||||
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
|
||||
|
||||
|
||||
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } });
|
||||
|
||||
const currentStatus: string[] = [];
|
||||
|
||||
let responseSent = false;
|
||||
|
||||
let targetChatId = '';
|
||||
|
||||
await sendMessageOnChat(text, pid, timeOffset, chat_id, {
|
||||
onChatId: async chat_id => {
|
||||
if (!responseSent) {
|
||||
event.node.res.setHeader('Content-Type', 'application/json');
|
||||
event.node.res.end(JSON.stringify({ chat_id }));
|
||||
targetChatId = chat_id;
|
||||
responseSent = true;
|
||||
}
|
||||
},
|
||||
onDelta: async text => {
|
||||
currentStatus.push(text);
|
||||
await updateChatStatus(targetChatId, currentStatus.join(''), false);
|
||||
},
|
||||
onFunctionName: async name => {
|
||||
currentStatus.push('[data:FunctionName]');
|
||||
await updateChatStatus(targetChatId, currentStatus.join(''), false);
|
||||
},
|
||||
onFunctionCall: async name => {
|
||||
currentStatus.push('[data:FunctionCall]');
|
||||
await updateChatStatus(targetChatId, currentStatus.join(''), false);
|
||||
},
|
||||
onFunctionResult: async (name, result) => {
|
||||
currentStatus.push('[data:FunctionResult]');
|
||||
await updateChatStatus(targetChatId, currentStatus.join(''), false);
|
||||
},
|
||||
onFinish: async calls => {
|
||||
// currentStatus.push('[data:FunctionFinish]');
|
||||
// await updateChatStatus(targetChatId, currentStatus.join(''), false);
|
||||
}
|
||||
});
|
||||
|
||||
await updateChatStatus(targetChatId, '', true);
|
||||
|
||||
});
|
||||
@@ -1,28 +1,49 @@
|
||||
|
||||
import { createUserJwt, readRegisterJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import { PasswordModel } from '@schema/PasswordSchema';
|
||||
import { EmailService } from '@services/EmailService';
|
||||
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||
import { RegisterModel } from '~/shared/schema/RegisterSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const { register_code } = getQuery(event);
|
||||
//TODO: SELFHOST
|
||||
|
||||
const { code } = getQuery(event);
|
||||
|
||||
const data = readRegisterJwt(register_code as string);
|
||||
if (!data) return setResponseStatus(event, 400, 'Error decoding register_code');
|
||||
const registerTarget = await RegisterModel.findOne({ code });
|
||||
if (!registerTarget) throw createError({ status: 400, message: 'Code not valid' });
|
||||
|
||||
const userExist = await UserModel.exists({ email: registerTarget.email });
|
||||
if (userExist) throw createError({ status: 400, message: 'Email already registered' });
|
||||
|
||||
await PasswordModel.updateOne({ email: registerTarget.email }, { password: registerTarget.password }, { upsert: true });
|
||||
|
||||
const user = await UserModel.create({ email: registerTarget.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
|
||||
|
||||
const tRpc = useTRPC();
|
||||
|
||||
const customer = await tRpc.payments.customer.create.mutate({ email: user.email });
|
||||
await tRpc.payments.subscription.activate.mutate({ user_id: user._id.toString(), customer_id: customer.customer_id, plan_tag: 'FREE_TRIAL_LITLYX_PRO' });
|
||||
|
||||
setImmediate(() => {
|
||||
tRpc.emails.brevo.addToBrevoList.mutate({ email: user.email });
|
||||
});
|
||||
|
||||
setImmediate(() => {
|
||||
tRpc.emails.email.send_trial_1_started.mutate({ email: user.email });
|
||||
});
|
||||
|
||||
await replaceUserSession(event, {
|
||||
user: {
|
||||
email: user.email,
|
||||
name: user.name
|
||||
},
|
||||
secure: {
|
||||
user_id: user.id
|
||||
},
|
||||
v: '0.0.1'
|
||||
});
|
||||
|
||||
return sendRedirect(event, '/');
|
||||
|
||||
try {
|
||||
await PasswordModel.create({ email: data.email, password: data.password })
|
||||
await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
|
||||
setImmediate(() => {
|
||||
const emailData = EmailService.getEmailServerInfo('welcome', { target: data.email });
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
});
|
||||
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
|
||||
return sendRedirect(event, `https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
|
||||
} catch (ex) {
|
||||
return setResponseStatus(event, 400, 'Error creating user');
|
||||
}
|
||||
|
||||
});
|
||||
65
dashboard/server/api/auth/google/authenticate.ts
Normal file
65
dashboard/server/api/auth/google/authenticate.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { UserModel } from "~/shared/schema/UserSchema"
|
||||
|
||||
|
||||
|
||||
export default defineOAuthGoogleEventHandler({
|
||||
|
||||
async onSuccess(event, result) {
|
||||
|
||||
const user = await UserModel.findOne({
|
||||
email: result.user.email
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
||||
const newUser = await UserModel.create({
|
||||
email: result.user.email,
|
||||
name: result.user.name ?? 'NO_NAME',
|
||||
given_name: result.user.given_name ?? 'NO_NAME',
|
||||
locale: result.user.locale ?? '',
|
||||
picture: ''
|
||||
})
|
||||
|
||||
await replaceUserSession(event, {
|
||||
user: {
|
||||
email: newUser.email,
|
||||
name: newUser.name
|
||||
},
|
||||
secure: {
|
||||
user_id: newUser.id
|
||||
},
|
||||
v: '0.0.1'
|
||||
});
|
||||
|
||||
const tRpc = useTRPC();
|
||||
|
||||
const customer = await tRpc.payments.customer.create.mutate({ email: newUser.email });
|
||||
await tRpc.payments.subscription.activate.mutate({
|
||||
user_id: newUser._id.toString(),
|
||||
customer_id: customer.customer_id,
|
||||
plan_tag: 'FREE_TRIAL_LITLYX_PRO'
|
||||
});
|
||||
|
||||
setImmediate(() => {
|
||||
tRpc.emails.email.send_trial_1_started.mutate({ email: newUser.email });
|
||||
});
|
||||
|
||||
return sendRedirect(event, '/')
|
||||
}
|
||||
|
||||
await replaceUserSession(event, {
|
||||
user: {
|
||||
email: user.email,
|
||||
name: user.name
|
||||
},
|
||||
secure: {
|
||||
user_id: user.id
|
||||
},
|
||||
v: '0.0.1'
|
||||
});
|
||||
|
||||
return sendRedirect(event, '/')
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import { createUserJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import { EmailService } from '@services/EmailService';
|
||||
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||
|
||||
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
|
||||
|
||||
const client = new OAuth2Client({
|
||||
clientId: GOOGLE_AUTH_CLIENT_ID,
|
||||
clientSecret: GOOGLE_AUTH_CLIENT_SECRET
|
||||
});
|
||||
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const body = await readBody(event)
|
||||
|
||||
const origin = event.headers.get('origin');
|
||||
|
||||
const tokenResponse = await client.getToken({
|
||||
code: body.code,
|
||||
redirect_uri: origin || ''
|
||||
});
|
||||
|
||||
const tokens = tokenResponse.tokens;
|
||||
|
||||
const ticket = await client.verifyIdToken({
|
||||
idToken: tokens.id_token || '',
|
||||
audience: GOOGLE_AUTH_CLIENT_ID,
|
||||
});
|
||||
|
||||
const payload = ticket.getPayload();
|
||||
if (!payload) return { error: true, access_token: '' };
|
||||
|
||||
|
||||
const user = await UserModel.findOne({ email: payload.email });
|
||||
|
||||
if (user) {
|
||||
user.google_tokens = tokens as any;
|
||||
await user.save();
|
||||
return {
|
||||
error: false,
|
||||
access_token: createUserJwt({ email: user.email, name: user.name })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newUser = new UserModel({
|
||||
email: payload.email,
|
||||
given_name: payload.given_name,
|
||||
name: payload.name,
|
||||
locale: payload.locale,
|
||||
picture: payload.picture,
|
||||
google_tokens: tokens,
|
||||
created_at: Date.now()
|
||||
});
|
||||
|
||||
const savedUser = await newUser.save();
|
||||
|
||||
setImmediate(() => {
|
||||
console.log('SENDING WELCOME EMAIL TO', payload.email);
|
||||
if (!payload.email) return;
|
||||
const emailData = EmailService.getEmailServerInfo('welcome', { target: payload.email });
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
});
|
||||
|
||||
return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) }
|
||||
|
||||
});
|
||||
@@ -1,24 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import { PasswordModel } from '~/shared/schema/PasswordSchema';
|
||||
import { UserModel } from '~/shared/schema/UserSchema';
|
||||
|
||||
import { createUserJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import crypto from 'crypto';
|
||||
import { PasswordModel } from '@schema/PasswordSchema';
|
||||
const ZLoginBody = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6).max(64)
|
||||
});
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
const { email, password } = await readBody(event);
|
||||
//TODO: SELFHOST
|
||||
|
||||
const { email, password } = await readValidatedBody(event, ZLoginBody.parse)
|
||||
|
||||
const user = await UserModel.findOne({ email });
|
||||
if (!user) throw createError({ status: 400, message: 'Email or Password wrong' });
|
||||
|
||||
if (!user) return { error: true, message: 'Email or Password wrong' }
|
||||
const passwordData = await PasswordModel.findOne({ email });
|
||||
if (!passwordData) throw createError({ status: 400, message: 'Email or Password wrong' });
|
||||
const passwordOk = await verifyPassword(passwordData.password, password);
|
||||
if (!passwordOk) throw createError({ status: 400, message: 'Email or Password wrong' });
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
const hashedPassword = hash.update(password + '_litlyx').digest('hex');
|
||||
await replaceUserSession(event, {
|
||||
user: {
|
||||
email: user.email,
|
||||
name: user.name
|
||||
},
|
||||
secure: {
|
||||
user_id: user.id
|
||||
},
|
||||
v: '0.0.1'
|
||||
}, { maxAge: 60 * 60 * 24 * 7 });
|
||||
|
||||
const target = await PasswordModel.findOne({ email, password: hashedPassword });
|
||||
|
||||
if (!target) return { error: true, message: 'Email or Password wrong' }
|
||||
|
||||
return { error: false, access_token: createUserJwt({ email: target.email, name: user.name }) }
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
import { createUserJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
|
||||
const { NOAUTH_USER_EMAIL, NOAUTH_USER_PASS, public: publicRuntime } = useRuntimeConfig();
|
||||
|
||||
const noAuthMode = publicRuntime.AUTH_MODE == 'NO_AUTH';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
if (!noAuthMode) {
|
||||
console.error('Endpoint available only in NO_AUTH mode');
|
||||
return { error: true, access_token: '' }
|
||||
}
|
||||
|
||||
if (!NOAUTH_USER_EMAIL) {
|
||||
console.error('NOAUTH_USER_EMAIL is required in NO_AUTH mode');
|
||||
return { error: true, access_token: '' }
|
||||
}
|
||||
|
||||
if (!NOAUTH_USER_PASS) {
|
||||
console.error('NOAUTH_USER_PASS is required in NO_AUTH mode');
|
||||
return { error: true, access_token: '' }
|
||||
}
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body.email != NOAUTH_USER_EMAIL || body.password != NOAUTH_USER_PASS) return { error: true, access_token: '', errorMessage: 'Username or password invalid' }
|
||||
|
||||
const user = await UserModel.findOne({ email: NOAUTH_USER_EMAIL });
|
||||
|
||||
if (user) return {
|
||||
error: false,
|
||||
access_token: createUserJwt({
|
||||
email: user.email,
|
||||
name: user.name
|
||||
})
|
||||
}
|
||||
|
||||
const newUser = new UserModel({
|
||||
email: NOAUTH_USER_EMAIL,
|
||||
given_name: NOAUTH_USER_EMAIL.split('@')[0] || 'NONAME',
|
||||
name: NOAUTH_USER_EMAIL.split('@')[0] || 'NONAME',
|
||||
locale: 'no-auth',
|
||||
picture: '',
|
||||
created_at: Date.now()
|
||||
});
|
||||
|
||||
const savedUser = await newUser.save();
|
||||
|
||||
return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) }
|
||||
|
||||
});
|
||||
@@ -1,47 +1,36 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { z } from "zod";
|
||||
import { RegisterModel } from '~/shared/schema/RegisterSchema';
|
||||
import { UserModel } from '~/shared/schema/UserSchema';
|
||||
|
||||
import { createRegisterJwt } from '~/server/AuthManager';
|
||||
import { UserModel } from '@schema/UserSchema';
|
||||
import { RegisterModel } from '@schema/RegisterSchema';
|
||||
import { EmailService } from '@services/EmailService';
|
||||
import crypto from 'crypto';
|
||||
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
|
||||
const ZRegisterBody = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6).max(64)
|
||||
});
|
||||
|
||||
function canRegister(email: string, password: string) {
|
||||
if (email.length == 0) return false;
|
||||
if (!email.includes('@')) return false;
|
||||
if (!email.includes('.')) return false;
|
||||
if (password.length < 6) return false;
|
||||
return true;
|
||||
};
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
//TODO: SELFHOST
|
||||
|
||||
const { email, password } = await readValidatedBody(event, ZRegisterBody.parse);
|
||||
|
||||
const { email, password } = await readBody(event);
|
||||
const user = await UserModel.exists({ email });
|
||||
if (user) throw createError({ statusCode: 400, message: 'Email already registered' });
|
||||
|
||||
if (!canRegister(email, password)) return setResponseStatus(event, 400, 'Email or Password not match criteria');
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
const user = await UserModel.findOne({ email });
|
||||
const code = crypto.randomBytes(3).toString('hex').toUpperCase();
|
||||
|
||||
if (user) return {
|
||||
error: true,
|
||||
message: 'Email already registered'
|
||||
}
|
||||
await RegisterModel.updateOne({ email }, { password: hashedPassword, code }, { upsert: true });
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
const hashedPassword = hash.update(password + '_litlyx').digest('hex');
|
||||
const { BASE_URL } = useRuntimeConfig();
|
||||
|
||||
const jwt = createRegisterJwt(email, hashedPassword);
|
||||
|
||||
await RegisterModel.create({ email, password: hashedPassword });
|
||||
const link = `${BASE_URL}/api/auth/confirm_email?code=${code}`;
|
||||
|
||||
setImmediate(() => {
|
||||
const emailData = EmailService.getEmailServerInfo('confirm', { target: email, link: `https://dashboard.litlyx.com/api/auth/confirm_email?register_code=${jwt}` });
|
||||
EmailServiceHelper.sendEmail(emailData);
|
||||
const tRpc = useTRPC();
|
||||
tRpc.emails.email.sendConfirmEmail.mutate({ email, link });
|
||||
});
|
||||
|
||||
return {
|
||||
error: false,
|
||||
message: 'OK'
|
||||
}
|
||||
|
||||
});
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
@@ -4,22 +4,22 @@ import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit, domain } = data;
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics','flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `browsers:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$browser", count: { $sum: 1, } } },
|
||||
|
||||
33
dashboard/server/api/data/cities.ts
Normal file
33
dashboard/server/api/data/cities.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics','flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `cities:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: { city: "$city", region: "$region", country: "$country" }, count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
33
dashboard/server/api/data/continents.ts
Normal file
33
dashboard/server/api/data/continents.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics','flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `continents:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$continent", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
|
||||
const data = await getRequestData(event, ['DOMAIN', 'RANGE', 'SCHEMA'], ['WEB']);
|
||||
if (!data) return;
|
||||
|
||||
const { schemaName, pid, from, to, model, project_id, domain } = data;
|
||||
|
||||
const cacheKey = `count:${schemaName}:${pid}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 20;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
|
||||
const result = await model.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain
|
||||
}
|
||||
},
|
||||
{ $count: 'count' },
|
||||
]);
|
||||
|
||||
return result;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -3,23 +3,22 @@ import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit, domain } = data;
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics', 'flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `countries:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$country", count: { $sum: 1, } } },
|
||||
|
||||
@@ -4,22 +4,22 @@ import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit, domain } = data;
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics', 'flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `devices:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$device", count: { $sum: 1, } } },
|
||||
|
||||
36
dashboard/server/api/data/entry_pages.ts
Normal file
36
dashboard/server/api/data/entry_pages.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics', 'flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `entry_pages:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
...websiteMatch
|
||||
},
|
||||
},
|
||||
{ $sort: { session: 1, created_at: 1 } },
|
||||
{ $group: { _id: "$session", entryPage: { $first: "$page" } } },
|
||||
{ $group: { _id: "$entryPage", count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
37
dashboard/server/api/data/event_metadata_analyze.ts
Normal file
37
dashboard/server/api/data/event_metadata_analyze.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { EventModel } from "~/shared/schema/metrics/EventSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const { event_name, field_name } = getQuery(event);
|
||||
|
||||
if (!event_name || typeof event_name !== 'string') throw createError({ status: 400, message: 'event_name is required' });
|
||||
if (!field_name || typeof field_name !== 'string') throw createError({ status: 400, message: 'field_name is required' });
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'permission:webAnalytics');
|
||||
const { project_id, domain, from, to } = ctx;
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const aggregation = [
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
},
|
||||
name: event_name,
|
||||
...websiteMatch,
|
||||
$expr: { $eq: [{ $type: "$metadata" }, "object"] }
|
||||
}
|
||||
},
|
||||
{ $group: { _id: `$metadata.${field_name}`, count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
]
|
||||
|
||||
const result = await EventModel.aggregate(aggregation as any);
|
||||
|
||||
return result as { _id: string, count: number }[]
|
||||
|
||||
});
|
||||
38
dashboard/server/api/data/event_metadata_fields.ts
Normal file
38
dashboard/server/api/data/event_metadata_fields.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { EventModel } from "~/shared/schema/metrics/EventSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const { event_name } = getQuery(event);
|
||||
|
||||
if (!event_name || typeof event_name !== 'string') throw createError({ status: 400, message: 'event_name is required' });
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'permission:webAnalytics');
|
||||
const { project_id, domain, from, to } = ctx;
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const aggregation = [
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
},
|
||||
name: event_name,
|
||||
...websiteMatch,
|
||||
$expr: { $eq: [{ $type: "$metadata" }, "object"] }
|
||||
}
|
||||
},
|
||||
{ $project: { metadataKeys: { $objectToArray: "$metadata" } } },
|
||||
{ $unwind: "$metadataKeys" },
|
||||
{ $group: { _id: "result", uniqueFields: { $addToSet: "$metadataKeys.k" } } }
|
||||
]
|
||||
|
||||
const events = await EventModel.aggregate(aggregation);
|
||||
|
||||
if (!events[0]) return [];
|
||||
return events[0].uniqueFields as string[];
|
||||
|
||||
});
|
||||
85
dashboard/server/api/data/event_user_flow.ts
Normal file
85
dashboard/server/api/data/event_user_flow.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { EventModel } from "~/shared/schema/metrics/EventSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const { event_name } = getQuery(event);
|
||||
|
||||
if (!event_name || typeof event_name !== 'string') throw createError({ status: 400, message: 'event_name is required' });
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'permission:webAnalytics');
|
||||
const { project_id, domain, from, to } = ctx;
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const aggregation = [
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
},
|
||||
name: event_name,
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: "$name",
|
||||
flowHash: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "visits",
|
||||
let: {
|
||||
flowHash: "$flowHash"
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{
|
||||
$eq: [
|
||||
"$project_id", project_id
|
||||
]
|
||||
},
|
||||
{
|
||||
$eq: ["$flowHash", "$$flowHash"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
referrer: 1
|
||||
}
|
||||
}
|
||||
],
|
||||
as: "visitInfo"
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: "$visitInfo"
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$visitInfo.referrer",
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
const result = await EventModel.aggregate(aggregation);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
@@ -4,22 +4,23 @@ import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, ['DOMAIN', 'RANGE'], ['EVENTS']);
|
||||
if (!data) return;
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:events');
|
||||
|
||||
const { pid, from, to, project_id, limit, domain } = data;
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `events:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 20;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await EventModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$name", count: { $sum: 1, } } },
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id, from, to } = data;
|
||||
|
||||
const { name: eventName } = getQuery(event);
|
||||
|
||||
if (!eventName) return setResponseStatus(event, 400, 'name is required');
|
||||
|
||||
const allEvents = await EventModel.find({
|
||||
project_id: project_id,
|
||||
name: eventName,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to),
|
||||
}
|
||||
}, { flowHash: 1 });
|
||||
|
||||
|
||||
const allFlowHashes = new Map<string, number>();
|
||||
|
||||
allEvents.forEach(e => {
|
||||
if (!e.flowHash) return;
|
||||
if (e.flowHash.length == 0) return;
|
||||
if (allFlowHashes.has(e.flowHash)) {
|
||||
const count = allFlowHashes.get(e.flowHash) as number;
|
||||
allFlowHashes.set(e.flowHash, count + 1);
|
||||
} else {
|
||||
allFlowHashes.set(e.flowHash, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const flowHashIds = Array.from(allFlowHashes.keys());
|
||||
|
||||
const allReferrers: { referrer: string, flowHash: string }[] = [];
|
||||
|
||||
const promises: any[] = [];
|
||||
while (flowHashIds.length > 0) {
|
||||
promises.push(new Promise<void>(async resolve => {
|
||||
const flowHashIdsChunk = flowHashIds.splice(0, 10);
|
||||
const visits = await VisitModel.find({ project_id, flowHash: { $in: flowHashIdsChunk } }, { referrer: 1, flowHash: 1 });
|
||||
allReferrers.push(...visits.map(e => { return { referrer: e.referrer, flowHash: e.flowHash } }));
|
||||
resolve();
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const groupedFlows: Record<string, { referrers: string[] }> = {};
|
||||
|
||||
flowHashIds.forEach(flowHash => {
|
||||
if (!groupedFlows[flowHash]) groupedFlows[flowHash] = { referrers: [] };
|
||||
const target = groupedFlows[flowHash];
|
||||
if (!target) return;
|
||||
const referrers = allReferrers.filter(e => e.flowHash === flowHash).map(e => e.referrer);
|
||||
for (const referrer of referrers) {
|
||||
if (target.referrers.includes(referrer)) continue;
|
||||
target.referrers.push(referrer);
|
||||
}
|
||||
});
|
||||
|
||||
const grouped: Record<string, number> = {};
|
||||
|
||||
for (const referrerPlusHash of allReferrers) {
|
||||
const referrer = referrerPlusHash.referrer;
|
||||
if (!grouped[referrer]) grouped[referrer] = 0
|
||||
grouped[referrer]++;
|
||||
}
|
||||
|
||||
|
||||
const eventsCount = allEvents.length;
|
||||
|
||||
const allGroupedValue = Object.keys(grouped)
|
||||
.map(key => grouped[key])
|
||||
.reduce((a, e) => a + e, 0);
|
||||
|
||||
for (const key in grouped) {
|
||||
grouped[key] = 100 / allGroupedValue * grouped[key];
|
||||
}
|
||||
|
||||
return grouped;
|
||||
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { EVENT_METADATA_FIELDS_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
import { PipelineStage } from "mongoose";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
|
||||
const { name: eventName, field, from, to } = getQuery(event);
|
||||
|
||||
if (!from) return setResponseStatus(event, 400, 'from is required');
|
||||
if (!to) return setResponseStatus(event, 400, 'to is required');
|
||||
if (!eventName) return setResponseStatus(event, 400, 'name is required');
|
||||
if (!field) return setResponseStatus(event, 400, 'field is required');
|
||||
|
||||
|
||||
const aggregation: PipelineStage[] = [
|
||||
{
|
||||
$match: {
|
||||
project_id, name: eventName,
|
||||
created_at: {
|
||||
$gte: new Date(from.toString()),
|
||||
$lte: new Date(to.toString()),
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $group: { _id: `$metadata.${field}`, count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]
|
||||
|
||||
const metadataGrouped = await EventModel.aggregate(aggregation);
|
||||
|
||||
return metadataGrouped;
|
||||
|
||||
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const { name: eventName } = getQuery(event);
|
||||
if (!eventName) return [];
|
||||
|
||||
const fields: string[] = await Redis.useCache({
|
||||
key: `metadata_fields:${project_id}:${eventName}`,
|
||||
exp: 60
|
||||
}, async () => {
|
||||
const eventsWithName = await EventModel.find({ project_id, name: eventName }, { metadata: 1 }, { limit: 10, sort: { created_at: -1 } });
|
||||
const allMetadata = eventsWithName.map(e => e.metadata);
|
||||
const allFields = new Set<string>();
|
||||
for (const metadata of allMetadata) {
|
||||
const keys = Object.keys(metadata || {});
|
||||
keys.forEach(key => allFields.add(key));
|
||||
}
|
||||
return Array.from(allFields.values());
|
||||
});
|
||||
|
||||
return fields;
|
||||
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const names: string[] = await Redis.useCache({
|
||||
key: `event_names:${project_id}`,
|
||||
exp: 60
|
||||
}, async () => {
|
||||
const namesAggregation = await EventModel.aggregate([
|
||||
{ $match: { project_id } },
|
||||
{ $group: { _id: "$name" } }
|
||||
]);
|
||||
return namesAggregation.map(e => e._id);
|
||||
});
|
||||
|
||||
return names;
|
||||
|
||||
});
|
||||
36
dashboard/server/api/data/exit_pages.ts
Normal file
36
dashboard/server/api/data/exit_pages.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics','flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `exit_pages:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
...websiteMatch
|
||||
},
|
||||
},
|
||||
{ $sort: { session: 1, created_at: 1 } },
|
||||
{ $group: { _id: "$session", exitPage: { $last: "$page" } } },
|
||||
{ $group: { _id: "$exitPage", count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestDataOld(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const online_users = await SessionModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
updated_at: { $gt: new Date(Date.now() - 1000 * 60 * 5) }
|
||||
}
|
||||
},
|
||||
{ $count: 'count' }
|
||||
]);
|
||||
|
||||
if (!online_users[0]) return 0;
|
||||
|
||||
return online_users[0].count;
|
||||
|
||||
});
|
||||
@@ -4,22 +4,22 @@ import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit, domain } = data;
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics','flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `oss:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$os", count: { $sum: 1, } } },
|
||||
|
||||
@@ -4,29 +4,29 @@ import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, ['RANGE', 'DOMAIN'], ['WEB']);
|
||||
if (!data) return;
|
||||
|
||||
const { pid, from, to, project_id, limit, domain } = data;
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics', 'flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `pages:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain
|
||||
...websiteMatch
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$page", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
76
dashboard/server/api/data/pages_durations.ts
Normal file
76
dashboard/server/api/data/pages_durations.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics', 'flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `pages_durations:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{
|
||||
$setWindowFields: {
|
||||
partitionBy: "$session",
|
||||
sortBy: { created_at: 1 },
|
||||
output: {
|
||||
nextCreatedAt: { $shift: { output: "$created_at", by: 1 } },
|
||||
nextPage: { $shift: { output: "$page", by: 1 } }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
page: 1,
|
||||
created_at: 1,
|
||||
nextCreatedAt: 1,
|
||||
durationMs: {
|
||||
$cond: [
|
||||
{ $ne: ["$nextCreatedAt", null] },
|
||||
{ $subtract: ["$nextCreatedAt", "$created_at"] },
|
||||
null
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
durationMs: { $ne: null, $gt: 0, $lte: 1000 * 60 * 60 * 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$page",
|
||||
count: { $sum: 1 },
|
||||
avgMs: { $avg: "$durationMs" }
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
count: 1,
|
||||
avgSeconds: { $round: [{ $divide: ["$avgMs", 1000] }, 0] },
|
||||
}
|
||||
},
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit },
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,25 +1,24 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, ['OFFSET', 'RANGE', 'DOMAIN'], ['WEB']);
|
||||
if (!data) return;
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics', 'flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const { pid, from, to, project_id, limit, domain } = data;
|
||||
|
||||
const cacheKey = `referrers:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheKey = `data:referrers:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
website: domain,
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: "$referrer", count: { $sum: 1, } } },
|
||||
@@ -32,4 +31,6 @@ export default defineEventHandler(async event => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
33
dashboard/server/api/data/regions.ts
Normal file
33
dashboard/server/api/data/regions.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics','flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const cacheKey = `regions:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
...websiteMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: { region: "$region", country: "$country" }, count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
44
dashboard/server/api/data/utm.ts
Normal file
44
dashboard/server/api/data/utm.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'domain', 'range', 'limit', 'permission:webAnalytics', 'flag:allowShare');
|
||||
const { pid, project_id, domain, from, to, limit } = ctx;
|
||||
|
||||
const { utm_type } = getQuery(event);
|
||||
|
||||
const cacheKey = `data:utm:${utm_type}:${pid}:${limit}:${from}:${to}:${domain}`;
|
||||
const cacheExp = 60;
|
||||
|
||||
return await Redis.useCache(cacheKey, cacheExp, async () => {
|
||||
|
||||
const websiteMatch = domain ? { website: domain } : {};
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||
...websiteMatch,
|
||||
[`utm_${utm_type}`]: { $ne: null }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: `$utm_${utm_type}`,
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
|
||||
return result.map(item => ({
|
||||
_id: item._id ?? 'unknown',
|
||||
count: item.count
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
56
dashboard/server/api/domains/delete_data.post.ts
Normal file
56
dashboard/server/api/domains/delete_data.post.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Types } from "mongoose";
|
||||
import { z } from "zod";
|
||||
import { ProjectCountModel } from "~/shared/schema/project/ProjectsCounts";
|
||||
|
||||
|
||||
const ZDeleteDataBody = z.object({
|
||||
domain: z.string(),
|
||||
visits: z.boolean(),
|
||||
events: z.boolean()
|
||||
});
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
|
||||
const { project_id } = ctx;
|
||||
|
||||
const { domain, visits, events } = await readValidatedBody(event, ZDeleteDataBody.parse);
|
||||
|
||||
taskDeleteDomain(project_id, domain, visits, events);
|
||||
|
||||
return { ok: true }
|
||||
|
||||
});
|
||||
|
||||
|
||||
async function taskDeleteDomain(project_id: Types.ObjectId, domain: string, deleteVisits: boolean, deleteEvents: boolean) {
|
||||
|
||||
console.log('Deletation started', project_id.toString(), { domain, deleteVisits, deleteEvents });
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
if (deleteVisits) {
|
||||
const deleteVisits = await VisitModel.deleteMany({ project_id, website: domain });
|
||||
console.log('Visits deleted', deleteVisits.deletedCount);
|
||||
}
|
||||
|
||||
if (deleteEvents === true) {
|
||||
const deleteEvents = await EventModel.deleteMany({ project_id, website: domain });
|
||||
console.log('Events deleted', deleteEvents.deletedCount);
|
||||
}
|
||||
|
||||
// Refresh count
|
||||
|
||||
const events = await EventModel.countDocuments({ project_id });
|
||||
const visits = await VisitModel.countDocuments({ project_id });
|
||||
await ProjectCountModel.updateOne({ project_id, events, visits }, {}, { upsert: true });
|
||||
|
||||
const s = (Date.now() - start) / 1000;
|
||||
|
||||
console.log(`Deletation done in ${s.toFixed(2)} seconds`);
|
||||
|
||||
}
|
||||
@@ -1,45 +1,76 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { EventModel } from "~/shared/schema/metrics/EventSchema";
|
||||
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||
|
||||
|
||||
export type TDomainSimpleRes = { _id: string, name: string }
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, []);
|
||||
if (!data) return;
|
||||
const ctx = await getRequestContext(event, 'pid', 'permission:member');
|
||||
|
||||
const { project_id, project, user } = data;
|
||||
const { project_id, project, user_id, user_email } = ctx;
|
||||
|
||||
const result: { _id: string, visits: number }[] = await VisitModel.aggregate([
|
||||
{ $match: { project_id, } },
|
||||
{ $group: { _id: "$website", visits: { $sum: 1 } } },
|
||||
const result_visits: TDomainSimpleRes[] = await VisitModel.aggregate([
|
||||
{ $match: { project_id } },
|
||||
{ $group: { _id: "$website" } },
|
||||
{ $project: { _id: 0, name: "$_id" } }
|
||||
]);
|
||||
|
||||
const isOwner = user.id === project.owner.toString();
|
||||
const result_events: TDomainSimpleRes[] = await EventModel.aggregate([
|
||||
{ $match: { project_id } },
|
||||
{ $group: { _id: "$website" } },
|
||||
{ $project: { _id: 0, name: "$_id" } }
|
||||
]);
|
||||
|
||||
|
||||
const result: TDomainSimpleRes[] = result_visits;
|
||||
|
||||
result_events.forEach(e => {
|
||||
if (result.find(e => e.name === e.name)) return;
|
||||
result.push(e);
|
||||
});
|
||||
|
||||
const isOwner = user_id === project.owner.toString();
|
||||
if (isOwner) return [
|
||||
{
|
||||
_id: 'All domains',
|
||||
visits: result.reduce((a, e) => a + e.visits, 0)
|
||||
_id: '*',
|
||||
name: 'All domains',
|
||||
},
|
||||
...result
|
||||
]
|
||||
...result.map(e => ({ ...e, _id: e.name }))
|
||||
] as TDomainSimpleRes[];
|
||||
|
||||
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')) {
|
||||
//TODO: Create admin list
|
||||
if (user_email !== 'helplitlyx@gmail.com') {
|
||||
const member = await TeamMemberModel.findOne({ project_id, $or: [{ user_id }, { email: user_email }], 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('*')) {
|
||||
return [
|
||||
{
|
||||
_id: '*',
|
||||
name: 'All domains'
|
||||
},
|
||||
...result
|
||||
] as TDomainSimpleRes[];
|
||||
}
|
||||
|
||||
return result.filter(e => {
|
||||
return member.permission.domains.includes(e.name);
|
||||
}).map(e => ({ ...e, _id: e.name })) as TDomainSimpleRes[];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
_id: 'All domains',
|
||||
visits: result.reduce((a, e) => a + e.visits, 0)
|
||||
_id: '*',
|
||||
name: 'All domains',
|
||||
},
|
||||
...result
|
||||
]
|
||||
...result.map(e => ({ ...e, _id: e.name }))
|
||||
] as TDomainSimpleRes[];
|
||||
}
|
||||
|
||||
return result.filter(e => {
|
||||
return member.permission.domains.includes(e._id);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
63
dashboard/server/api/domains/list_count.ts
Normal file
63
dashboard/server/api/domains/list_count.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { EventModel } from "~/shared/schema/metrics/EventSchema";
|
||||
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||
|
||||
|
||||
export type TDomainRes = { _id: string, name: string, visits: number }
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
|
||||
const { project_id, project, user_id, user_email } = ctx;
|
||||
|
||||
const result_visits = await VisitModel.aggregate([
|
||||
{ $match: { project_id, } },
|
||||
{ $group: { _id: "$website", visits: { $sum: 1 } } },
|
||||
{ $addFields: { name: '$_id' } }
|
||||
]);
|
||||
const result_events = await EventModel.aggregate([
|
||||
{ $match: { project_id, } },
|
||||
{ $group: { _id: "$website", visits: { $sum: 1 } } },
|
||||
{ $addFields: { name: '$_id' } }
|
||||
]);
|
||||
|
||||
const result: TDomainRes[] = result_visits;
|
||||
|
||||
result_events.forEach(e => {
|
||||
if (result.find(e => e.name === e.name)) return;
|
||||
result.push(e);
|
||||
});
|
||||
|
||||
const isOwner = user_id === project.owner.toString();
|
||||
if (isOwner) return [
|
||||
{
|
||||
_id: '*',
|
||||
name: 'All domains',
|
||||
visits: result.reduce((a, e) => a + e.visits, 0)
|
||||
},
|
||||
...result
|
||||
] as TDomainRes[];
|
||||
|
||||
const member = await TeamMemberModel.findOne({ project_id, $or: [{ user_id }, { email: user_email }], 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('*')) {
|
||||
return [
|
||||
{
|
||||
_id: '*',
|
||||
name: 'All domains',
|
||||
visits: result.reduce((a, e) => a + e.visits, 0)
|
||||
},
|
||||
...result
|
||||
] as TDomainRes[];
|
||||
}
|
||||
|
||||
return result.filter(e => {
|
||||
return member.permission.domains.includes(e._id);
|
||||
}) as TDomainRes[];
|
||||
|
||||
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
import { FeedbackModel } from '@schema/FeedbackSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const data = await getRequestDataOld(event);
|
||||
if (!data) return;
|
||||
|
||||
const { text } = await readBody(event);
|
||||
|
||||
const save = await FeedbackModel.create({
|
||||
user_id: data.user.id,
|
||||
project_id: data.project_id,
|
||||
text
|
||||
});
|
||||
|
||||
return { ok: true }
|
||||
|
||||
});
|
||||
18
dashboard/server/api/feedback/send.post.ts
Normal file
18
dashboard/server/api/feedback/send.post.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FeedbackModel } from "~/shared/schema/FeedbackSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
if (isSelfhosted()) return;
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id, user_id } = ctx;
|
||||
|
||||
const { text } = await readBody(event);
|
||||
|
||||
if (!text || typeof text != 'string' || text.length == 0) {
|
||||
throw createError({ status: 400, message: 'Something went wrong.' })
|
||||
}
|
||||
|
||||
await FeedbackModel.create({ project_id, user_id, text })
|
||||
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { ApiSettingsModel, TApiSettings } from "@schema/ApiSettingsSchema";
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
function generateApiKey() {
|
||||
return 'lit_' + crypto.randomBytes(6).toString('hex');
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
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.trim(), created_at: Date.now(), usage: 0 });
|
||||
|
||||
return newApiSettings.toJSON();
|
||||
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
import { ApiSettingsModel } from "@schema/ApiSettingsSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
const data = await getRequestDataOld(event, { allowGuests: false, allowLitlyx: false, });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const deletation = await ApiSettingsModel.deleteOne({ project_id, _id: body.api_id });
|
||||
return { ok: deletation.acknowledged };
|
||||
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
import { ApiSettingsModel, TApiSettings } from "@schema/ApiSettingsSchema";
|
||||
|
||||
function cryptApiKeyName(apiSettings: TApiSettings): TApiSettings {
|
||||
return { ...apiSettings, apiKey: apiSettings.apiKey.substring(0, 6) + '******' }
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestDataOld(event, { allowGuests: false, allowLitlyx: false, requireRange: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const apiKeys = await ApiSettingsModel.find({ project_id }, { project_id: 0 })
|
||||
return apiKeys.map(e => cryptApiKeyName(e.toJSON())) as TApiSettings[];
|
||||
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const liveDemoProject = await ProjectModel.findById('6643cd08a1854e3b81722ab5');
|
||||
if (!liveDemoProject) return;
|
||||
return liveDemoProject.toJSON() as TProject;
|
||||
});
|
||||
26
dashboard/server/api/members/accept.post.ts
Normal file
26
dashboard/server/api/members/accept.post.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'flag:allowAnonRegistered');
|
||||
const { user_id, user_email } = ctx;
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
const { project_id } = body;
|
||||
if (!project_id) throw createError({ status: 400, message: 'project_id is required' });
|
||||
|
||||
const member = await TeamMemberModel.findOne({
|
||||
project_id, $or: [
|
||||
{ user_id },
|
||||
{ email: user_email }
|
||||
]
|
||||
});
|
||||
if (!member) throw createError({ status: 400, message: 'Member not found' });
|
||||
|
||||
member.pending = false;
|
||||
await member.save();
|
||||
|
||||
return { ok: true };
|
||||
|
||||
});
|
||||
94
dashboard/server/api/members/add.post.ts
Normal file
94
dashboard/server/api/members/add.post.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { z } from "zod";
|
||||
import { getPlanFromId } from "~/shared/data/PLANS";
|
||||
import { PremiumModel } from "~/shared/schema/PremiumSchema";
|
||||
|
||||
const ZEmailBody = z.object({
|
||||
email: z.string().email('Not a valid email')
|
||||
});
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id, user_id, project } = ctx;
|
||||
|
||||
const { email } = await readValidatedBody(event, ZEmailBody.parse);
|
||||
|
||||
const { BASE_URL } = useRuntimeConfig();
|
||||
|
||||
const link = `${BASE_URL}/accept_invite?project_id=${project_id.toString()}`;
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ user_id });
|
||||
if (!premiumData) throw createError({ status: 400, message: 'Error getting premiumData. Please contact support.' });
|
||||
|
||||
const price = getPlanFromId(premiumData.premium_type);
|
||||
if (!price) throw createError({ status: 400, message: 'Error getting price. Please contact support.' });
|
||||
|
||||
const maxMembers = price.features.members;
|
||||
const currentMembers = await TeamMemberModel.countDocuments({ project_id });
|
||||
|
||||
if (currentMembers >= maxMembers) throw createError({ status: 400, message: 'MEMBERS_LIMIT_REACHED' });
|
||||
|
||||
const targetUser = await UserModel.findOne({ email });
|
||||
|
||||
if (targetUser) {
|
||||
|
||||
if (targetUser._id.toString() == user_id) throw createError({ status: 400, message: 'Cannot invite yourself' });
|
||||
|
||||
const exists = await TeamMemberModel.exists({ project_id, user_id });
|
||||
if (exists) throw createError({ status: 400, message: 'Member already invited' });
|
||||
|
||||
await TeamMemberModel.create({
|
||||
project_id,
|
||||
user_id: targetUser.id,
|
||||
pending: true,
|
||||
role: 'GUEST',
|
||||
permission: {
|
||||
webAnalytics: true,
|
||||
events: false,
|
||||
ai: false,
|
||||
domains: ['*']
|
||||
}
|
||||
});
|
||||
|
||||
setImmediate(() => {
|
||||
const tRpc = useTRPC();
|
||||
tRpc.emails.email.sendInviteEmail.mutate({ email, project_name: project.name, link });
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
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',
|
||||
permission: {
|
||||
webAnalytics: true,
|
||||
events: false,
|
||||
ai: false,
|
||||
domains: ['*']
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
setImmediate(() => {
|
||||
const tRpc = useTRPC();
|
||||
tRpc.emails.email.sendInviteEmail.mutate({ email, project_name: project.name, link });
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return { ok: true };
|
||||
|
||||
|
||||
});
|
||||
25
dashboard/server/api/members/decline.post.ts
Normal file
25
dashboard/server/api/members/decline.post.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'flag:allowAnonRegistered');
|
||||
const { user_id, user_email } = ctx;
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
const { project_id } = body;
|
||||
if (!project_id) throw createError({ status: 400, message: 'project_id is required' });
|
||||
|
||||
const member = await TeamMemberModel.deleteOne({
|
||||
project_id,
|
||||
$or: [
|
||||
{ user_id },
|
||||
{ email: user_email }
|
||||
]
|
||||
});
|
||||
|
||||
if (!member) return setResponseStatus(event, 400, 'Member not found');
|
||||
|
||||
return { ok: true };
|
||||
|
||||
});
|
||||
@@ -1,15 +1,19 @@
|
||||
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, [], ['OWNER']);
|
||||
if (!data) return [];
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
|
||||
const { user_id, project } = ctx;
|
||||
|
||||
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 isOwner = user_id === project.owner.toString();
|
||||
if (!isOwner) throw createError({ status: 403, message: 'Only owner can change roles' })
|
||||
|
||||
if (!member_id) return setResponseStatus(event, 400, 'member_id is required');
|
||||
|
||||
const edited = await TeamMemberModel.updateOne({ _id: member_id }, {
|
||||
permission: {
|
||||
@@ -5,17 +5,17 @@ import { UserModel } from "@schema/UserSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, [], ['OWNER']);
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id } = ctx;
|
||||
|
||||
const { email } = await readBody(event);
|
||||
|
||||
const user = await UserModel.findOne({ email });
|
||||
if (!user) return setResponseStatus(event, 400, 'Email not found');
|
||||
|
||||
await TeamMemberModel.deleteOne({ project_id, user_id: user.id });
|
||||
if (user) {
|
||||
await TeamMemberModel.deleteOne({ project_id, user_id: user.id });
|
||||
} else {
|
||||
await TeamMemberModel.deleteOne({ project_id, email: email });
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
|
||||
7
dashboard/server/api/members/leave.ts
Normal file
7
dashboard/server/api/members/leave.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const ctx = await getRequestContext(event, 'pid', 'permission:member');
|
||||
const { project_id, user_id } = ctx;
|
||||
await TeamMemberModel.deleteOne({ project_id, user_id });
|
||||
});
|
||||
83
dashboard/server/api/members/list.ts
Normal file
83
dashboard/server/api/members/list.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { Redis } from "~/server/services/CacheService";
|
||||
import { executeTimelineAggregation } from "~/server/services/TimelineService";
|
||||
import { TeamMemberModel, TeamMemberRole, TPermission } from "~/shared/schema/TeamMemberSchema";
|
||||
import { TUser, UserModel } from "~/shared/schema/UserSchema";
|
||||
|
||||
|
||||
export type MemberWithPermissions = {
|
||||
id: string | null,
|
||||
email: string,
|
||||
role: TeamMemberRole,
|
||||
pending: boolean,
|
||||
me: boolean,
|
||||
permission: TPermission
|
||||
}
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { project_id, user_id, user_email, project } = ctx;
|
||||
|
||||
const result: MemberWithPermissions[] = [];
|
||||
|
||||
const members = await TeamMemberModel.find({ project_id });
|
||||
|
||||
result.push({
|
||||
id: user_id,
|
||||
email: user_email,
|
||||
role: 'OWNER',
|
||||
me: user_id === project.owner.toString(),
|
||||
pending: false,
|
||||
permission: {
|
||||
webAnalytics: true,
|
||||
domains: ['*'],
|
||||
ai: true,
|
||||
events: true
|
||||
}
|
||||
})
|
||||
|
||||
for (const member of members) {
|
||||
|
||||
let userMember: TUser | null;
|
||||
|
||||
if (member.user_id) {
|
||||
userMember = await UserModel.findOne({ _id: member.user_id });
|
||||
} else {
|
||||
userMember = await UserModel.findOne({ email: member.email });
|
||||
}
|
||||
|
||||
const permission: TPermission = {
|
||||
webAnalytics: member.permission?.webAnalytics || false,
|
||||
events: member.permission?.events || false,
|
||||
ai: member.permission?.ai || false,
|
||||
domains: member.permission?.domains || []
|
||||
}
|
||||
|
||||
if (userMember) {
|
||||
result.push({
|
||||
id: member.id,
|
||||
email: userMember.email || member.email || 'NO_EMAIL',
|
||||
role: member.role,
|
||||
pending: member.pending,
|
||||
me: user_id === ((userMember as any).id.toString() || member.user_id || 'NO_ID'),
|
||||
permission
|
||||
})
|
||||
} else {
|
||||
result.push({
|
||||
id: member.id,
|
||||
email: member.email ?? 'error',
|
||||
role: member.role,
|
||||
pending: member.pending,
|
||||
me: false,
|
||||
permission: member.permission
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
});
|
||||
25
dashboard/server/api/members/me.ts
Normal file
25
dashboard/server/api/members/me.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const ctx = await getRequestContext(event, 'pid', 'permission:member');
|
||||
const { project_id, user_id, user_email, project } = ctx;
|
||||
|
||||
if (project.owner.toString() === user_id) {
|
||||
return { webAnalytics: true, domains: ['*'], ai: true, events: true }
|
||||
}
|
||||
|
||||
//TODO: Create admin list
|
||||
if (user_email === 'helplitlyx@gmail.com') {
|
||||
return { webAnalytics: true, domains: ['*'], ai: true, events: true }
|
||||
}
|
||||
|
||||
const meUserId = await TeamMemberModel.findOne({ project_id, user_id });
|
||||
if (meUserId) return meUserId.permission;
|
||||
|
||||
const meEmail = await TeamMemberModel.findOne({ project_id, email: user_email });
|
||||
if (meEmail) return meEmail.permission;
|
||||
|
||||
return { webAnalytics: false, domains: [], ai: false, events: false }
|
||||
|
||||
});
|
||||
@@ -2,19 +2,28 @@
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
export type TPendingInvite = {
|
||||
_id: string,
|
||||
project_id: string,
|
||||
user_id: string,
|
||||
role: string,
|
||||
pending: boolean,
|
||||
creted_at: string,
|
||||
project_name: string
|
||||
};
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event);
|
||||
if (!data) return;
|
||||
const ctx = await getRequestContext(event, 'pid');
|
||||
const { user_id, user_email } = ctx;
|
||||
|
||||
const members = await TeamMemberModel.aggregate([
|
||||
{
|
||||
$match:
|
||||
{
|
||||
$or: [
|
||||
{ user_id: new Types.ObjectId(data.user.id) },
|
||||
{ email: data.user.user.email }
|
||||
{ user_id: new Types.ObjectId(user_id) },
|
||||
{ email: user_email }
|
||||
],
|
||||
pending: true
|
||||
}
|
||||
@@ -38,7 +47,7 @@ export default defineEventHandler(async event => {
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
return members;
|
||||
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { ProjectCountModel } from "@schema/project/ProjectsCounts";
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
import { COUNTS_EXPIRE_TIME, COUNTS_SESSIONS_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
|
||||
export type MetricsCounts = {
|
||||
eventsCount: number,
|
||||
visitsCount: number,
|
||||
sessionsVisitsCount: number,
|
||||
firstEventDate: number,
|
||||
firstViewDate: number,
|
||||
avgSessionDuration: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `counts:${project_id}`,
|
||||
exp: COUNTS_EXPIRE_TIME
|
||||
}, async () => {
|
||||
|
||||
|
||||
const count: { events: number, visits: number }[] = await ProjectCountModel.aggregate([
|
||||
{ $match: { project_id: project._id } },
|
||||
{
|
||||
$group: {
|
||||
_id: "$project_id",
|
||||
events: { $sum: "$events" },
|
||||
visits: { $sum: "$visits" },
|
||||
sessions: { $sum: "$sessions" },
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const sessionsVisitsCount: any[] = await Redis.useCache({
|
||||
key: `counts:${project_id}:sessions_count`,
|
||||
exp: COUNTS_SESSIONS_EXPIRE_TIME
|
||||
}, async () => {
|
||||
return await SessionModel.aggregate([
|
||||
{ $match: { project_id: project._id } },
|
||||
{ $group: { _id: "$session", time: { $sum: '$duration' }, count: { $sum: 1 } } },
|
||||
])
|
||||
});
|
||||
|
||||
const totalSessions = sessionsVisitsCount.length;
|
||||
const totalSessionsTime = sessionsVisitsCount.reduce((a, e) => a + e.time, 0);
|
||||
const avgSessionDuration = totalSessionsTime / totalSessions;
|
||||
|
||||
const firstEvent = await EventModel.findOne({ project_id: project._id }, { created_at: 1 });
|
||||
const firstView = await VisitModel.findOne({ project_id: project._id }, { created_at: 1 });
|
||||
|
||||
return {
|
||||
eventsCount: count[0].events,
|
||||
visitsCount: count[0].visits,
|
||||
sessionsVisitsCount: totalSessions || 0,
|
||||
avgSessionDuration,
|
||||
firstEventDate: firstEvent?.created_at.getTime() || Date.now(),
|
||||
firstViewDate: firstView?.created_at.getTime() || Date.now(),
|
||||
} as MetricsCounts;
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export type BrowsersAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
|
||||
const limit = getRequestHeader(event, 'x-query-limit');
|
||||
const numLimit = parseInt(limit || '10');
|
||||
|
||||
|
||||
const from = getRequestHeader(event, 'x-from');
|
||||
const to = getRequestHeader(event, 'x-to');
|
||||
|
||||
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `browsers:${project_id}:${numLimit}:${from}:${to}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
const browsers: BrowsersAggregated[] = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
}
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$browser", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: numLimit }
|
||||
]);
|
||||
|
||||
return browsers;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export type CountriesAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const limit = getRequestHeader(event, 'x-query-limit');
|
||||
const numLimit = parseInt(limit || '10');
|
||||
|
||||
const from = getRequestHeader(event, 'x-from');
|
||||
const to = getRequestHeader(event, 'x-to');
|
||||
|
||||
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `countries:${project_id}:${numLimit}:${from}:${to}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
const countries: CountriesAggregated[] = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id,
|
||||
country: { $ne: null },
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
}
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$country", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: numLimit }
|
||||
]);
|
||||
|
||||
return countries;
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export type DevicesAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const limit = getRequestHeader(event, 'x-query-limit');
|
||||
const numLimit = parseInt(limit || '10');
|
||||
|
||||
const from = getRequestHeader(event, 'x-from');
|
||||
const to = getRequestHeader(event, 'x-to');
|
||||
|
||||
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `devices:${project_id}:${numLimit}:${from}:${to}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
const devices: DevicesAggregated[] = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id,
|
||||
device: { $ne: null },
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
}
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$device", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: numLimit }
|
||||
]);
|
||||
|
||||
return devices;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
export type CustomEventsAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const from = getRequestHeader(event, 'x-from');
|
||||
const to = getRequestHeader(event, 'x-to');
|
||||
|
||||
|
||||
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
|
||||
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `events:${project_id}:${from}:${to}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
|
||||
const events: CustomEventsAggregated[] = await EventModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id, created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
}
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$name", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]);
|
||||
|
||||
return events;
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export type OssAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
|
||||
const limit = getRequestHeader(event, 'x-query-limit');
|
||||
const numLimit = parseInt(limit || '10');
|
||||
|
||||
const from = getRequestHeader(event, 'x-from');
|
||||
const to = getRequestHeader(event, 'x-to');
|
||||
|
||||
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `oss:${project_id}:${numLimit}:${from}:${to}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
const oss: OssAggregated[] = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
}
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$os", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: numLimit }
|
||||
]);
|
||||
|
||||
return oss;
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export type VisitsPageAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
|
||||
const websiteName = getRequestHeader(event, 'x-website-name');
|
||||
if (!websiteName) return [];
|
||||
const limit = getRequestHeader(event, 'x-query-limit');
|
||||
const numLimit = parseInt(limit || '10');
|
||||
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `pages:${project_id}:${websiteName}:${numLimit}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
const pages: VisitsPageAggregated[] = await VisitModel.aggregate([
|
||||
{ $match: { project_id: project._id }, },
|
||||
{ $match: { website: websiteName, }, },
|
||||
{ $group: { _id: "$page", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: numLimit }
|
||||
]);
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export type ReferrersAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
|
||||
const limit = getRequestHeader(event, 'x-query-limit');
|
||||
const numLimit = parseInt(limit || '10');
|
||||
|
||||
const from = getRequestHeader(event, 'x-from');
|
||||
const to = getRequestHeader(event, 'x-to');
|
||||
|
||||
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `referrers:${project_id}:${numLimit}:${from}:${to}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
const referrers: ReferrersAggregated[] = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
}
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$referrer", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: numLimit }
|
||||
]);
|
||||
|
||||
return referrers;
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { DATA_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export type VisitsWebsiteAggregated = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
|
||||
const limit = getRequestHeader(event, 'x-query-limit');
|
||||
const numLimit = parseInt(limit || '10');
|
||||
|
||||
const from = getRequestHeader(event, 'x-from');
|
||||
const to = getRequestHeader(event, 'x-to');
|
||||
|
||||
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `websites:${project_id}:${numLimit}`,
|
||||
exp: DATA_EXPIRE_TIME
|
||||
}, async () => {
|
||||
const websites: VisitsWebsiteAggregated[] = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id,
|
||||
created_at: {
|
||||
$gte: new Date(from),
|
||||
$lte: new Date(to)
|
||||
}
|
||||
},
|
||||
},
|
||||
{ $group: { _id: "$website", count: { $sum: 1, } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: numLimit }
|
||||
]);
|
||||
|
||||
return websites;
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const { name: eventName, from, to } = getQuery(event);
|
||||
|
||||
if (!from) return setResponseStatus(event, 400, 'from is required');
|
||||
if (!to) return setResponseStatus(event, 400, 'to is required');
|
||||
if (!eventName) return setResponseStatus(event, 400, 'name is required');
|
||||
|
||||
|
||||
|
||||
const allEvents = await EventModel.find({
|
||||
project_id: project_id,
|
||||
name: eventName,
|
||||
created_at: {
|
||||
$gte: new Date(from.toString()),
|
||||
$lte: new Date(to.toString()),
|
||||
}
|
||||
}, { flowHash: 1 });
|
||||
|
||||
|
||||
const allFlowHashes = new Map<string, number>();
|
||||
|
||||
allEvents.forEach(e => {
|
||||
if (!e.flowHash) return;
|
||||
if (e.flowHash.length == 0) return;
|
||||
if (allFlowHashes.has(e.flowHash)) {
|
||||
const count = allFlowHashes.get(e.flowHash) as number;
|
||||
allFlowHashes.set(e.flowHash, count + 1);
|
||||
} else {
|
||||
allFlowHashes.set(e.flowHash, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const flowHashIds = Array.from(allFlowHashes.keys());
|
||||
|
||||
const allReferrers: { referrer: string, flowHash: string }[] = [];
|
||||
|
||||
const promises: any[] = [];
|
||||
while (flowHashIds.length > 0) {
|
||||
promises.push(new Promise<void>(async resolve => {
|
||||
const flowHashIdsChunk = flowHashIds.splice(0, 10);
|
||||
const visits = await VisitModel.find({ project_id, flowHash: { $in: flowHashIdsChunk } }, { referrer: 1, flowHash: 1 });
|
||||
allReferrers.push(...visits.map(e => { return { referrer: e.referrer, flowHash: e.flowHash } }));
|
||||
resolve();
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const groupedFlows: Record<string, { referrers: string[] }> = {};
|
||||
|
||||
flowHashIds.forEach(flowHash => {
|
||||
if (!groupedFlows[flowHash]) groupedFlows[flowHash] = { referrers: [] };
|
||||
const target = groupedFlows[flowHash];
|
||||
if (!target) return;
|
||||
const referrers = allReferrers.filter(e => e.flowHash === flowHash).map(e => e.referrer);
|
||||
for (const referrer of referrers) {
|
||||
if (target.referrers.includes(referrer)) continue;
|
||||
target.referrers.push(referrer);
|
||||
}
|
||||
});
|
||||
|
||||
const grouped: Record<string, number> = {};
|
||||
|
||||
for (const referrerPlusHash of allReferrers) {
|
||||
const referrer = referrerPlusHash.referrer;
|
||||
if (!grouped[referrer]) grouped[referrer] = 0
|
||||
grouped[referrer]++;
|
||||
}
|
||||
|
||||
|
||||
const eventsCount = allEvents.length;
|
||||
|
||||
const allGroupedValue = Object.keys(grouped)
|
||||
.map(key => grouped[key])
|
||||
.reduce((a, e) => a + e, 0);
|
||||
|
||||
for (const key in grouped) {
|
||||
grouped[key] = 100 / allGroupedValue * grouped[key];
|
||||
}
|
||||
|
||||
return grouped;
|
||||
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { EVENT_METADATA_FIELDS_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
import { PipelineStage } from "mongoose";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const { name: eventName, field, from, to } = getQuery(event);
|
||||
|
||||
if (!from) return setResponseStatus(event, 400, 'from is required');
|
||||
if (!to) return setResponseStatus(event, 400, 'to is required');
|
||||
if (!eventName) return setResponseStatus(event, 400, 'name is required');
|
||||
if (!field) return setResponseStatus(event, 400, 'field is required');
|
||||
|
||||
|
||||
const aggregation: PipelineStage[] = [
|
||||
{
|
||||
$match: {
|
||||
project_id: project._id, name: eventName,
|
||||
created_at: {
|
||||
$gte: new Date(from.toString()),
|
||||
$lte: new Date(to.toString()),
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $group: { _id: `$metadata.${field}`, count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]
|
||||
|
||||
const metadataGrouped = await EventModel.aggregate(aggregation);
|
||||
|
||||
return metadataGrouped;
|
||||
|
||||
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { EVENT_METADATA_FIELDS_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const { name: eventName } = getQuery(event);
|
||||
if (!eventName) return [];
|
||||
|
||||
const fields: string[] = await Redis.useCache({ key: `metadata_fields:${project_id}:${eventName}`, exp: EVENT_METADATA_FIELDS_EXPIRE_TIME }, async () => {
|
||||
const eventsWithName = await EventModel.find({ project_id, name: eventName }, { metadata: 1 }, { limit: 10, sort: { created_at: -1 } });
|
||||
const allMetadata = eventsWithName.map(e => e.metadata);
|
||||
const allFields = new Set<string>();
|
||||
for (const metadata of allMetadata) {
|
||||
const keys = Object.keys(metadata || {});
|
||||
keys.forEach(key => allFields.add(key));
|
||||
}
|
||||
return Array.from(allFields.values());
|
||||
});
|
||||
|
||||
return fields;
|
||||
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { EVENT_NAMES_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const names: string[] = await Redis.useCache({ key: `event_names:${project_id}`, exp: EVENT_NAMES_EXPIRE_TIME }, async () => {
|
||||
const namesAggregation = await EventModel.aggregate([{ $match: { project_id: project._id } }, { $group: { _id: "$name" } }]);
|
||||
return namesAggregation.map(e => e._id);
|
||||
});
|
||||
|
||||
return names;
|
||||
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { ProjectModel } from "@schema/project/ProjectSchema";
|
||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||
|
||||
export type EventsPie = {
|
||||
_id: string,
|
||||
count: number
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const user = getRequestUser(event);
|
||||
if (!user?.logged) return;
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
const project = await ProjectModel.findOne({ _id: project_id, owner: user.id });
|
||||
if (!project) return;
|
||||
|
||||
|
||||
return await Redis.useCache({
|
||||
key: `events_pie${project_id}`,
|
||||
exp: TIMELINE_EXPIRE_TIME
|
||||
}, async () => {
|
||||
|
||||
const eventsPie: EventsPie[] = await EventModel.aggregate([
|
||||
{ $match: { project_id: project._id } },
|
||||
{ $group: { _id: "$name", count: { $sum: 1 } } }
|
||||
]);
|
||||
|
||||
return eventsPie as EventsPie[];
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user