From 75f0787c3bf81829ce4fedcdfe3953682649dbd1 Mon Sep 17 00:00:00 2001 From: Litlyx Date: Sat, 1 Jun 2024 15:27:21 +0200 Subject: [PATCH] add shared --- shared/.gitignore | 1 + shared/data/ADMINS.ts | 4 + shared/data/LITLYX.ts | 1 + shared/data/PREMIUM_LIMITS.ts | 46 ++++ shared/data/broker/EventType.ts | 5 + shared/data/broker/Limits.ts | 5 + shared/functions/UtilsProjectCounts.ts | 70 ++++++ shared/package.json | 10 + shared/pnpm-lock.yaml | 232 +++++++++++++++++++ shared/schema/ProjectSchema.ts | 22 ++ shared/schema/ProjectsCounts.ts | 26 +++ shared/schema/UserSchema.ts | 22 ++ shared/schema/UserSettings.ts | 14 ++ shared/schema/ai/AiChatSchema.ts | 20 ++ shared/schema/broker/LimitNotifySchema.ts | 18 ++ shared/schema/metrics/EventSchema.ts | 18 ++ shared/schema/metrics/SessionSchema.ts | 21 ++ shared/schema/metrics/VisitSchema.ts | 39 ++++ shared/services/DatabaseService.ts | 6 + shared/services/EmailService.ts | 259 ++++++++++++++++++++++ shared/tsconfig.json | 10 + shared/utilts/TIME.ts | 9 + shared/utilts/requireEnv.ts | 8 + 23 files changed, 866 insertions(+) create mode 100644 shared/.gitignore create mode 100644 shared/data/ADMINS.ts create mode 100644 shared/data/LITLYX.ts create mode 100644 shared/data/PREMIUM_LIMITS.ts create mode 100644 shared/data/broker/EventType.ts create mode 100644 shared/data/broker/Limits.ts create mode 100644 shared/functions/UtilsProjectCounts.ts create mode 100644 shared/package.json create mode 100644 shared/pnpm-lock.yaml create mode 100644 shared/schema/ProjectSchema.ts create mode 100644 shared/schema/ProjectsCounts.ts create mode 100644 shared/schema/UserSchema.ts create mode 100644 shared/schema/UserSettings.ts create mode 100644 shared/schema/ai/AiChatSchema.ts create mode 100644 shared/schema/broker/LimitNotifySchema.ts create mode 100644 shared/schema/metrics/EventSchema.ts create mode 100644 shared/schema/metrics/SessionSchema.ts create mode 100644 shared/schema/metrics/VisitSchema.ts create mode 100644 shared/services/DatabaseService.ts create mode 100644 shared/services/EmailService.ts create mode 100644 shared/tsconfig.json create mode 100644 shared/utilts/TIME.ts create mode 100644 shared/utilts/requireEnv.ts diff --git a/shared/.gitignore b/shared/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/shared/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/shared/data/ADMINS.ts b/shared/data/ADMINS.ts new file mode 100644 index 0000000..59fe4f7 --- /dev/null +++ b/shared/data/ADMINS.ts @@ -0,0 +1,4 @@ +export const ADMIN_EMAILS = [ + 'laura.emily.lovi@gmail.com', + 'mangaiomaster@gmail.com' +] \ No newline at end of file diff --git a/shared/data/LITLYX.ts b/shared/data/LITLYX.ts new file mode 100644 index 0000000..132a0c3 --- /dev/null +++ b/shared/data/LITLYX.ts @@ -0,0 +1 @@ +export const LITLYX_PROJECT_ID = '6643cd08a1854e3b81722ab5'; \ No newline at end of file diff --git a/shared/data/PREMIUM_LIMITS.ts b/shared/data/PREMIUM_LIMITS.ts new file mode 100644 index 0000000..26e8a33 --- /dev/null +++ b/shared/data/PREMIUM_LIMITS.ts @@ -0,0 +1,46 @@ + + +export const PREMIUM_PLANS = [ + { id: 0, tag: 'FREE', name: 'Free' }, + { id: 1, tag: 'PLAN_1', name: 'Premium 1' }, + { id: 2, tag: 'PLAN_2', name: 'Premium 2' }, + { id: 3, tag: 'PLAN_3', name: 'Premium 3' }, + { id: 99, tag: 'PLAN_99', name: 'Premium 99' }, +] as const; + +export function getPlanFromPremiumType(premium_type?: number) { + if (!premium_type) return PREMIUM_PLANS[0]; + const plan = PREMIUM_PLANS.find(e => e.id === premium_type); + if (!plan) return PREMIUM_PLANS[0]; + return plan; +} + +export type PREMIUM_PLAN_TAG = typeof PREMIUM_PLANS[number]['tag']; + +export type PROJECT_LIMIT = { + COUNT_LIMIT: number, + AI_MESSAGE_LIMIT: number, +} + +export const PREMIUM_LIMITS: Record = { + FREE: { + COUNT_LIMIT: 3_000, + AI_MESSAGE_LIMIT: 10 + }, + PLAN_1: { + COUNT_LIMIT: 150_000, + AI_MESSAGE_LIMIT: 100 + }, + PLAN_2: { + COUNT_LIMIT: 500_000, + AI_MESSAGE_LIMIT: 5_000 + }, + PLAN_3: { + COUNT_LIMIT: 2_000_000, + AI_MESSAGE_LIMIT: 10_000 + }, + PLAN_99: { + COUNT_LIMIT: 10_000_000, + AI_MESSAGE_LIMIT: 100_000 + } +} \ No newline at end of file diff --git a/shared/data/broker/EventType.ts b/shared/data/broker/EventType.ts new file mode 100644 index 0000000..073ca6e --- /dev/null +++ b/shared/data/broker/EventType.ts @@ -0,0 +1,5 @@ + +export enum EventType { + VISIT = 1, + EVENT = 2 +} \ No newline at end of file diff --git a/shared/data/broker/Limits.ts b/shared/data/broker/Limits.ts new file mode 100644 index 0000000..4a8c0ef --- /dev/null +++ b/shared/data/broker/Limits.ts @@ -0,0 +1,5 @@ + + +// Default: 1.1 +// ((events + visits) * VALUE) > limit +export const EVENT_LOG_LIMIT_PERCENT = 1.1; \ No newline at end of file diff --git a/shared/functions/UtilsProjectCounts.ts b/shared/functions/UtilsProjectCounts.ts new file mode 100644 index 0000000..54f9a77 --- /dev/null +++ b/shared/functions/UtilsProjectCounts.ts @@ -0,0 +1,70 @@ +import { ProjectCountModel } from '../schema/ProjectsCounts'; +import { ProjectModel } from '../schema/ProjectSchema'; +import { LimitNotifyModel } from '../schema/broker/LimitNotifySchema'; +import { PREMIUM_LIMITS, getPlanFromPremiumType } from '../data/PREMIUM_LIMITS'; +import { MONTH } from '../utilts/TIME'; + + +export async function getCurrentProjectCountId(project_id: string) { + const projectCount = await ProjectCountModel.findOne({ project_id }, { _id: 1 }, { sort: { billing_expire_at: -1 } }); + return projectCount?._id.toString(); +} + +export async function getAllLimitsFromProjectId(project_id: string) { + const targetProject = await ProjectModel.findById(project_id, { + premium: 1, premium_type: 1, premium_expire_at: 1 + }); + if (!targetProject) return PREMIUM_LIMITS.FREE; + if (!targetProject.premium) return PREMIUM_LIMITS.FREE; + const plan = getPlanFromPremiumType(targetProject.premium_type); + return PREMIUM_LIMITS[plan.tag]; +} + +export async function checkProjectCount(project_id: string) { + + const targetProject = await ProjectModel.findById(project_id, { + premium: 1, premium_type: 1, premium_expire_at: 1 + }); + + if (!targetProject) return; + + if (new Date(targetProject.premium_expire_at).getTime() < Date.now()) { + await ProjectModel.updateOne({ _id: project_id }, { + premium: false, + $unset: { + premium_type: 1, + premium_expire_at: 1 + }, + }); + } + + const limits = await getAllLimitsFromProjectId(project_id); + + const projectCounts = await ProjectCountModel.findOne({ project_id }, {}, { sort: { billing_expire_at: -1 } }); + + const billingExpireAt = new Date(projectCounts.billing_expire_at).getTime(); + + if (projectCounts && Date.now() < billingExpireAt) { + if (projectCounts.ai_limit) return projectCounts.toJSON(); + projectCounts.ai_limit = limits.AI_MESSAGE_LIMIT; + const saved = await projectCounts.save(); + return saved.toJSON(); + } + + const newProjectCounts = await ProjectCountModel.create({ + project_id, + events: 0, + visits: 0, + limit: limits.COUNT_LIMIT, + ai_messages: 0, + ai_limit: limits.AI_MESSAGE_LIMIT, + billing_start_at: projectCounts ? billingExpireAt : Date.now(), + billing_expire_at: (projectCounts ? billingExpireAt : Date.now()) + MONTH + }); + + await LimitNotifyModel.updateOne({ project_id }, { limit1: false, limit2: false, limit3: false }); + + return newProjectCounts.toJSON(); + + +} \ No newline at end of file diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 0000000..de73e95 --- /dev/null +++ b/shared/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "mongoose": "^8.4.0", + "nodemailer": "^6.9.13" + }, + "devDependencies": { + "@types/node": "^20.12.13", + "@types/nodemailer": "^6.4.15" + } +} diff --git a/shared/pnpm-lock.yaml b/shared/pnpm-lock.yaml new file mode 100644 index 0000000..d49956b --- /dev/null +++ b/shared/pnpm-lock.yaml @@ -0,0 +1,232 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + mongoose: + specifier: ^8.4.0 + version: 8.4.0 + nodemailer: + specifier: ^6.9.13 + version: 6.9.13 + devDependencies: + '@types/node': + specifier: ^20.12.13 + version: 20.12.13 + '@types/nodemailer': + specifier: ^6.4.15 + version: 6.4.15 + +packages: + + '@mongodb-js/saslprep@1.1.7': + resolution: {integrity: sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==} + + '@types/node@20.12.13': + resolution: {integrity: sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==} + + '@types/nodemailer@6.4.15': + resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==} + + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + + bson@6.7.0: + resolution: {integrity: sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==} + engines: {node: '>=16.20.1'} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} + engines: {node: '>=12.0.0'} + + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + + mongodb-connection-string-url@3.0.1: + resolution: {integrity: sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==} + + mongodb@6.6.2: + resolution: {integrity: sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + + mongoose@8.4.0: + resolution: {integrity: sha512-fgqRMwVEP1qgRYfh+tUe2YBBFnPO35FIg2lfFH+w9IhRGg1/ataWGIqvf/MjwM29cZ60D5vSnqtN2b8Qp0sOZA==} + engines: {node: '>=16.20.1'} + + mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + + mquery@5.0.0: + resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} + engines: {node: '>=14.0.0'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nodemailer@6.9.13: + resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} + engines: {node: '>=6.0.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + + tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-url@13.0.0: + resolution: {integrity: sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==} + engines: {node: '>=16'} + +snapshots: + + '@mongodb-js/saslprep@1.1.7': + dependencies: + sparse-bitfield: 3.0.3 + + '@types/node@20.12.13': + dependencies: + undici-types: 5.26.5 + + '@types/nodemailer@6.4.15': + dependencies: + '@types/node': 20.12.13 + + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + + bson@6.7.0: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + kareem@2.6.3: {} + + memory-pager@1.5.0: {} + + mongodb-connection-string-url@3.0.1: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 13.0.0 + + mongodb@6.6.2: + dependencies: + '@mongodb-js/saslprep': 1.1.7 + bson: 6.7.0 + mongodb-connection-string-url: 3.0.1 + + mongoose@8.4.0: + dependencies: + bson: 6.7.0 + kareem: 2.6.3 + mongodb: 6.6.2 + mpath: 0.9.0 + mquery: 5.0.0 + ms: 2.1.3 + sift: 17.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + + mpath@0.9.0: {} + + mquery@5.0.0: + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + ms@2.1.2: {} + + ms@2.1.3: {} + + nodemailer@6.9.13: {} + + punycode@2.3.1: {} + + sift@17.1.3: {} + + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + + tr46@4.1.1: + dependencies: + punycode: 2.3.1 + + undici-types@5.26.5: {} + + webidl-conversions@7.0.0: {} + + whatwg-url@13.0.0: + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 diff --git a/shared/schema/ProjectSchema.ts b/shared/schema/ProjectSchema.ts new file mode 100644 index 0000000..d3e9bc6 --- /dev/null +++ b/shared/schema/ProjectSchema.ts @@ -0,0 +1,22 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TProject = { + _id: Schema.Types.ObjectId, + owner: Schema.Types.ObjectId, + name: string, + premium: boolean, + premium_type?: number, + premium_expire_at: Date, + created_at: Date +} + +const ProjectSchema = new Schema({ + owner: { type: Types.ObjectId, index: 1 }, + name: { type: String, required: true }, + premium: { type: Boolean, default: false }, + premium_type: { type: Number }, + premium_expire_at: { type: Date }, + created_at: { type: Date, default: () => Date.now() }, +}) + +export const ProjectModel = model('projects', ProjectSchema); diff --git a/shared/schema/ProjectsCounts.ts b/shared/schema/ProjectsCounts.ts new file mode 100644 index 0000000..ebde4f2 --- /dev/null +++ b/shared/schema/ProjectsCounts.ts @@ -0,0 +1,26 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TProjectCount = { + _id: Schema.Types.ObjectId, + project_id: Schema.Types.ObjectId, + events: number, + visits: number, + ai_messages: number, + limit: number, + ai_limit: number, + billing_expire_at: Date, + billing_start_at: Date, +} + +const ProjectCountSchema = new Schema({ + project_id: { type: Types.ObjectId, index: 1 }, + events: { type: Number, required: true, default: 0 }, + visits: { type: Number, required: true, default: 0 }, + ai_messages: { type: Number, required: true, default: 0 }, + limit: { type: Number, required: true }, + ai_limit: { type: Number, required: true }, + billing_start_at: { type: Date, required: true }, + billing_expire_at: { type: Date, required: true }, +}); + +export const ProjectCountModel = model('project_counts', ProjectCountSchema); \ No newline at end of file diff --git a/shared/schema/UserSchema.ts b/shared/schema/UserSchema.ts new file mode 100644 index 0000000..be9509d --- /dev/null +++ b/shared/schema/UserSchema.ts @@ -0,0 +1,22 @@ +import { model, Schema } from 'mongoose'; + +export type TUser = { + email: string, + name: string, + given_name: string, + locale: string, + picture: string, + created_at: Date +} + +const UserSchema = new Schema({ + email: { type: String, unique: true, index: 1 }, + name: String, + given_name: String, + locale: String, + picture: String, + created_at: { type: Date, default: () => Date.now() } +}) + +export const UserModel = model('users', UserSchema); + diff --git a/shared/schema/UserSettings.ts b/shared/schema/UserSettings.ts new file mode 100644 index 0000000..646869d --- /dev/null +++ b/shared/schema/UserSettings.ts @@ -0,0 +1,14 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TUserSettings = { + user_id: Schema.Types.ObjectId, + active_project_id: Schema.Types.ObjectId +} + +const UserSettingsSchema = new Schema({ + user_id: { type: Types.ObjectId, unique: true, index: 1 }, + active_project_id: Schema.Types.ObjectId, +}); + +export const UserSettingsModel = model('user_settings', UserSettingsSchema); + diff --git a/shared/schema/ai/AiChatSchema.ts b/shared/schema/ai/AiChatSchema.ts new file mode 100644 index 0000000..7a0336d --- /dev/null +++ b/shared/schema/ai/AiChatSchema.ts @@ -0,0 +1,20 @@ +import { model, Schema } from 'mongoose'; + +export type TAiChatSchema = { + _id: Schema.Types.ObjectId, + project_id: Schema.Types.ObjectId, + messages: any[], + title: string, + created_at: Date, + updated_at: Date +} + +const AiChatSchema = new Schema({ + project_id: { type: Schema.Types.ObjectId, index: 1 }, + messages: [{ _id: false, type: Schema.Types.Mixed }], + title: { type: String, required: true }, + created_at: { type: Date, default: () => Date.now() }, + updated_at: { type: Date, default: () => Date.now() }, +}); + +export const AiChatModel = model('ai_chats', AiChatSchema); \ No newline at end of file diff --git a/shared/schema/broker/LimitNotifySchema.ts b/shared/schema/broker/LimitNotifySchema.ts new file mode 100644 index 0000000..c1ec3d6 --- /dev/null +++ b/shared/schema/broker/LimitNotifySchema.ts @@ -0,0 +1,18 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TLimitNotify = { + _id: Schema.Types.ObjectId, + project_id: Schema.Types.ObjectId, + limit1: boolean, + limit2: boolean, + limit3: boolean +} + +const LimitNotifySchema = new Schema({ + project_id: { type: Types.ObjectId, index: 1 }, + limit1: { type: Boolean }, + limit2: { type: Boolean }, + limit3: { type: Boolean } +}); + +export const LimitNotifyModel = model('limit_notifies', LimitNotifySchema); \ No newline at end of file diff --git a/shared/schema/metrics/EventSchema.ts b/shared/schema/metrics/EventSchema.ts new file mode 100644 index 0000000..a738015 --- /dev/null +++ b/shared/schema/metrics/EventSchema.ts @@ -0,0 +1,18 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TEvent = { + project_id: Schema.Types.ObjectId, + name: string, + metadata: Record, + created_at: Date +} + +const EventSchema = new Schema({ + project_id: { type: Types.ObjectId, index: 1 }, + name: { type: String, required: true }, + metadata: Schema.Types.Mixed, + created_at: { type: Date, default: () => Date.now() }, +}) + +export const EventModel = model('events', EventSchema); + diff --git a/shared/schema/metrics/SessionSchema.ts b/shared/schema/metrics/SessionSchema.ts new file mode 100644 index 0000000..8ae27c6 --- /dev/null +++ b/shared/schema/metrics/SessionSchema.ts @@ -0,0 +1,21 @@ +import { model, Schema, Types } from 'mongoose'; + + +export type TSession = { + project_id: Schema.Types.ObjectId, + session: string, + duration: number, + updated_at: Date, + created_at: Date, +} + +const SessionSchema = new Schema({ + project_id: { type: Types.ObjectId, index: 1 }, + session: { type: String, required: true }, + duration: { type: Number, required: true, default: 0 }, + updated_at: { type: Date, default: () => Date.now() }, + created_at: { type: Date, default: () => Date.now() }, +}) + +export const SessionModel = model('sessions', SessionSchema); + diff --git a/shared/schema/metrics/VisitSchema.ts b/shared/schema/metrics/VisitSchema.ts new file mode 100644 index 0000000..128accc --- /dev/null +++ b/shared/schema/metrics/VisitSchema.ts @@ -0,0 +1,39 @@ +import { model, Schema } from 'mongoose'; + +export type TVisit = { + project_id: Schema.Types.ObjectId, + + browser: string, + os: string, + + continent: string, + country: string, + + device: string, + + website: string, + page: string, + referrer: string, + + created_at: Date +} + +const VisitSchema = new Schema({ + project_id: { type: Schema.Types.ObjectId, index: 1 }, + + browser: { type: String, required: true }, + os: { type: String, required: true }, + + continent: { type: String }, + country: { type: String }, + + device: { type: String }, + + website: { type: String, required: true }, + page: { type: String, required: true }, + referrer: { type: String, required: true }, + created_at: { type: Date, default: () => Date.now() }, +}) + +export const VisitModel = model('visits', VisitSchema); + diff --git a/shared/services/DatabaseService.ts b/shared/services/DatabaseService.ts new file mode 100644 index 0000000..7af0165 --- /dev/null +++ b/shared/services/DatabaseService.ts @@ -0,0 +1,6 @@ + +import mongoose from "mongoose"; + +export async function connectDatabase(connectionString: string) { + await mongoose.connect(connectionString); +} \ No newline at end of file diff --git a/shared/services/EmailService.ts b/shared/services/EmailService.ts new file mode 100644 index 0000000..1b6815a --- /dev/null +++ b/shared/services/EmailService.ts @@ -0,0 +1,259 @@ +import nodemailer from 'nodemailer'; +import { requireEnv } from '../utilts/requireEnv'; + +const transport = nodemailer.createTransport({ + service: requireEnv('EMAIL_SERVICE'), + host: requireEnv('EMAIL_HOST'), + auth: { + user: requireEnv('EMAIL_USER'), + pass: requireEnv('EMAIL_PASS') + } +}); + +const TemplateEmail50 = ` + + + + + + + LitLyx Limit Reached Email + + + + +
+
+

⚠ Limit for project ⚠

+
+
+

Hey there! We found that one of your projects is at 50% of the limit of the plan. In order to continue to log visits & events, you should upgrade the plan of your project!

+
+
+

How can I upgrade the plan?

+

We offer different plans, each of them follows the stage of your project, so based on the reach, you should upgrade to the most appropriate one for your web platform. It takes 1 minute to upgrade the plan! You can find everything in the "Billing" section in the left menu of your dashboard.

+ Visit your dashboard +
+
+

We are in early phases!

+

Want to become an early adopter? Book a demo with me! I'm Antonio & I'll guide you through all the features and benefits of LitLyx. A big discount is waiting for you❗️❗️❗️

+ Book a Demo with Me! +
+ +
+ + + +` +const TemplateEmailWelcome = ` + + + + + + + LitLyx Onboarding Email + + + + +
+
+

👋 Welcome to LitLyx!

+
+
+

We're Super happy to have you on board! 🚀 At LitLyx, we've designed our service to simplify your life by enabling real-time tracking of visits and custom events for your website or app with minimal setup.

+
+
+

👇 Let's Dive in 👇

+
+
+ +
+
+

See the Documentation

+

Get familiar with our platform by checking out our comprehensive documentation. Everything you need to + know in 1 Min Read Time ⏱️.

+ Go to Docs +
+
+

Visit the Live Demo

+

Experience LitLyx in action by visiting our live demo. See how our features can benefit you in real-time. + We give you a comprehensive dashboard to visualize data on litlyx itself!

+ Visit Live Demo +
+
+

Book a Demo & Became an Early-Adopter

+

Want to became an early-adopter? Book a demo with me!I'm Antonio & i'll guide you through all the + features and benefits of LitLyx. A big discount is waiting for you❗️❗️❗️

+ Book a Demo with Me! +
+ +
+ + + +` + +export async function sendLimitEmail50(target: string) { + try { + await transport.sendMail({ + from: 'helplitlyx@gmail.com', + to: target, + subject: 'Project limit 50%', + html: TemplateEmail50 + }); + return true; + } catch (ex) { + console.error('ERROR SENDING EMAIL', ex); + return false; + } +} + +export async function sendWelcomeEmail(target: string) { + try { + await transport.sendMail({ + from: 'helplitlyx@gmail.com', + to: target, + subject: 'Welcome to Litlyx', + html: TemplateEmailWelcome + }); + return true; + } catch (ex) { + console.error('ERROR SENDING EMAIL', ex); + return false; + } +} \ No newline at end of file diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 0000000..f7e4af6 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext" + }, + "include": ["**/*.ts"], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/shared/utilts/TIME.ts b/shared/utilts/TIME.ts new file mode 100644 index 0000000..3c4948b --- /dev/null +++ b/shared/utilts/TIME.ts @@ -0,0 +1,9 @@ + + +export const SECOND = 1000; +export const MINUTE = SECOND * 60; +export const HOUR = MINUTE * 60; +export const DAY = HOUR * 24; +export const WEEK = HOUR * 7; +export const MONTH = DAY * 30; +export const YEAR = MONTH * 12; \ No newline at end of file diff --git a/shared/utilts/requireEnv.ts b/shared/utilts/requireEnv.ts new file mode 100644 index 0000000..f9b5cdc --- /dev/null +++ b/shared/utilts/requireEnv.ts @@ -0,0 +1,8 @@ + +export function requireEnv(name: string, errorMessage?: string) { + if (!process.env[name]) { + console.error(errorMessage || `ENV variable ${name} is required`); + return process.exit(1); + } + return process.env[name] as string; +} \ No newline at end of file