+
+ class="far fa-trash hover:text-gray-300 cursor-pointer">
diff --git a/dashboard/server/api/keys/create.post.ts b/dashboard/server/api/keys/create.post.ts
index cde6c50..1f31b08 100644
--- a/dashboard/server/api/keys/create.post.ts
+++ b/dashboard/server/api/keys/create.post.ts
@@ -29,7 +29,7 @@ export default defineEventHandler(async event => {
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
-
+
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
diff --git a/dashboard/server/api/metrics/test.ts b/dashboard/server/api/metrics/test.ts
deleted file mode 100644
index eb093a8..0000000
--- a/dashboard/server/api/metrics/test.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-
-export default defineEventHandler(async event => {
-
- console.log('TEST');
- return;
-});
\ No newline at end of file
diff --git a/dashboard/server/api/pay/[project_id]/create-onetime.post.ts b/dashboard/server/api/pay/[project_id]/create-onetime.post.ts
index 18febd2..68db0e2 100644
--- a/dashboard/server/api/pay/[project_id]/create-onetime.post.ts
+++ b/dashboard/server/api/pay/[project_id]/create-onetime.post.ts
@@ -9,9 +9,13 @@ export default defineEventHandler(async event => {
if (!project_id) return;
const user = getRequestUser(event);
+ if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
+
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
+ if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
+
const body = await readBody(event);
const { planId } = body;
diff --git a/dashboard/server/api/pay/[project_id]/create.post.ts b/dashboard/server/api/pay/[project_id]/create.post.ts
index ff04f52..7a5a08e 100644
--- a/dashboard/server/api/pay/[project_id]/create.post.ts
+++ b/dashboard/server/api/pay/[project_id]/create.post.ts
@@ -9,9 +9,13 @@ export default defineEventHandler(async event => {
if (!project_id) return;
const user = getRequestUser(event);
+ if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
+
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
+ if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
+
const body = await readBody(event);
const { planId } = body;
diff --git a/dashboard/server/api/pay/webhook.post.ts b/dashboard/server/api/pay/webhook.post.ts
index 7dae67d..f163bd1 100644
--- a/dashboard/server/api/pay/webhook.post.ts
+++ b/dashboard/server/api/pay/webhook.post.ts
@@ -143,6 +143,7 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
EmailService.sendPurchaseEmail(user.email, project.name);
}, 1);
+
return { ok: true };
diff --git a/dashboard/server/api/project/delete.delete.ts b/dashboard/server/api/project/delete.delete.ts
index 75daf8b..f15166a 100644
--- a/dashboard/server/api/project/delete.delete.ts
+++ b/dashboard/server/api/project/delete.delete.ts
@@ -18,6 +18,8 @@ export default defineEventHandler(async event => {
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not exist');
+ if (userData.id != project.owner.toString()) return setResponseStatus(event, 400, 'You cannot delete a project as guest');
+
const projects = await ProjectModel.countDocuments({ owner: userData.id });
if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project');
diff --git a/dashboard/server/init.ts b/dashboard/server/init.ts
index ce007cd..d1a460f 100644
--- a/dashboard/server/init.ts
+++ b/dashboard/server/init.ts
@@ -2,10 +2,17 @@ import mongoose from "mongoose";
import { Redis } from "~/server/services/CacheService";
import EmailService from '@services/EmailService';
import StripeService from '~/server/services/StripeService';
+import { anomalyLoop } from "./services/AnomalyService";
const config = useRuntimeConfig();
let connection: mongoose.Mongoose;
+
+let anomalyMinutesCount = 0;
+function anomalyCheck() {
+
+}
+
export default async () => {
console.log('[SERVER] Initializing');
@@ -37,4 +44,7 @@ export default async () => {
console.log('[SERVER] Completed');
+ console.log('[ANOMALY LOOP] Started');
+ anomalyLoop();
+
};
\ No newline at end of file
diff --git a/dashboard/server/services/AnomalyService.ts b/dashboard/server/services/AnomalyService.ts
new file mode 100644
index 0000000..2504d16
--- /dev/null
+++ b/dashboard/server/services/AnomalyService.ts
@@ -0,0 +1,148 @@
+import mongoose from "mongoose";
+import { executeTimelineAggregation } from "./TimelineService";
+import { VisitModel } from "@schema/metrics/VisitSchema";
+import { AnomalyDomainModel } from '@schema/anomalies/AnomalyDomainSchema';
+import { AnomalyVisitModel } from '@schema/anomalies/AnomalyVisitSchema';
+import { AnomalyEventsModel } from '@schema/anomalies/AnomalyEventsSchema';
+import { EventModel } from "@schema/metrics/EventSchema";
+import EmailService from "@services/EmailService";
+
+import * as url from 'url';
+import { ProjectModel } from "@schema/ProjectSchema";
+import { UserModel } from "@schema/UserSchema";
+
+type TAvgInput = { _id: string, count: number }
+
+const anomalyData = { minutes: 0 }
+
+async function anomalyCheckAll() {
+ const start = performance.now();
+ console.log('START ANOMALY CHECK');
+ const projects = await ProjectModel.find({}, { _id: 1 });
+ for (const project of projects) {
+ await findAnomalies(project.id);
+ }
+ const end = start - performance.now();
+ console.log('END ANOMALY CHECK', end, 'ms');
+}
+
+export function anomalyLoop() {
+ if (anomalyData.minutes == 60 * 12) {
+ anomalyCheckAll();
+ anomalyData.minutes = 0;
+ }
+ setTimeout(() => anomalyLoop(), 1000 * 60);
+}
+
+
+function movingAverageAnomaly(visits: TAvgInput[], windowSize: number, threshold: number): TAvgInput[] {
+ const anomalies: TAvgInput[] = [];
+ for (let i = windowSize; i < visits.length; i++) {
+ const window = visits.slice(i - windowSize, i);
+ const mean = window.reduce((a, b) => a + b.count, 0) / window.length;
+ const stdDev = Math.sqrt(window.reduce((sum, visit) => sum + Math.pow(visit.count - mean, 2), 0) / window.length);
+ const currentVisit = visits[i];
+ if (Math.abs(currentVisit.count - mean) > threshold * stdDev) {
+ if (currentVisit.count <= mean) continue;
+ anomalies.push(currentVisit);
+ }
+ }
+ return anomalies;
+}
+
+function getUrlFromString(str: string) {
+ const res = str.startsWith('http') ? str : 'http://' + str;
+ return res;
+}
+
+export async function findAnomalies(project_id: string) {
+
+ const THRESHOLD = 6;
+ const WINDOW_SIZE = 14;
+
+ const pid = new mongoose.Types.ObjectId(project_id) as any;
+
+ const from = Date.now() - 1000 * 60 * 60 * 24 * 30;
+ const to = Date.now() - 1000 * 60 * 60 * 24;
+
+ const visitsTimelineData = await executeTimelineAggregation({
+ projectId: pid,
+ model: VisitModel,
+ from, to, slice: 'day'
+ });
+
+ const eventsTimelineData = await executeTimelineAggregation({
+ projectId: pid,
+ model: EventModel,
+ from, to, slice: 'day'
+ });
+
+ const websites: { _id: string, count: number }[] = await VisitModel.aggregate([
+ { $match: { project_id: pid, created_at: { $gte: new Date(from), $lte: new Date(to) } }, },
+ { $group: { _id: "$website", count: { $sum: 1, } } }
+ ]);
+
+
+ const rootWebsite = websites.reduce((a, e) => {
+ return a.count > e.count ? a : e;
+ });
+
+ const rootDomain = new url.URL(getUrlFromString(rootWebsite._id)).hostname;
+
+ const detectedWebsites: string[] = [];
+
+ for (const website of websites) {
+ const websiteDomain = new url.URL(getUrlFromString(website._id)).hostname;
+
+ if (!websiteDomain.includes(rootDomain)) {
+ detectedWebsites.push(website._id);
+ }
+ }
+
+ const visitAnomalies = movingAverageAnomaly(visitsTimelineData, WINDOW_SIZE, THRESHOLD);
+ const eventAnomalies = movingAverageAnomaly(eventsTimelineData, WINDOW_SIZE, THRESHOLD);
+
+ const shouldSendMail = {
+ visitsEvents: false,
+ domains: false
+ }
+
+ for (const visit of visitAnomalies) {
+ const anomalyAlreadyExist = await AnomalyVisitModel.findOne({ visitDate: visit._id }, { _id: 1 });
+ if (anomalyAlreadyExist) continue;
+ await AnomalyVisitModel.create({ project_id: pid, visitDate: visit._id, created_at: Date.now() });
+ shouldSendMail.visitsEvents = true;
+ }
+
+ for (const event of eventAnomalies) {
+ const anomalyAlreadyExist = await AnomalyEventsModel.findOne({ eventDate: event._id }, { _id: 1 });
+ if (anomalyAlreadyExist) continue;
+ await AnomalyEventsModel.create({ project_id: pid, eventDate: event._id, created_at: Date.now() });
+ shouldSendMail.visitsEvents = true;
+ }
+
+ for (const website of detectedWebsites) {
+ const anomalyAlreadyExist = await AnomalyDomainModel.findOne({ domain: website }, { _id: 1 });
+ if (anomalyAlreadyExist) continue;
+ await AnomalyDomainModel.create({ project_id: pid, domain: website, created_at: Date.now() });
+ shouldSendMail.domains = true;
+ }
+
+
+ const project = await ProjectModel.findById(pid);
+ if (!project) return { ok: false, error: 'Cannot find project with id ' + pid.toString() }
+ const user = await UserModel.findById(project.owner);
+ if (!user) return { ok: false, error: 'Cannot find user with id ' + project.owner.toString() }
+
+ if (shouldSendMail.visitsEvents === true) {
+ await EmailService.sendAnomalyVisitsEventsEmail(user.email, project.name);
+ }
+ if (shouldSendMail.domains === true) {
+ await EmailService.sendAnomalyDomainEmail(user.email, project.name);
+ }
+
+
+ return { ok: true };
+
+
+}
diff --git a/producer/src/deprecated.ts b/producer/src/deprecated.ts
index 42c8e5b..7171430 100644
--- a/producer/src/deprecated.ts
+++ b/producer/src/deprecated.ts
@@ -6,7 +6,7 @@ import { RedisStreamService } from "@services/RedisStreamService";
const router = Router();
const allowAnyType = () => true;
-const jsonOptions = { limit: '5mb', type: allowAnyType }
+const jsonOptions = { limit: '25kb', type: allowAnyType }
const streamName = requireEnv('STREAM_NAME');
diff --git a/producer/src/index.ts b/producer/src/index.ts
index 13de99f..886eaa9 100644
--- a/producer/src/index.ts
+++ b/producer/src/index.ts
@@ -9,7 +9,7 @@ const app = express();
app.use(cors());
const allowAnyType = () => true;
-const jsonOptions = { limit: '5mb', type: allowAnyType }
+const jsonOptions = { limit: '25kb', type: allowAnyType }
const streamName = requireEnv('STREAM_NAME');
diff --git a/shared/schema/anomalies/AnomalyDomainSchema.ts b/shared/schema/anomalies/AnomalyDomainSchema.ts
new file mode 100644
index 0000000..a67f575
--- /dev/null
+++ b/shared/schema/anomalies/AnomalyDomainSchema.ts
@@ -0,0 +1,16 @@
+import { model, Schema, Types } from 'mongoose';
+
+
+export type TAnomalyDomain = {
+ project_id: Schema.Types.ObjectId
+ domain: string,
+ created_at: Date
+}
+
+const AnomalyDomainSchema = new Schema
({
+ project_id: { type: Types.ObjectId, required: true },
+ domain: { type: String, required: true },
+ created_at: { type: Date, required: true },
+})
+
+export const AnomalyDomainModel = model('anomaly_domains', AnomalyDomainSchema);
diff --git a/shared/schema/anomalies/AnomalyEventsSchema.ts b/shared/schema/anomalies/AnomalyEventsSchema.ts
new file mode 100644
index 0000000..8cb2622
--- /dev/null
+++ b/shared/schema/anomalies/AnomalyEventsSchema.ts
@@ -0,0 +1,16 @@
+import { model, Schema, Types } from 'mongoose';
+
+
+export type TAnomalyEvents = {
+ project_id: Schema.Types.ObjectId
+ eventDate: string,
+ created_at: Date
+}
+
+const AnomalyEventsSchema = new Schema({
+ project_id: { type: Types.ObjectId, required: true },
+ eventDate: { type: String, required: true },
+ created_at: { type: Date, required: true },
+})
+
+export const AnomalyEventsModel = model('anomaly_events', AnomalyEventsSchema);
diff --git a/shared/schema/anomalies/AnomalyVisitSchema.ts b/shared/schema/anomalies/AnomalyVisitSchema.ts
new file mode 100644
index 0000000..8230098
--- /dev/null
+++ b/shared/schema/anomalies/AnomalyVisitSchema.ts
@@ -0,0 +1,16 @@
+import { model, Schema, Types } from 'mongoose';
+
+
+export type TAnomalyVisit = {
+ project_id: Schema.Types.ObjectId
+ visitDate: string,
+ created_at: Date
+}
+
+const AnomalyVisitSchema = new Schema({
+ project_id: { type: Types.ObjectId, required: true },
+ visitDate: { type: String, required: true },
+ created_at: { type: Date, required: true },
+})
+
+export const AnomalyVisitModel = model('anomaly_visits', AnomalyVisitSchema);
diff --git a/shared/services/EmailService.ts b/shared/services/EmailService.ts
index 189394f..a0b04d4 100644
--- a/shared/services/EmailService.ts
+++ b/shared/services/EmailService.ts
@@ -4,6 +4,8 @@ import { LIMIT_50_EMAIL } from './email_templates/Limit50Email';
import { LIMIT_90_EMAIL } from './email_templates/Limit90Email';
import { LIMIT_MAX_EMAIL } from './email_templates/LimitMaxEmail';
import { PURCHASE_EMAIL } from './email_templates/PurchaseEmail';
+import { ANOMALY_VISITS_EVENTS_EMAIL } from './email_templates/AnomalyUsageEmail';
+import { ANOMALY_DOMAIN_EMAIL } from './email_templates/AnomalyDomainEmail';
class EmailService {
@@ -99,6 +101,40 @@ class EmailService {
}
}
+ async sendAnomalyVisitsEventsEmail(target: string, projectName: string) {
+ try {
+ const sendSmtpEmail = new SendSmtpEmail();
+ sendSmtpEmail.subject = "🚨 Unexpected Activity Detected by our AI";
+ sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
+ sendSmtpEmail.to = [{ "email": target }];
+ sendSmtpEmail.htmlContent = ANOMALY_VISITS_EVENTS_EMAIL
+ .replace(/\[Project Name\]/, projectName)
+ .toString();;
+ await this.apiInstance.sendTransacEmail(sendSmtpEmail);
+ return true;
+ } catch (ex) {
+ console.error('ERROR SENDING EMAIL', ex);
+ return false;
+ }
+ }
+
+ async sendAnomalyDomainEmail(target: string, projectName: string) {
+ try {
+ const sendSmtpEmail = new SendSmtpEmail();
+ sendSmtpEmail.subject = "🚨 Anomaly detected by our AI";
+ sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" };
+ sendSmtpEmail.to = [{ "email": target }];
+ sendSmtpEmail.htmlContent = ANOMALY_DOMAIN_EMAIL
+ .replace(/\[Project Name\]/, projectName)
+ .toString();;
+ await this.apiInstance.sendTransacEmail(sendSmtpEmail);
+ return true;
+ } catch (ex) {
+ console.error('ERROR SENDING EMAIL', ex);
+ return false;
+ }
+ }
+
}
const instance = new EmailService();
diff --git a/shared/services/email_templates/anomaly-dns-email.html b/shared/services/email_templates/AnomalyDomainEmail.ts
similarity index 80%
rename from shared/services/email_templates/anomaly-dns-email.html
rename to shared/services/email_templates/AnomalyDomainEmail.ts
index 7eb7257..156b0c4 100644
--- a/shared/services/email_templates/anomaly-dns-email.html
+++ b/shared/services/email_templates/AnomalyDomainEmail.ts
@@ -1,4 +1,4 @@
-
+export const ANOMALY_DOMAIN_EMAIL = `
@@ -9,17 +9,12 @@
- ❗️ Anomaly detected by our AI
Dear User,
- We wanted to let you know that [Project Name] on Litlyx has an anomaly that our AI agent detected. Here is the report:
+ We wanted to let you know that [Project Name] on Litlyx has an anomaly that our AI agent detected.
- Anomaly: Suspicious DNS
- Message: [Suspicious DNS name] is logging data in your project. Is that you?
- Date: Current date!
-
- You can analyze the suspicious DNS on your Litlyx dashboard. We put a symbol next to each suspicious DNS to let users know something might be wrong!
+ You can analyze a suspicious DNS on your Litlyx dashboard. We put a symbol next to each suspicious DNS to let users know something might be wrong!
What can I do?
@@ -45,3 +40,4 @@
+`
\ No newline at end of file
diff --git a/shared/services/email_templates/anomaly-usage-email.html b/shared/services/email_templates/AnomalyUsageEmail.ts
similarity index 95%
rename from shared/services/email_templates/anomaly-usage-email.html
rename to shared/services/email_templates/AnomalyUsageEmail.ts
index d2cb96d..6f29eaf 100644
--- a/shared/services/email_templates/anomaly-usage-email.html
+++ b/shared/services/email_templates/AnomalyUsageEmail.ts
@@ -1,4 +1,4 @@
-
+export const ANOMALY_VISITS_EVENTS_EMAIL = `
@@ -9,7 +9,6 @@
- 🚨 Unexpected Activity Detected by our AI
Dear User,
@@ -40,3 +39,4 @@
+`
\ No newline at end of file