new selfhosted version

This commit is contained in:
antonio
2025-11-28 14:11:51 +01:00
parent afda29997d
commit 951860f67e
1046 changed files with 72586 additions and 574750 deletions

View 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')
});

View 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;
});

View File

@@ -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 }
});

View File

@@ -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;
});

View File

@@ -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 }
});

View 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;
});

View File

@@ -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[];
});

View 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;
});

View File

@@ -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 }
});

View File

@@ -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([
{

View File

@@ -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) };
});

View File

@@ -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[]
};
});

View File

@@ -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 };
});

View 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 };
});

View File

@@ -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 };
});

View File

@@ -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[];
});

View File

@@ -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;
});

View File

@@ -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);
})
});

View File

@@ -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 }
});

View 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 }
});

View 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;
});

View File

@@ -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());
});

View File

@@ -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;
});

View File

@@ -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;
});

View 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;
});

View 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;
});

View 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;
});

View 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;
});

View File

@@ -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);
});

View File

@@ -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');
}
});

View 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, '/')
}
});

View File

@@ -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 }) }
});

View File

@@ -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 }) }
});
});

View File

@@ -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 }) }
});

View File

@@ -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 };
});

View File

@@ -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, } } },

View 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 }[];
});
});

View 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 }[];
});
});

View File

@@ -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;
});
});

View File

@@ -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, } } },

View File

@@ -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, } } },

View 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 }[];
});
});

View 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 }[]
});

View 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[];
});

View 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 }[];
});

View File

@@ -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, } } },

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View 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 }[];
});
});

View File

@@ -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;
});

View File

@@ -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, } } },

View File

@@ -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 }[];
});

View 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 }[];
});
});

View File

@@ -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 => {
});
});

View 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 }[];
});
});

View 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
}));
});
});

View 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`);
}

View File

@@ -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);
});
});

View 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[];
});

View File

@@ -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 }
});

View 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 })
});

View File

@@ -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();
});

View File

@@ -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 };
});

View File

@@ -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[];
});

View File

@@ -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;
});

View 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 };
});

View 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 };
});

View 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 };
});

View File

@@ -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: {

View File

@@ -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 }

View 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 });
});

View 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;
});

View 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 }
});

View File

@@ -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;
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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[];
});
});

View File

@@ -1,21 +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 hasEvent = await EventModel.exists({ project_id: project._id });
if (hasEvent) return true;
const hasVisit = await VisitModel.exists({ project_id: project._id });
if (hasVisit) return true;
return false;
});

View File

@@ -1,29 +0,0 @@
import { Types } from "mongoose";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { SessionModel } from "@schema/metrics/SessionSchema";
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 online_users = await SessionModel.aggregate([
{
$match: {
project_id: new Types.ObjectId(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;
});

View File

@@ -1,40 +0,0 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
import { ProjectModel } from "@schema/project/ProjectSchema";
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 });
if (!project) return;
const [hasAccess] = await hasAccessToProject(user.id, project_id, project)
if (!hasAccess) return;
const query = getQuery(event);
const { orderBy, order, page, limit, type } = query;
const limitValue = limit ? parseInt(limit.toString()) : 20;
const skipValue = page ? (parseInt(page.toString()) - 1) * limitValue : 0;
if (type == '0') {
const visits = await VisitModel.find({ project_id: project }, {}, {
limit: limitValue,
skip: skipValue,
sort: { [(orderBy || '').toString()]: order == 'asc' ? 1 : -1 }
});
return visits;
} else {
const events = await EventModel.find({ project_id: project }, {}, {
limit: limitValue,
skip: skipValue,
sort: { [(orderBy || '').toString()]: order == 'asc' ? 1 : -1 }
});
return events;
}
});

View File

@@ -1,36 +0,0 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { getTimeline } from "./generic";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { executeTimelineAggregation, fillAndMergeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
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 { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:events:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
exp: TIMELINE_EXPIRE_TIME
}, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project._id,
model: EventModel,
from, to, slice
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
return timelineFilledMerged;
});
});

View File

@@ -1,36 +0,0 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { getTimeline } from "./generic";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
import DateService from '@services/DateService';
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 { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
const timelineStackedEvents = await executeAdvancedTimelineAggregation<{ name: String }>({
model: EventModel,
projectId: project._id,
from, to, slice,
customProjection: { name: "$_id.name" },
customIdGroup: { name: '$name' },
})
return timelineStackedEvents;
});
});

View File

@@ -1,81 +0,0 @@
import { AggregateOptions, Model, Types } from "mongoose";
import { ProjectModel } from "@schema/project/ProjectSchema";
export type MetricsTimeline = {
_id: string,
count: number
}
export async function getTimeline(model: Model<any>, project_id: string, slice: 'hour' | 'day' | 'month' | 'year' = 'day', duration?: number, customOptions: AggregateOptions = {}, customGroup: Object = {}, customProjection: Object = {}, customGroupId: Object = {}, customMatch: Object = {}) {
const groupId: any = {};
const sort: any = {};
const fromParts: any = {};
const from = new Date();
const to = new Date();
from.setMinutes(0, 0, 0);
to.setMinutes(0, 0, 0);
switch (slice) {
case 'day':
from.setDate(from.getDate() - (duration || 7));
from.setHours(0);
to.setHours(0);
break;
case 'hour':
from.setHours(from.getHours() - (duration || 24));
break;
}
switch (slice) {
case 'hour':
groupId.hour = { $hour: '$created_at' }
sort['_id.hour'] = 1;
fromParts.hour = "$_id.hour";
case 'day':
groupId.day = { $dayOfMonth: '$created_at' }
sort['_id.day'] = 1;
fromParts.day = "$_id.day";
case 'month':
groupId.month = { $month: '$created_at' }
sort['_id.month'] = 1;
fromParts.month = "$_id.month";
case 'year':
groupId.year = { $year: '$created_at' }
sort['_id.year'] = 1;
fromParts.year = "$_id.year";
}
const aggregation: any[] = [
{
$match: {
project_id: new Types.ObjectId(project_id),
created_at: { $gte: from, $lte: to },
...customMatch
}
},
{ $group: { _id: { ...groupId, ...customGroupId }, count: { $sum: 1 }, ...customGroup } },
{ $sort: sort },
{ $project: { _id: { $dateFromParts: fromParts }, count: "$count", ...customProjection } }
]
const result: MetricsTimeline[] = await model.aggregate(aggregation, customOptions);
return { data: result, from, to };
}
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;
});

View File

@@ -1,37 +0,0 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
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 { slice, from, to, referrer } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:referrers:${referrer}:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
exp: TIMELINE_EXPIRE_TIME
}, async () => {
const timelineData = await executeAdvancedTimelineAggregation({
projectId: project._id,
model: VisitModel,
from, to, slice,
customMatch: {
referrer
}
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
return timelineFilledMerged;
});
});

View File

@@ -1,37 +0,0 @@
import { getTimeline } from "./generic";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { SessionModel } from "@schema/metrics/SessionSchema";
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
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 { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:sessions:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
exp: TIMELINE_EXPIRE_TIME
}, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project._id,
model: SessionModel,
from, to, slice
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
return timelineFilledMerged;
});
});

View File

@@ -1,55 +0,0 @@
import { getTimeline } from "./generic";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { SessionModel } from "@schema/metrics/SessionSchema";
import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
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 { slice, duration } = await readBody(event);
// return await Redis.useCache({ key: `timeline:sessions_duration:${project_id}:${slice}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
// const timelineSessionsDuration = await getTimeline(SessionModel, project_id, slice, duration,
// {},
// { duration: { $sum: '$duration' } },
// { count: { $divide: ["$duration", "$count"] } }
// );
// return timelineSessionsDuration;
// });
const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:sessions_duration:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
exp: TIMELINE_EXPIRE_TIME
}, async () => {
const timelineData = await executeAdvancedTimelineAggregation({
projectId: project._id,
model: SessionModel,
from, to, slice,
customGroup: {
duration: { $sum: '$duration' }
},
customProjection: {
count: { $divide: ["$duration", "$count"] }
},
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from ,to);
return timelineFilledMerged;
});
});

View File

@@ -1,36 +0,0 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import DateService from "@services/DateService";
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
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 { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:visits:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
exp: TIMELINE_EXPIRE_TIME,
}, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project._id,
model: VisitModel,
from, to, slice,
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
return timelineFilledMerged;
});
});

Some files were not shown because too many files have changed in this diff Show More