add shared

This commit is contained in:
Litlyx
2024-06-01 15:27:21 +02:00
parent 9caec3b92b
commit 75f0787c3b
23 changed files with 866 additions and 0 deletions

1
shared/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

4
shared/data/ADMINS.ts Normal file
View File

@@ -0,0 +1,4 @@
export const ADMIN_EMAILS = [
'laura.emily.lovi@gmail.com',
'mangaiomaster@gmail.com'
]

1
shared/data/LITLYX.ts Normal file
View File

@@ -0,0 +1 @@
export const LITLYX_PROJECT_ID = '6643cd08a1854e3b81722ab5';

View File

@@ -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<PREMIUM_PLAN_TAG, PROJECT_LIMIT> = {
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
}
}

View File

@@ -0,0 +1,5 @@
export enum EventType {
VISIT = 1,
EVENT = 2
}

View File

@@ -0,0 +1,5 @@
// Default: 1.1
// ((events + visits) * VALUE) > limit
export const EVENT_LOG_LIMIT_PERCENT = 1.1;

View File

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

10
shared/package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"dependencies": {
"mongoose": "^8.4.0",
"nodemailer": "^6.9.13"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/nodemailer": "^6.4.15"
}
}

232
shared/pnpm-lock.yaml generated Normal file
View File

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

View File

@@ -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<TProject>({
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<TProject>('projects', ProjectSchema);

View File

@@ -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<TProjectCount>({
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<TProjectCount>('project_counts', ProjectCountSchema);

View File

@@ -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<TUser>({
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<TUser>('users', UserSchema);

View File

@@ -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<TUserSettings>({
user_id: { type: Types.ObjectId, unique: true, index: 1 },
active_project_id: Schema.Types.ObjectId,
});
export const UserSettingsModel = model<TUserSettings>('user_settings', UserSettingsSchema);

View File

@@ -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<TAiChatSchema>({
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<TAiChatSchema>('ai_chats', AiChatSchema);

View File

@@ -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<TLimitNotify>({
project_id: { type: Types.ObjectId, index: 1 },
limit1: { type: Boolean },
limit2: { type: Boolean },
limit3: { type: Boolean }
});
export const LimitNotifyModel = model<TLimitNotify>('limit_notifies', LimitNotifySchema);

View File

@@ -0,0 +1,18 @@
import { model, Schema, Types } from 'mongoose';
export type TEvent = {
project_id: Schema.Types.ObjectId,
name: string,
metadata: Record<string, string>,
created_at: Date
}
const EventSchema = new Schema<TEvent>({
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<TEvent>('events', EventSchema);

View File

@@ -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<TSession>({
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<TSession>('sessions', SessionSchema);

View File

@@ -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<TVisit>({
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<TVisit>('visits', VisitSchema);

View File

@@ -0,0 +1,6 @@
import mongoose from "mongoose";
export async function connectDatabase(connectionString: string) {
await mongoose.connect(connectionString);
}

File diff suppressed because one or more lines are too long

10
shared/tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "NodeNext",
"target": "ESNext"
},
"include": ["**/*.ts"],
"exclude": [
"node_modules"
]
}

9
shared/utilts/TIME.ts Normal file
View File

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

View File

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