mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
update
This commit is contained in:
8
broker/.gitignore
vendored
8
broker/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
node_modules
|
||||
static
|
||||
ecosystem.config.cjs
|
||||
dist
|
||||
scripts/start_dev.js
|
||||
package-lock.json
|
||||
build_all.bat
|
||||
tests
|
||||
@@ -1,43 +0,0 @@
|
||||
ARG NODE_VERSION=21
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine as base
|
||||
|
||||
ENV NODE_ENV=development
|
||||
|
||||
# Build stage
|
||||
|
||||
FROM base as build
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
COPY --link broker/package.json broker/pnpm-lock.yaml home/app/
|
||||
|
||||
COPY --link shared/package.json shared/pnpm-lock.yaml /home/shared/
|
||||
|
||||
WORKDIR /home/app
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
WORKDIR /home/shared
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY --link ../broker /home/app
|
||||
|
||||
COPY --link ../shared /home/shared
|
||||
|
||||
WORKDIR /home/app
|
||||
|
||||
RUN pnpm run build_all
|
||||
|
||||
RUN pnpm prune
|
||||
|
||||
# Final stage
|
||||
|
||||
FROM base
|
||||
|
||||
COPY --from=build /home/app /home/app
|
||||
|
||||
WORKDIR /home/app
|
||||
|
||||
EXPOSE ${PORT}
|
||||
|
||||
CMD ["node", "dist/app/src/index.js"]
|
||||
@@ -1,20 +0,0 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'QueueBroker',
|
||||
port: '3999',
|
||||
exec_mode: 'fork',
|
||||
script: './dist/producer/src/index.js',
|
||||
env: {
|
||||
EMAIL_SERVICE: "",
|
||||
BREVO_API_KEY: "",
|
||||
PORT: "",
|
||||
MONGO_CONNECTION_STRING: "",
|
||||
REDIS_URL: "",
|
||||
REDIS_USERNAME: "",
|
||||
REDIS_PASSWORD: "",
|
||||
STREAM_NAME: ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
"^.+.tsx?$": ["ts-jest",{}],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'@services/(.*)': '<rootDir>/../shared/services/$1',
|
||||
'@data/(.*)': '<rootDir>/../shared/data/$1',
|
||||
'@functions/(.*)': '<rootDir>/../shared/functions/$1',
|
||||
'@schema/(.*)': '<rootDir>/../shared/schema/$1',
|
||||
}
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@getbrevo/brevo": "^2.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"mongoose": "^8.3.2",
|
||||
"nodemailer": "^6.9.13",
|
||||
"redis": "^4.6.14",
|
||||
"ua-parser-js": "^1.0.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.13",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"glob": "^10.4.1",
|
||||
"jest": "^29.7.0",
|
||||
"node-ssh": "^13.2.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"name": "litlyx-queue-broker",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start_dev.js",
|
||||
"compile": "tsc",
|
||||
"build": "ts-node scripts/build.ts",
|
||||
"create_db": "cd scripts && ts-node create_database.ts",
|
||||
"build_all": "npm run compile && npm run build && npm run create_db",
|
||||
"docker-build": "docker build -t litlyx-broker -f Dockerfile ../",
|
||||
"docker-inspect": "docker run -it litlyx-broker sh",
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Emily",
|
||||
"license": "MIT",
|
||||
"description": "Queue broker for Litlyx - Saves events to database."
|
||||
}
|
||||
4685
broker/pnpm-lock.yaml
generated
4685
broker/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,253 +0,0 @@
|
||||
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,is_in_european_union
|
||||
49518,en,AF,Africa,RW,Rwanda,0
|
||||
51537,en,AF,Africa,SO,Somalia,0
|
||||
69543,en,AS,Asia,YE,Yemen,0
|
||||
99237,en,AS,Asia,IQ,Iraq,0
|
||||
102358,en,AS,Asia,SA,"Saudi Arabia",0
|
||||
130758,en,AS,Asia,IR,Iran,0
|
||||
146669,en,EU,Europe,CY,Cyprus,1
|
||||
149590,en,AF,Africa,TZ,Tanzania,0
|
||||
163843,en,AS,Asia,SY,Syria,0
|
||||
174982,en,AS,Asia,AM,Armenia,0
|
||||
192950,en,AF,Africa,KE,Kenya,0
|
||||
203312,en,AF,Africa,CD,"DR Congo",0
|
||||
223816,en,AF,Africa,DJ,Djibouti,0
|
||||
226074,en,AF,Africa,UG,Uganda,0
|
||||
239880,en,AF,Africa,CF,"Central African Republic",0
|
||||
241170,en,AF,Africa,SC,Seychelles,0
|
||||
248816,en,AS,Asia,JO,Jordan,0
|
||||
272103,en,AS,Asia,LB,Lebanon,0
|
||||
285570,en,AS,Asia,KW,Kuwait,0
|
||||
286963,en,AS,Asia,OM,Oman,0
|
||||
289688,en,AS,Asia,QA,Qatar,0
|
||||
290291,en,AS,Asia,BH,Bahrain,0
|
||||
290557,en,AS,Asia,AE,"United Arab Emirates",0
|
||||
294640,en,AS,Asia,IL,Israel,0
|
||||
298795,en,AS,Asia,TR,Türkiye,0
|
||||
337996,en,AF,Africa,ET,Ethiopia,0
|
||||
338010,en,AF,Africa,ER,Eritrea,0
|
||||
357994,en,AF,Africa,EG,Egypt,0
|
||||
366755,en,AF,Africa,SD,Sudan,0
|
||||
390903,en,EU,Europe,GR,Greece,1
|
||||
433561,en,AF,Africa,BI,Burundi,0
|
||||
453733,en,EU,Europe,EE,Estonia,1
|
||||
458258,en,EU,Europe,LV,Latvia,1
|
||||
587116,en,AS,Asia,AZ,Azerbaijan,0
|
||||
597427,en,EU,Europe,LT,Lithuania,1
|
||||
607072,en,EU,Europe,SJ,"Svalbard and Jan Mayen",0
|
||||
614540,en,AS,Asia,GE,Georgia,0
|
||||
617790,en,EU,Europe,MD,Moldova,0
|
||||
630336,en,EU,Europe,BY,Belarus,0
|
||||
660013,en,EU,Europe,FI,Finland,1
|
||||
661882,en,EU,Europe,AX,"Åland Islands",1
|
||||
690791,en,EU,Europe,UA,Ukraine,0
|
||||
718075,en,EU,Europe,MK,"North Macedonia",0
|
||||
719819,en,EU,Europe,HU,Hungary,1
|
||||
732800,en,EU,Europe,BG,Bulgaria,1
|
||||
783754,en,EU,Europe,AL,Albania,0
|
||||
798544,en,EU,Europe,PL,Poland,1
|
||||
798549,en,EU,Europe,RO,Romania,1
|
||||
831053,en,EU,Europe,XK,Kosovo,0
|
||||
878675,en,AF,Africa,ZW,Zimbabwe,0
|
||||
895949,en,AF,Africa,ZM,Zambia,0
|
||||
921929,en,AF,Africa,KM,Comoros,0
|
||||
927384,en,AF,Africa,MW,Malawi,0
|
||||
932692,en,AF,Africa,LS,Lesotho,0
|
||||
933860,en,AF,Africa,BW,Botswana,0
|
||||
934292,en,AF,Africa,MU,Mauritius,0
|
||||
934841,en,AF,Africa,SZ,Eswatini,0
|
||||
935317,en,AF,Africa,RE,Réunion,1
|
||||
953987,en,AF,Africa,ZA,"South Africa",0
|
||||
1024031,en,AF,Africa,YT,Mayotte,1
|
||||
1036973,en,AF,Africa,MZ,Mozambique,0
|
||||
1062947,en,AF,Africa,MG,Madagascar,0
|
||||
1149361,en,AS,Asia,AF,Afghanistan,0
|
||||
1168579,en,AS,Asia,PK,Pakistan,0
|
||||
1210997,en,AS,Asia,BD,Bangladesh,0
|
||||
1218197,en,AS,Asia,TM,Turkmenistan,0
|
||||
1220409,en,AS,Asia,TJ,Tajikistan,0
|
||||
1227603,en,AS,Asia,LK,"Sri Lanka",0
|
||||
1252634,en,AS,Asia,BT,Bhutan,0
|
||||
1269750,en,AS,Asia,IN,India,0
|
||||
1282028,en,AS,Asia,MV,Maldives,0
|
||||
1282588,en,AS,Asia,IO,"British Indian Ocean Territory",0
|
||||
1282988,en,AS,Asia,NP,Nepal,0
|
||||
1327865,en,AS,Asia,MM,Myanmar,0
|
||||
1512440,en,AS,Asia,UZ,Uzbekistan,0
|
||||
1522867,en,AS,Asia,KZ,Kazakhstan,0
|
||||
1527747,en,AS,Asia,KG,Kyrgyzstan,0
|
||||
1546748,en,AN,Antarctica,TF,"French Southern Territories",0
|
||||
1547314,en,AN,Antarctica,HM,"Heard and McDonald Islands",0
|
||||
1547376,en,AS,Asia,CC,"Cocos (Keeling) Islands",0
|
||||
1559582,en,OC,Oceania,PW,Palau,0
|
||||
1562822,en,AS,Asia,VN,Vietnam,0
|
||||
1605651,en,AS,Asia,TH,Thailand,0
|
||||
1643084,en,AS,Asia,ID,Indonesia,0
|
||||
1655842,en,AS,Asia,LA,Laos,0
|
||||
1668284,en,AS,Asia,TW,Taiwan,0
|
||||
1694008,en,AS,Asia,PH,Philippines,0
|
||||
1733045,en,AS,Asia,MY,Malaysia,0
|
||||
1814991,en,AS,Asia,CN,China,0
|
||||
1819730,en,AS,Asia,HK,"Hong Kong",0
|
||||
1820814,en,AS,Asia,BN,Brunei,0
|
||||
1821275,en,AS,Asia,MO,Macao,0
|
||||
1831722,en,AS,Asia,KH,Cambodia,0
|
||||
1835841,en,AS,Asia,KR,"South Korea",0
|
||||
1861060,en,AS,Asia,JP,Japan,0
|
||||
1873107,en,AS,Asia,KP,"North Korea",0
|
||||
1880251,en,AS,Asia,SG,Singapore,0
|
||||
1899402,en,OC,Oceania,CK,"Cook Islands",0
|
||||
1966436,en,OC,Oceania,TL,Timor-Leste,0
|
||||
2017370,en,EU,Europe,RU,Russia,0
|
||||
2029969,en,AS,Asia,MN,Mongolia,0
|
||||
2077456,en,OC,Oceania,AU,Australia,0
|
||||
2078138,en,OC,Oceania,CX,"Christmas Island",0
|
||||
2080185,en,OC,Oceania,MH,"Marshall Islands",0
|
||||
2081918,en,OC,Oceania,FM,"Federated States of Micronesia",0
|
||||
2088628,en,OC,Oceania,PG,"Papua New Guinea",0
|
||||
2103350,en,OC,Oceania,SB,"Solomon Islands",0
|
||||
2110297,en,OC,Oceania,TV,Tuvalu,0
|
||||
2110425,en,OC,Oceania,NR,Nauru,0
|
||||
2134431,en,OC,Oceania,VU,Vanuatu,0
|
||||
2139685,en,OC,Oceania,NC,"New Caledonia",0
|
||||
2155115,en,OC,Oceania,NF,"Norfolk Island",0
|
||||
2186224,en,OC,Oceania,NZ,"New Zealand",0
|
||||
2205218,en,OC,Oceania,FJ,Fiji,0
|
||||
2215636,en,AF,Africa,LY,Libya,0
|
||||
2233387,en,AF,Africa,CM,Cameroon,0
|
||||
2245662,en,AF,Africa,SN,Senegal,0
|
||||
2260494,en,AF,Africa,CG,"Congo Republic",0
|
||||
2264397,en,EU,Europe,PT,Portugal,1
|
||||
2275384,en,AF,Africa,LR,Liberia,0
|
||||
2287781,en,AF,Africa,CI,"Ivory Coast",0
|
||||
2300660,en,AF,Africa,GH,Ghana,0
|
||||
2309096,en,AF,Africa,GQ,"Equatorial Guinea",0
|
||||
2328926,en,AF,Africa,NG,Nigeria,0
|
||||
2361809,en,AF,Africa,BF,"Burkina Faso",0
|
||||
2363686,en,AF,Africa,TG,Togo,0
|
||||
2372248,en,AF,Africa,GW,Guinea-Bissau,0
|
||||
2378080,en,AF,Africa,MR,Mauritania,0
|
||||
2395170,en,AF,Africa,BJ,Benin,0
|
||||
2400553,en,AF,Africa,GA,Gabon,0
|
||||
2403846,en,AF,Africa,SL,"Sierra Leone",0
|
||||
2410758,en,AF,Africa,ST,"São Tomé and Príncipe",0
|
||||
2411586,en,EU,Europe,GI,Gibraltar,0
|
||||
2413451,en,AF,Africa,GM,Gambia,0
|
||||
2420477,en,AF,Africa,GN,Guinea,0
|
||||
2434508,en,AF,Africa,TD,Chad,0
|
||||
2440476,en,AF,Africa,NE,Niger,0
|
||||
2453866,en,AF,Africa,ML,Mali,0
|
||||
2461445,en,AF,Africa,EH,"Western Sahara",0
|
||||
2464461,en,AF,Africa,TN,Tunisia,0
|
||||
2510769,en,EU,Europe,ES,Spain,1
|
||||
2542007,en,AF,Africa,MA,Morocco,0
|
||||
2562770,en,EU,Europe,MT,Malta,1
|
||||
2589581,en,AF,Africa,DZ,Algeria,0
|
||||
2622320,en,EU,Europe,FO,"Faroe Islands",0
|
||||
2623032,en,EU,Europe,DK,Denmark,1
|
||||
2629691,en,EU,Europe,IS,Iceland,0
|
||||
2635167,en,EU,Europe,GB,"United Kingdom",0
|
||||
2658434,en,EU,Europe,CH,Switzerland,0
|
||||
2661886,en,EU,Europe,SE,Sweden,1
|
||||
2750405,en,EU,Europe,NL,"The Netherlands",1
|
||||
2782113,en,EU,Europe,AT,Austria,1
|
||||
2802361,en,EU,Europe,BE,Belgium,1
|
||||
2921044,en,EU,Europe,DE,Germany,1
|
||||
2960313,en,EU,Europe,LU,Luxembourg,1
|
||||
2963597,en,EU,Europe,IE,Ireland,1
|
||||
2993457,en,EU,Europe,MC,Monaco,0
|
||||
3017382,en,EU,Europe,FR,France,1
|
||||
3041565,en,EU,Europe,AD,Andorra,0
|
||||
3042058,en,EU,Europe,LI,Liechtenstein,0
|
||||
3042142,en,EU,Europe,JE,Jersey,0
|
||||
3042225,en,EU,Europe,IM,"Isle of Man",0
|
||||
3042362,en,EU,Europe,GG,Guernsey,0
|
||||
3057568,en,EU,Europe,SK,Slovakia,1
|
||||
3077311,en,EU,Europe,CZ,Czechia,1
|
||||
3144096,en,EU,Europe,NO,Norway,0
|
||||
3164670,en,EU,Europe,VA,"Vatican City",0
|
||||
3168068,en,EU,Europe,SM,"San Marino",0
|
||||
3175395,en,EU,Europe,IT,Italy,1
|
||||
3190538,en,EU,Europe,SI,Slovenia,1
|
||||
3194884,en,EU,Europe,ME,Montenegro,0
|
||||
3202326,en,EU,Europe,HR,Croatia,1
|
||||
3277605,en,EU,Europe,BA,"Bosnia and Herzegovina",0
|
||||
3351879,en,AF,Africa,AO,Angola,0
|
||||
3355338,en,AF,Africa,NA,Namibia,0
|
||||
3370751,en,AF,Africa,SH,"Saint Helena",0
|
||||
3371123,en,AN,Antarctica,BV,"Bouvet Island",0
|
||||
3374084,en,NA,"North America",BB,Barbados,0
|
||||
3374766,en,AF,Africa,CV,"Cabo Verde",0
|
||||
3378535,en,SA,"South America",GY,Guyana,0
|
||||
3381670,en,SA,"South America",GF,"French Guiana",1
|
||||
3382998,en,SA,"South America",SR,Suriname,0
|
||||
3424932,en,NA,"North America",PM,"Saint Pierre and Miquelon",0
|
||||
3425505,en,NA,"North America",GL,Greenland,0
|
||||
3437598,en,SA,"South America",PY,Paraguay,0
|
||||
3439705,en,SA,"South America",UY,Uruguay,0
|
||||
3469034,en,SA,"South America",BR,Brazil,0
|
||||
3474414,en,SA,"South America",FK,"Falkland Islands",0
|
||||
3474415,en,AN,Antarctica,GS,"South Georgia and the South Sandwich Islands",0
|
||||
3489940,en,NA,"North America",JM,Jamaica,0
|
||||
3508796,en,NA,"North America",DO,"Dominican Republic",0
|
||||
3562981,en,NA,"North America",CU,Cuba,0
|
||||
3570311,en,NA,"North America",MQ,Martinique,1
|
||||
3572887,en,NA,"North America",BS,Bahamas,0
|
||||
3573345,en,NA,"North America",BM,Bermuda,0
|
||||
3573511,en,NA,"North America",AI,Anguilla,0
|
||||
3573591,en,NA,"North America",TT,"Trinidad and Tobago",0
|
||||
3575174,en,NA,"North America",KN,"St Kitts and Nevis",0
|
||||
3575830,en,NA,"North America",DM,Dominica,0
|
||||
3576396,en,NA,"North America",AG,"Antigua and Barbuda",0
|
||||
3576468,en,NA,"North America",LC,"Saint Lucia",0
|
||||
3576916,en,NA,"North America",TC,"Turks and Caicos Islands",0
|
||||
3577279,en,NA,"North America",AW,Aruba,0
|
||||
3577718,en,NA,"North America",VG,"British Virgin Islands",0
|
||||
3577815,en,NA,"North America",VC,"St Vincent and Grenadines",0
|
||||
3578097,en,NA,"North America",MS,Montserrat,0
|
||||
3578421,en,NA,"North America",MF,"Saint Martin",1
|
||||
3578476,en,NA,"North America",BL,"Saint Barthélemy",0
|
||||
3579143,en,NA,"North America",GP,Guadeloupe,1
|
||||
3580239,en,NA,"North America",GD,Grenada,0
|
||||
3580718,en,NA,"North America",KY,"Cayman Islands",0
|
||||
3582678,en,NA,"North America",BZ,Belize,0
|
||||
3585968,en,NA,"North America",SV,"El Salvador",0
|
||||
3595528,en,NA,"North America",GT,Guatemala,0
|
||||
3608932,en,NA,"North America",HN,Honduras,0
|
||||
3617476,en,NA,"North America",NI,Nicaragua,0
|
||||
3624060,en,NA,"North America",CR,"Costa Rica",0
|
||||
3625428,en,SA,"South America",VE,Venezuela,0
|
||||
3658394,en,SA,"South America",EC,Ecuador,0
|
||||
3686110,en,SA,"South America",CO,Colombia,0
|
||||
3703430,en,NA,"North America",PA,Panama,0
|
||||
3723988,en,NA,"North America",HT,Haiti,0
|
||||
3865483,en,SA,"South America",AR,Argentina,0
|
||||
3895114,en,SA,"South America",CL,Chile,0
|
||||
3923057,en,SA,"South America",BO,Bolivia,0
|
||||
3932488,en,SA,"South America",PE,Peru,0
|
||||
3996063,en,NA,"North America",MX,Mexico,0
|
||||
4030656,en,OC,Oceania,PF,"French Polynesia",0
|
||||
4030699,en,OC,Oceania,PN,"Pitcairn Islands",0
|
||||
4030945,en,OC,Oceania,KI,Kiribati,0
|
||||
4031074,en,OC,Oceania,TK,Tokelau,0
|
||||
4032283,en,OC,Oceania,TO,Tonga,0
|
||||
4034749,en,OC,Oceania,WF,"Wallis and Futuna",0
|
||||
4034894,en,OC,Oceania,WS,Samoa,0
|
||||
4036232,en,OC,Oceania,NU,Niue,0
|
||||
4041468,en,OC,Oceania,MP,"Northern Mariana Islands",0
|
||||
4043988,en,OC,Oceania,GU,Guam,0
|
||||
4566966,en,NA,"North America",PR,"Puerto Rico",0
|
||||
4796775,en,NA,"North America",VI,"U.S. Virgin Islands",0
|
||||
5854968,en,OC,Oceania,UM,"U.S. Outlying Islands",0
|
||||
5880801,en,OC,Oceania,AS,"American Samoa",0
|
||||
6251999,en,NA,"North America",CA,Canada,0
|
||||
6252001,en,NA,"North America",US,"United States",0
|
||||
6254930,en,AS,Asia,PS,Palestine,0
|
||||
6255147,en,AS,Asia,,,0
|
||||
6255148,en,EU,Europe,,,0
|
||||
6290252,en,EU,Europe,RS,Serbia,0
|
||||
6697173,en,AN,Antarctica,AQ,Antarctica,0
|
||||
7609695,en,NA,"North America",SX,"Sint Maarten",0
|
||||
7626836,en,NA,"North America",CW,Curaçao,0
|
||||
7626844,en,NA,"North America",BQ,"Bonaire, Sint Eustatius, and Saba",0
|
||||
7909807,en,AF,Africa,SS,"South Sudan",0
|
||||
|
@@ -1,17 +0,0 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import { globSync } from 'glob';
|
||||
const tsconfigContent = fs.readFileSync('tsconfig.json', 'utf8');
|
||||
const tsconfigObject = JSON.parse(tsconfigContent);
|
||||
const paths = tsconfigObject.compilerOptions.paths;
|
||||
const filesList = globSync('dist/**/*.js');
|
||||
filesList.forEach(file => {
|
||||
let raw = fs.readFileSync(file, 'utf8');
|
||||
for (const path in paths) {
|
||||
const deep = (file.match(/\\|\//g) || []).length;
|
||||
const pathText = path.replace('*', '');
|
||||
const toReplaceText = new RegExp(`"${pathText}(.*?)"`, 'g');
|
||||
raw = raw.replace(toReplaceText, `"${new Array(deep - 2).fill('../').join('')}${paths[path][0].replace('*', '')}${'$1'}"`);
|
||||
}
|
||||
fs.writeFileSync(file, raw);
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import fs from 'fs';
|
||||
|
||||
function createIpDatabase() {
|
||||
const data = fs.readFileSync('GeoLite2-Country-Blocks-IPv4.csv', 'utf8');
|
||||
const rows = data.split('\n');
|
||||
rows.splice(0, 1);
|
||||
rows.splice(-1);
|
||||
const parsed: [number, string, number][] = [];
|
||||
for (const row of rows) {
|
||||
const lineData = row.trim().split(',');
|
||||
parsed.push([
|
||||
parseInt(lineData[0].split('.')[0]),
|
||||
lineData[0],
|
||||
parseInt(lineData[1] || '0')
|
||||
]);
|
||||
}
|
||||
fs.writeFileSync('../dist/ipv4-db.json', JSON.stringify(parsed));
|
||||
|
||||
}
|
||||
|
||||
function createCountryDatabase() {
|
||||
const data = fs.readFileSync('GeoLite2-Country-Locations-en.csv', 'utf8');
|
||||
const rows = data.split('\n');
|
||||
rows.splice(0, 1);
|
||||
rows.splice(-1);
|
||||
const parsed: [number, string, string][] = [];
|
||||
for (const row of rows) {
|
||||
const lineData = row.trim().split(',');
|
||||
parsed.push([parseInt(lineData[0]), lineData[2], lineData[4]]);
|
||||
}
|
||||
fs.writeFileSync('../dist/countries-db.json', JSON.stringify(parsed));
|
||||
|
||||
}
|
||||
|
||||
createIpDatabase();
|
||||
createCountryDatabase();
|
||||
@@ -1,67 +0,0 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
|
||||
import EmailService from '@services/EmailService';
|
||||
import { requireEnv } from "@utils/requireEnv";
|
||||
import { TProjectLimit } from "@schema/ProjectsLimits";
|
||||
|
||||
if (process.env.EMAIL_SERVICE) {
|
||||
EmailService.init(requireEnv('BREVO_API_KEY'));
|
||||
}
|
||||
|
||||
export async function checkLimitsForEmail(projectCounts: TProjectLimit) {
|
||||
|
||||
const project_id = projectCounts.project_id;
|
||||
const hasNotifyEntry = await LimitNotifyModel.findOne({ project_id });
|
||||
if (!hasNotifyEntry) {
|
||||
await LimitNotifyModel.create({ project_id, limit1: false, limit2: false, limit3: false })
|
||||
}
|
||||
|
||||
if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit)) {
|
||||
|
||||
const notify = await LimitNotifyModel.findOne({ project_id });
|
||||
if (notify && notify.limit3 === true) return;
|
||||
|
||||
const project = await ProjectModel.findById(project_id);
|
||||
if (!project) return;
|
||||
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return;
|
||||
|
||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmailMax(owner.email, project.name);
|
||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: true });
|
||||
|
||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.9)) {
|
||||
|
||||
const notify = await LimitNotifyModel.findOne({ project_id });
|
||||
if (notify && notify.limit2 === true) return;
|
||||
|
||||
const project = await ProjectModel.findById(project_id);
|
||||
if (!project) return;
|
||||
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return;
|
||||
|
||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmail90(owner.email, project.name);
|
||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: true, limit3: false });
|
||||
|
||||
} else if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit * 0.5)) {
|
||||
|
||||
const notify = await LimitNotifyModel.findOne({ project_id });
|
||||
if (notify && notify.limit1 === true) return;
|
||||
|
||||
const project = await ProjectModel.findById(project_id);
|
||||
if (!project) return;
|
||||
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return;
|
||||
|
||||
if (process.env.EMAIL_SERVICE) await EmailService.sendLimitEmail50(owner.email, project.name);
|
||||
await LimitNotifyModel.updateOne({ project_id: projectCounts.project_id }, { limit1: true, limit2: false, limit3: false });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
|
||||
export function getDeviceFromScreenSize(width: number, height: number) {
|
||||
const totalArea = width * height;
|
||||
|
||||
const mobileArea = 375 * 667;
|
||||
const tabletMinArea = 768 * 1366
|
||||
const tabletMaxArea = 1024 * 1366
|
||||
|
||||
const isMobile = totalArea <= mobileArea;
|
||||
const isTablet = totalArea >= tabletMinArea && totalArea <= tabletMaxArea;
|
||||
|
||||
if (isMobile) return 'mobile';
|
||||
if (isTablet) return 'tablet'
|
||||
return 'desktop';
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
|
||||
import { RedisStreamService } from '@services/RedisStreamService';
|
||||
import { requireEnv } from '@utils/requireEnv';
|
||||
import { EventModel } from '@schema/metrics/EventSchema';
|
||||
import { SessionModel } from '@schema/metrics/SessionSchema';
|
||||
import { ProjectModel } from '@schema/ProjectSchema';
|
||||
import { ProjectLimitModel } from '@schema/ProjectsLimits';
|
||||
import { ProjectCountModel } from '@schema/ProjectsCounts';
|
||||
import { EVENT_LOG_LIMIT_PERCENT } from '@data/broker/Limits';
|
||||
import { checkLimitsForEmail } from './Controller';
|
||||
import { lookup } from './lookup';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
import { VisitModel } from '@schema/metrics/VisitSchema';
|
||||
|
||||
|
||||
export async function startStreamLoop() {
|
||||
|
||||
await RedisStreamService.connect();
|
||||
|
||||
await RedisStreamService.startReadingLoop({
|
||||
streamName: requireEnv('STREAM_NAME'),
|
||||
delay: { base: 10, empty: 5000 },
|
||||
readBlock: 2000,
|
||||
consumer: 'consumer_' + process.env.NODE_APP_INSTANCE
|
||||
}, processStreamEvent);
|
||||
|
||||
}
|
||||
|
||||
|
||||
export async function processStreamEvent(data: Record<string, string>) {
|
||||
try {
|
||||
const eventType = data._type;
|
||||
if (!eventType) return;
|
||||
|
||||
const { pid, sessionHash } = data;
|
||||
|
||||
const project = await ProjectModel.exists({ _id: pid });
|
||||
if (!project) return;
|
||||
|
||||
|
||||
if (eventType === 'event') return await process_event(data, sessionHash);
|
||||
if (eventType === 'keep_alive') return await process_keep_alive(data, sessionHash);
|
||||
if (eventType === 'visit') return await process_visit(data, sessionHash);
|
||||
|
||||
} catch (ex: any) {
|
||||
console.error('ERROR PROCESSING STREAM EVENT', ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function checkLimits(project_id: string) {
|
||||
const projectLimits = await ProjectLimitModel.findOne({ project_id });
|
||||
if (!projectLimits) return false;
|
||||
const TOTAL_COUNT = projectLimits.events + projectLimits.visits;
|
||||
const COUNT_LIMIT = projectLimits.limit;
|
||||
if ((TOTAL_COUNT) > COUNT_LIMIT * EVENT_LOG_LIMIT_PERCENT) return false;
|
||||
await checkLimitsForEmail(projectLimits);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function process_visit(data: Record<string, string>, sessionHash: string) {
|
||||
|
||||
const { pid, ip, website, page, referrer, userAgent, flowHash } = data;
|
||||
|
||||
const canLog = await checkLimits(pid);
|
||||
if (!canLog) return;
|
||||
|
||||
let referrerParsed;
|
||||
try {
|
||||
referrerParsed = new URL(referrer);
|
||||
} catch (ex) {
|
||||
referrerParsed = { hostname: referrer };
|
||||
}
|
||||
|
||||
const geoLocation = lookup(ip);
|
||||
|
||||
const userAgentParsed = UAParser(userAgent);
|
||||
|
||||
const device = userAgentParsed.device.type;
|
||||
|
||||
const visit = new VisitModel({
|
||||
project_id: pid, website, page, referrer: referrerParsed.hostname,
|
||||
browser: userAgentParsed.browser.name || 'NO_BROWSER',
|
||||
os: userAgentParsed.os.name || 'NO_OS',
|
||||
device: device ? device : (userAgentParsed.browser.name ? 'desktop' : undefined),
|
||||
session: sessionHash,
|
||||
flowHash,
|
||||
continent: geoLocation[0],
|
||||
country: geoLocation[1],
|
||||
});
|
||||
|
||||
await visit.save();
|
||||
|
||||
|
||||
|
||||
await ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'visits': 1 } }, { upsert: true });
|
||||
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { 'visits': 1 } });
|
||||
|
||||
}
|
||||
|
||||
async function process_keep_alive(data: Record<string, string>, sessionHash: string) {
|
||||
|
||||
const { pid, instant, flowHash } = data;
|
||||
|
||||
const canLog = await checkLimits(pid);
|
||||
if (!canLog) return;
|
||||
|
||||
const existingSession = await SessionModel.findOne({ project_id: pid, session: sessionHash }, { _id: 1 });
|
||||
if (!existingSession) {
|
||||
await ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'sessions': 1 } }, { upsert: true });
|
||||
}
|
||||
|
||||
if (instant == "true") {
|
||||
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
||||
$inc: { duration: 0 },
|
||||
flowHash,
|
||||
updated_at: Date.now()
|
||||
}, { upsert: true });
|
||||
} else {
|
||||
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
||||
$inc: { duration: 1 },
|
||||
flowHash,
|
||||
updated_at: Date.now()
|
||||
}, { upsert: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function process_event(data: Record<string, string>, sessionHash: string) {
|
||||
|
||||
const { name, metadata, pid, flowHash } = data;
|
||||
|
||||
const canLog = await checkLimits(pid);
|
||||
if (!canLog) return;
|
||||
|
||||
let metadataObject;
|
||||
try {
|
||||
if (metadata) metadataObject = JSON.parse(metadata);
|
||||
} catch (ex) {
|
||||
metadataObject = { error: 'Error parsing metadata' }
|
||||
}
|
||||
|
||||
const event = new EventModel({ project_id: pid, name, flowHash, metadata: metadataObject, session: sessionHash });
|
||||
await event.save();
|
||||
|
||||
await ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'events': 1 } }, { upsert: true });
|
||||
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { 'events': 1 } });
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
|
||||
import { requireEnv } from '@utils/requireEnv';
|
||||
import { connectDatabase } from '@services/DatabaseService';
|
||||
import { startStreamLoop } from './StreamLoopController';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
|
||||
connectDatabase(requireEnv('MONGO_CONNECTION_STRING'));
|
||||
|
||||
import HealthRouter from './routes/HealthRouter';
|
||||
app.use('/health', HealthRouter);
|
||||
|
||||
app.listen(requireEnv('PORT'), () => console.log(`Listening on port ${requireEnv('PORT')}`));
|
||||
|
||||
startStreamLoop();
|
||||
@@ -1,42 +0,0 @@
|
||||
import fs from 'fs';
|
||||
|
||||
const ipsData = JSON.parse(fs.readFileSync('./dist/ipv4-db.json', 'utf8'));
|
||||
const countriesData = JSON.parse(fs.readFileSync('./dist/countries-db.json', 'utf8'));
|
||||
|
||||
function inRange(ip: string, cidr: string) {
|
||||
const [subnet, mask] = cidr.split('/');
|
||||
const ipBytes = ip.split('.').map(Number);
|
||||
const subnetBytes = subnet.split('.').map(Number);
|
||||
|
||||
const ipInt = (ipBytes[0] << 24) | (ipBytes[1] << 16) | (ipBytes[2] << 8) | ipBytes[3];
|
||||
const subnetInt = (subnetBytes[0] << 24) | (subnetBytes[1] << 16) | (subnetBytes[2] << 8) | subnetBytes[3];
|
||||
|
||||
const maskInt = 0xffffffff << (32 - parseInt(mask));
|
||||
|
||||
return (ipInt & maskInt) === (subnetInt & maskInt);
|
||||
}
|
||||
|
||||
function getCountryFromId(id: number) {
|
||||
for (const country of countriesData) {
|
||||
if (country[0] == id) {
|
||||
return country;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function lookup(ip: string) {
|
||||
try {
|
||||
const startPiece = parseInt(ip.split('.')[0]);
|
||||
for (const target of ipsData) {
|
||||
const matchingStartPiece = target[0] == startPiece;
|
||||
if (!matchingStartPiece) continue;
|
||||
if (!inRange(ip, target[1])) continue;
|
||||
const country = getCountryFromId(target[2]);
|
||||
return [country[1], country[2]];
|
||||
}
|
||||
return ['??', '??'];
|
||||
} catch (ex) {
|
||||
console.error('ERROR DURING LOOKUP', ex);
|
||||
return ['??', '??'];
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
return res.json({ alive: true });
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
return res.status(500).json({ error: 'ERROR' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Request } from "express";
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function getIPFromRequest(req: Request) {
|
||||
const ip = req.header('X-Real-IP') || req.header('X-Forwarded-For') || '0.0.0.0';
|
||||
return ip;
|
||||
}
|
||||
|
||||
|
||||
export function createSessionHash(website: string, ip: string, userAgent: string) {
|
||||
const dailySalt = new Date().toLocaleDateString('it-IT');
|
||||
const sessionClean = dailySalt + website + ip + userAgent;
|
||||
const sessionHash = crypto.createHash('md5').update(sessionClean).digest("hex");
|
||||
return sessionHash;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "dist",
|
||||
"types": [
|
||||
"node",
|
||||
"jest"
|
||||
],
|
||||
"paths": {
|
||||
"@schema/*": [
|
||||
"../shared/schema/*"
|
||||
],
|
||||
"@services/*": [
|
||||
"../shared/services/*"
|
||||
],
|
||||
"@data/*": [
|
||||
"../shared/data/*"
|
||||
],
|
||||
"@functions/*": [
|
||||
"../shared/functions/*"
|
||||
],
|
||||
"@utils/*": [
|
||||
"../shared/utils/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"scripts/**/*.ts",
|
||||
"tests/**/*.test.ts",
|
||||
"tests/utils.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -76,7 +76,6 @@ async function confirmSnapshot() {
|
||||
</div>
|
||||
|
||||
<div class="mt-4 justify-center flex w-full">
|
||||
|
||||
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
|
||||
<UButton class="w-full" color="primary" variant="solid">
|
||||
<div class="flex items-center justify-center w-full gap-2">
|
||||
@@ -97,8 +96,6 @@ async function confirmSnapshot() {
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
157
dashboard/components/integrations/SupabaseChartDialog.vue
Normal file
157
dashboard/components/integrations/SupabaseChartDialog.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { sub, isSameDay, type Duration } from 'date-fns'
|
||||
|
||||
type ChartType = 'bar' | 'line';
|
||||
const chartTypeOptions: { value: ChartType, label: string }[] = [
|
||||
{ value: 'bar', label: 'Bar chart' },
|
||||
{ value: 'line', label: 'Line chart' },
|
||||
]
|
||||
|
||||
type yAxisMode = 'count';
|
||||
const yAxisModeOptions: { value: yAxisMode, label: string }[] = [
|
||||
{ value: 'count', label: 'Count fields' },
|
||||
]
|
||||
|
||||
type Slice = 'day' | 'month';
|
||||
const sliceOptions: Slice[] = ['day', 'month'];
|
||||
|
||||
const chartType = ref<ChartType>('line');
|
||||
const tableName = ref<string>('');
|
||||
const xAxis = ref<string>('');
|
||||
const yAxisMode = ref<yAxisMode>('count');
|
||||
const slice = ref<Slice>('day');
|
||||
const visualizationName = ref<string>('');
|
||||
|
||||
|
||||
const ranges = [
|
||||
{ label: 'Last 7 days', duration: { days: 7 } },
|
||||
{ label: 'Last 14 days', duration: { days: 14 } },
|
||||
{ label: 'Last 30 days', duration: { days: 30 } },
|
||||
{ label: 'Last 3 months', duration: { months: 3 } },
|
||||
{ label: 'Last 6 months', duration: { months: 6 } },
|
||||
{ label: 'Last year', duration: { years: 1 } }
|
||||
]
|
||||
const timeframe = ref<{ start: Date, end: Date }>({ start: sub(new Date(), { days: 14 }), end: new Date() })
|
||||
|
||||
function isRangeSelected(duration: Duration) {
|
||||
return isSameDay(timeframe.value.start, sub(new Date(), duration)) && isSameDay(timeframe.value.end, new Date())
|
||||
}
|
||||
|
||||
function selectRange(duration: Duration) {
|
||||
timeframe.value = { start: sub(new Date(), duration), end: new Date() }
|
||||
}
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
const { closeDialog } = useCustomDialog();
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
const { integrationsCredentials,testConnection } = useSupabase();
|
||||
|
||||
async function generate() {
|
||||
const credentials = integrationsCredentials.data.value;
|
||||
if (!credentials?.supabase) return createAlert('Credentials not found', 'Please add supabase credentials on the integration page', 'far fa-error', 5000);
|
||||
const connectionStatus = await testConnection();
|
||||
if (!connectionStatus) return createAlert('Invalid supabase credentials', 'Please check your supabase credentials on the integration page', 'far fa-error', 5000);
|
||||
|
||||
try {
|
||||
const creation = await $fetch('/api/integrations/supabase/add', {
|
||||
...signHeaders({
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: visualizationName.value,
|
||||
chart_type: chartType.value,
|
||||
table_name: tableName.value,
|
||||
xField: xAxis.value,
|
||||
yMode: yAxisMode.value,
|
||||
from: timeframe.value.start,
|
||||
to: timeframe.value.end,
|
||||
slice: slice.value
|
||||
})
|
||||
})
|
||||
|
||||
createAlert('Integration generated', 'Integration generated successfully', 'far fa-check-circle', 5000);
|
||||
closeDialog();
|
||||
} catch (ex: any) {
|
||||
createAlert('Error generating integrations', ex.response._data.message.toString(), 'far fa-error', 5000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div> Visualization name </div>
|
||||
<div>
|
||||
<LyxUiInput class="w-full px-2 py-1" v-model="visualizationName"></LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div> Chart type </div>
|
||||
<USelect v-model="chartType" :options="chartTypeOptions" />
|
||||
</div>
|
||||
<div>
|
||||
<div> Table name </div>
|
||||
<div>
|
||||
<LyxUiInput class="w-full px-2 py-1" v-model="tableName"></LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> X axis field </div>
|
||||
<div>
|
||||
<LyxUiInput class="w-full px-2 py-1" v-model="xAxis"></LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> Y axis mode </div>
|
||||
<div>
|
||||
<USelect v-model="yAxisMode" :options="yAxisModeOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> Timeframe </div>
|
||||
<div>
|
||||
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
|
||||
<UButton class="w-full" color="primary" variant="solid">
|
||||
<div class="flex items-center justify-center w-full gap-2">
|
||||
<i class="i-heroicons-calendar-days-20-solid"></i>
|
||||
{{ timeframe.start.toLocaleDateString() }} - {{ timeframe.end.toLocaleDateString() }}
|
||||
</div>
|
||||
</UButton>
|
||||
<template #panel="{ close }">
|
||||
<div class="flex items-center sm:divide-x divide-gray-200 dark:divide-gray-800">
|
||||
<div class="hidden sm:flex flex-col py-4">
|
||||
<UButton v-for="(range, index) in ranges" :key="index" :label="range.label" color="gray"
|
||||
variant="ghost" class="rounded-none px-6"
|
||||
:class="[isRangeSelected(range.duration) ? 'bg-gray-100 dark:bg-gray-800' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50']"
|
||||
truncate @click="selectRange(range.duration)" />
|
||||
</div>
|
||||
|
||||
<DatePicker v-model="timeframe" @close="close" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> View mode </div>
|
||||
<div>
|
||||
<USelect v-model="slice" :options="sliceOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LyxUiButton type="primary" @click="generate()">
|
||||
Generate
|
||||
</LyxUiButton>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
170
dashboard/components/integrations/SupabaseLineChart.vue
Normal file
170
dashboard/components/integrations/SupabaseLineChart.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import type { TSupabaseIntegration } from '@schema/integrations/SupabaseIntegrationSchema';
|
||||
import type { ChartData, ChartOptions } from 'chart.js';
|
||||
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||
|
||||
const props = defineProps<{ integration_id: string }>();
|
||||
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
const supabaseData = ref<{ labels: string[], data: number[] }>();
|
||||
const supabaseError = ref<string | undefined>(undefined);
|
||||
const supabaseFetching = ref<boolean>(false);
|
||||
|
||||
const { getRemoteData } = useSupabase();
|
||||
|
||||
function createGradient() {
|
||||
|
||||
const c = document.createElement('canvas');
|
||||
const ctx = c.getContext("2d");
|
||||
let gradient: any = `#34B67C22`;
|
||||
if (ctx) {
|
||||
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||
gradient.addColorStop(0, `#34B67C99`);
|
||||
gradient.addColorStop(0.35, `#34B67C66`);
|
||||
gradient.addColorStop(1, `#34B67C22`);
|
||||
} else {
|
||||
console.warn('Cannot get context for gradient');
|
||||
}
|
||||
|
||||
chartData.value.datasets[0].backgroundColor = [gradient];
|
||||
}
|
||||
|
||||
|
||||
|
||||
const chartOptions = ref<ChartOptions<'line'>>({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
includeInvisible: true
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { display: true },
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: '#CCCCCC22',
|
||||
// borderDash: [5, 10]
|
||||
},
|
||||
},
|
||||
x: {
|
||||
ticks: { display: true },
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: '#CCCCCC22',
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleFont: { size: 16, weight: 'bold' },
|
||||
bodyFont: { size: 14 },
|
||||
padding: 10,
|
||||
cornerRadius: 4,
|
||||
boxPadding: 10,
|
||||
caretPadding: 20,
|
||||
yAlign: 'bottom',
|
||||
xAlign: 'center',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const chartData = ref<ChartData<'line'>>({
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: ['#34B67C' + '77'],
|
||||
borderColor: '#34B67C',
|
||||
borderWidth: 4,
|
||||
fill: true,
|
||||
tension: 0.45,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
hoverBackgroundColor: '#34B67C',
|
||||
hoverBorderColor: 'white',
|
||||
hoverBorderWidth: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
supabaseFetching.value = true;
|
||||
supabaseError.value = undefined;
|
||||
|
||||
const integrationData = await $fetch<TSupabaseIntegration>('/api/integrations/supabase/get', {
|
||||
...signHeaders({
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'x-integration': props.integration_id
|
||||
})
|
||||
});
|
||||
|
||||
if (!integrationData) {
|
||||
supabaseError.value = 'Cannot get integration data';
|
||||
supabaseFetching.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await getRemoteData(
|
||||
integrationData.table_name,
|
||||
integrationData.xField,
|
||||
integrationData.yMode,
|
||||
integrationData.from.toString(),
|
||||
integrationData.to.toString(),
|
||||
integrationData.slice,
|
||||
);
|
||||
if (data.error) {
|
||||
supabaseError.value = data.error;
|
||||
supabaseFetching.value = false;
|
||||
return;
|
||||
}
|
||||
supabaseFetching.value = false;
|
||||
supabaseData.value = data.result;
|
||||
|
||||
chartData.value.labels = data.result?.labels || [];
|
||||
chartData.value.datasets[0].data = data.result?.data || [];
|
||||
|
||||
console.log(data.result);
|
||||
createGradient();
|
||||
} catch (ex: any) {
|
||||
if (!ex.response._data) {
|
||||
supabaseError.value = ex.message.toString();
|
||||
supabaseFetching.value = false;
|
||||
} else {
|
||||
supabaseError.value = ex.response._data.message.toString();
|
||||
supabaseFetching.value = false;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div v-if="!supabaseFetching">
|
||||
<div v-if="!supabaseError">
|
||||
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||
</div>
|
||||
<div v-if="supabaseError"> {{ supabaseError }} </div>
|
||||
</div>
|
||||
<div v-if="supabaseFetching">
|
||||
Getting remote data...
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@
|
||||
const ACCESS_TOKEN_STATE_KEY = 'access_token';
|
||||
const ACCESS_TOKEN_COOKIE_KEY = 'access_token';
|
||||
|
||||
|
||||
export function signHeaders(headers?: Record<string, string>) {
|
||||
const { token } = useAccessToken()
|
||||
return { headers: { ...(headers || {}), 'Authorization': 'Bearer ' + token.value } }
|
||||
|
||||
125
dashboard/composables/useSupabase.ts
Normal file
125
dashboard/composables/useSupabase.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
|
||||
|
||||
import type { TSupabaseIntegration } from "@schema/integrations/SupabaseIntegrationSchema";
|
||||
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
|
||||
const computedHeaders = computed<Record<string, string>>(() => {
|
||||
const signedHeaders = signHeaders();
|
||||
return {
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'Authorization': signedHeaders.headers.Authorization
|
||||
}
|
||||
})
|
||||
|
||||
const integrationsCredentials = useFetch(`/api/integrations/credentials/get`, {
|
||||
headers: computedHeaders,
|
||||
onResponse: (e) => {
|
||||
supabaseUrl.value = e.response._data.supabase.url || '';
|
||||
supabaseAnonKey.value = e.response._data.supabase.anon_key || '';
|
||||
supabaseServiceRoleKey.value = e.response._data.supabase.service_role_key || '';
|
||||
}
|
||||
});
|
||||
|
||||
const supabaseUrl = ref<string>('');
|
||||
const supabaseAnonKey = ref<string>('');
|
||||
const supabaseServiceRoleKey = ref<string>('');
|
||||
|
||||
const supabaseIntegrations = useFetch<TSupabaseIntegration[]>('/api/integrations/supabase/list', { headers: computedHeaders })
|
||||
|
||||
|
||||
const subabaseClientData: { client: SupabaseClient | undefined } = {
|
||||
client: undefined
|
||||
}
|
||||
|
||||
async function updateIntegrationsCredentails(data: { supabase_url: string, supabase_anon_key: string, supabase_service_role_key: string }) {
|
||||
try {
|
||||
await $fetch(`/api/integrations/credentials/${activeProjectId.data.value}/update`, {
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
supabase_url: data.supabase_url,
|
||||
supabase_anon_key: data.supabase_anon_key,
|
||||
supabase_service_role_key: data.supabase_service_role_key
|
||||
}),
|
||||
});
|
||||
integrationsCredentials.refresh();
|
||||
return { ok: true, error: '' }
|
||||
} catch (ex: any) {
|
||||
return { ok: false, error: ex.message.toString() };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function createSupabaseUrl(supabaseUrl: string) {
|
||||
let result = supabaseUrl;
|
||||
if (!result.includes('https://')) result = `https://${result}`;
|
||||
if (!result.endsWith('.supabase.co')) result = `${result}.supabase.co`;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
async function testConnection() {
|
||||
const url = createSupabaseUrl(supabaseUrl.value);
|
||||
subabaseClientData.client = createClient(url, supabaseAnonKey.value);
|
||||
const res = await subabaseClientData.client.from('_t_e_s_t_').select('*').limit(1);
|
||||
if (res.error?.message.startsWith('TypeError')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
type GroupBy = 'day' | 'month';
|
||||
|
||||
const groupByDate = (data: string[], groupBy: GroupBy) => {
|
||||
return data.reduce((acc, item) => {
|
||||
const date = new Date(item);
|
||||
const dateKey = groupBy === 'day'
|
||||
? format(date, 'yyyy-MM-dd') // Group by day
|
||||
: format(date, 'yyyy-MM'); // Group by month
|
||||
|
||||
if (!acc[dateKey]) { acc[dateKey] = []; }
|
||||
|
||||
acc[dateKey].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
}
|
||||
|
||||
async function getRemoteData(table: string, xField: string, yMode: string, from: string, to: string, slice: string) {
|
||||
const url = createSupabaseUrl(supabaseUrl.value);
|
||||
subabaseClientData.client = createClient(url, supabaseAnonKey.value);
|
||||
const res = await subabaseClientData.client.from(table).select(xField)
|
||||
.filter(xField, 'gte', from)
|
||||
.filter(xField, 'lte', to);
|
||||
|
||||
if (res.error) return { error: res.error.message };
|
||||
|
||||
const grouped = groupByDate(res.data.map((e: any) => e.created_at) || [], slice as any);
|
||||
|
||||
const result: { labels: string[], data: number[] } = { labels: [], data: [] }
|
||||
|
||||
for (const key in grouped) {
|
||||
result.labels.push(key);
|
||||
result.data.push(grouped[key].length);
|
||||
}
|
||||
|
||||
|
||||
return { result };
|
||||
}
|
||||
|
||||
export function useSupabase() {
|
||||
|
||||
return {
|
||||
getRemoteData,
|
||||
testConnection,
|
||||
supabaseIntegrations, integrationsCredentials,
|
||||
supabaseUrl, supabaseAnonKey,
|
||||
supabaseServiceRoleKey,
|
||||
updateIntegrationsCredentails
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const sections: Section[] = [
|
||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
||||
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||
{ label: 'Integrations (soon)', to: '#', icon: 'fal fa-cube', disabled: true },
|
||||
{ label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||
{
|
||||
grow: true,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@getbrevo/brevo": "^2.2.0",
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"@supabase/supabase-js": "^2.45.4",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-chart-funnel": "^4.2.1",
|
||||
"chartjs-plugin-annotation": "^2.2.1",
|
||||
|
||||
105
dashboard/pages/integrations.vue
Normal file
105
dashboard/pages/integrations.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import SupabaseChartDialog from '~/components/integrations/SupabaseChartDialog.vue';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
const {
|
||||
supabaseUrl, supabaseAnonKey, supabaseServiceRoleKey, integrationsCredentials,
|
||||
supabaseIntegrations, updateIntegrationsCredentails
|
||||
} = useSupabase()
|
||||
|
||||
async function updateCredentials() {
|
||||
|
||||
const res = await updateIntegrationsCredentails({
|
||||
supabase_url: supabaseUrl.value,
|
||||
supabase_anon_key: supabaseAnonKey.value,
|
||||
supabase_service_role_key: supabaseServiceRoleKey.value
|
||||
});
|
||||
|
||||
if (res.ok === true) {
|
||||
integrationsCredentials.refresh();
|
||||
createAlert('Credentials updated', 'Credentials updated successfully', 'far fa-error', 4000);
|
||||
} else {
|
||||
createAlert('Error updating credentials', res.error, 'far fa-error', 4000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const { openDialogEx } = useCustomDialog()
|
||||
|
||||
function showChartDialog() {
|
||||
openDialogEx(SupabaseChartDialog, {
|
||||
closable: true,
|
||||
width: '55vw',
|
||||
height: '65vh'
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="home w-full h-full px-10 pt-6 overflow-y-auto">
|
||||
|
||||
<CardTitled title="Supabase integration" class="w-full">
|
||||
<template #header>
|
||||
<img class="h-10 w-10" :src="'supabase.svg'" alt="Supabase logo">
|
||||
</template>
|
||||
<div class="flex gap-6 flex-col w-full">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-lyx-text"> Supabase url </div>
|
||||
<div class="text-lyx-text-dark"> Required to fetch data from supabase </div>
|
||||
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
|
||||
v-model="supabaseUrl" type="text"></LyxUiInput>
|
||||
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-lyx-text"> Supabase anon key </div>
|
||||
<div class="text-lyx-text-dark"> Required to fetch data from supabase </div>
|
||||
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
|
||||
v-model="supabaseAnonKey" type="password"></LyxUiInput>
|
||||
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-lyx-text"> Supabase service role key </div>
|
||||
<div class="text-lyx-text-dark"> Only used if you need to bypass RLS </div>
|
||||
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
|
||||
v-model="supabaseServiceRoleKey" type="password"></LyxUiInput>
|
||||
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<LyxUiButton v-if="!integrationsCredentials.pending.value" @click="updateCredentials()"
|
||||
type="primary"> Save
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
|
||||
<LyxUiCard class="mt-6 w-full">
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex gap-2 items-center" v-for="supabaseIntegration of supabaseIntegrations.data.value">
|
||||
<div> {{ supabaseIntegration.name }} </div>
|
||||
<div> <i class="far fa-edit"></i> </div>
|
||||
<div> <i class="far fa-trash"></i> </div>
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="primary" @click="showChartDialog()"> Add supabase chart </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
|
||||
|
||||
<div class="mt-10">
|
||||
<IntegrationsSupabaseLineChart integration_id="66f6c558d97e4abd408feee0"></IntegrationsSupabaseLineChart>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
86
dashboard/pnpm-lock.yaml
generated
86
dashboard/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@nuxtjs/tailwindcss':
|
||||
specifier: ^6.12.0
|
||||
version: 6.12.0(rollup@4.18.0)
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.45.4
|
||||
version: 2.45.4
|
||||
chart.js:
|
||||
specifier: ^3.9.1
|
||||
version: 3.9.1
|
||||
@@ -1138,6 +1141,28 @@ packages:
|
||||
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@supabase/auth-js@2.65.0':
|
||||
resolution: {integrity: sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==}
|
||||
|
||||
'@supabase/functions-js@2.4.1':
|
||||
resolution: {integrity: sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==}
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
||||
'@supabase/postgrest-js@1.16.1':
|
||||
resolution: {integrity: sha512-EOSEZFm5pPuCPGCmLF1VOCS78DfkSz600PBuvBND/IZmMciJ1pmsS3ss6TkB6UkuvTybYiBh7gKOYyxoEO3USA==}
|
||||
|
||||
'@supabase/realtime-js@2.10.2':
|
||||
resolution: {integrity: sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==}
|
||||
|
||||
'@supabase/storage-js@2.7.0':
|
||||
resolution: {integrity: sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==}
|
||||
|
||||
'@supabase/supabase-js@2.45.4':
|
||||
resolution: {integrity: sha512-E5p8/zOLaQ3a462MZnmnz03CrduA5ySH9hZyL03Y+QZLIOO4/Gs8Rdy4ZCKDHsN7x0xdanVEWWFN3pJFQr9/hg==}
|
||||
|
||||
'@swc/helpers@0.3.17':
|
||||
resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==}
|
||||
|
||||
@@ -1223,6 +1248,9 @@ packages:
|
||||
'@types/pdfkit@0.13.4':
|
||||
resolution: {integrity: sha512-ixGNDHYJCCKuamY305wbfYSphZ2WPe8FPkjn8oF4fHV+PgPV4V+hecPh2VOS2h4RNtpSB3zQcR4sCpNvvrEb1A==}
|
||||
|
||||
'@types/phoenix@1.6.5':
|
||||
resolution: {integrity: sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==}
|
||||
|
||||
'@types/qs@6.9.16':
|
||||
resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==}
|
||||
|
||||
@@ -1244,6 +1272,9 @@ packages:
|
||||
'@types/whatwg-url@11.0.5':
|
||||
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
|
||||
|
||||
'@types/ws@8.5.12':
|
||||
resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
|
||||
|
||||
'@ungap/structured-clone@1.2.0':
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
|
||||
@@ -1491,9 +1522,6 @@ packages:
|
||||
'@vue/shared@3.4.27':
|
||||
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
||||
|
||||
'@vue/shared@3.5.7':
|
||||
resolution: {integrity: sha512-NBE1PBIvzIedxIc2RZiKXvGbJkrZ2/hLf3h8GlS4/sP9xcXEZMFWOazFkNd6aGeUCMaproe5MHVYB3/4AW9q9g==}
|
||||
|
||||
'@vue/shared@3.5.8':
|
||||
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
|
||||
|
||||
@@ -6632,6 +6660,48 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@2.3.0': {}
|
||||
|
||||
'@supabase/auth-js@2.65.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/functions-js@2.4.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
'@supabase/postgrest-js@1.16.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/realtime-js@2.10.2':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@types/phoenix': 1.6.5
|
||||
'@types/ws': 8.5.12
|
||||
ws: 8.17.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@supabase/storage-js@2.7.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/supabase-js@2.45.4':
|
||||
dependencies:
|
||||
'@supabase/auth-js': 2.65.0
|
||||
'@supabase/functions-js': 2.4.1
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@supabase/postgrest-js': 1.16.1
|
||||
'@supabase/realtime-js': 2.10.2
|
||||
'@supabase/storage-js': 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@swc/helpers@0.3.17':
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
@@ -6719,6 +6789,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.12.12
|
||||
|
||||
'@types/phoenix@1.6.5': {}
|
||||
|
||||
'@types/qs@6.9.16': {}
|
||||
|
||||
'@types/resize-observer-browser@0.1.11': {}
|
||||
@@ -6735,6 +6807,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/webidl-conversions': 7.0.3
|
||||
|
||||
'@types/ws@8.5.12':
|
||||
dependencies:
|
||||
'@types/node': 20.12.12
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
|
||||
'@unhead/dom@1.9.11':
|
||||
@@ -7152,7 +7228,7 @@ snapshots:
|
||||
'@volar/language-core': 1.11.1
|
||||
'@volar/source-map': 1.11.1
|
||||
'@vue/compiler-dom': 3.4.27
|
||||
'@vue/shared': 3.5.7
|
||||
'@vue/shared': 3.5.8
|
||||
computeds: 0.0.1
|
||||
minimatch: 9.0.4
|
||||
muggle-string: 0.3.1
|
||||
@@ -7200,8 +7276,6 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.4.27': {}
|
||||
|
||||
'@vue/shared@3.5.7': {}
|
||||
|
||||
'@vue/shared@3.5.8': {}
|
||||
|
||||
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
||||
|
||||
15
dashboard/public/supabase.svg
Normal file
15
dashboard/public/supabase.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="98" height="100" viewBox="0 0 98 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M56.8944 98.338C54.3397 101.555 49.1599 99.7924 49.0983 95.6846L48.1982 35.6025H88.5973C95.9147 35.6025 99.9957 44.0542 95.4457 49.7849L56.8944 98.338Z" fill="url(#paint0_linear_99_24683)"/>
|
||||
<path d="M56.8944 98.338C54.3397 101.555 49.1599 99.7924 49.0983 95.6846L48.1982 35.6025H88.5973C95.9147 35.6025 99.9957 44.0542 95.4457 49.7849L56.8944 98.338Z" fill="url(#paint1_linear_99_24683)" fill-opacity="0.2"/>
|
||||
<path d="M40.464 1.66109C43.0187 -1.55638 48.1986 0.206562 48.2601 4.31445L48.6546 64.3964H8.76106C1.44348 64.3964 -2.63767 55.9448 1.91262 50.214L40.464 1.66109Z" fill="#3ECF8E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_99_24683" x1="48.1982" y1="48.9242" x2="84.1036" y2="63.9829" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#249361"/>
|
||||
<stop offset="1" stop-color="#3ECF8E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_99_24683" x1="32.2797" y1="27.1289" x2="48.6544" y2="57.9534" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
23
dashboard/server/api/integrations/credentials/get.ts
Normal file
23
dashboard/server/api/integrations/credentials/get.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { IntegrationsCredentialsModel } from '@schema/integrations/IntegrationsCredentialsSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const credentials = await IntegrationsCredentialsModel.findOne({ project_id });
|
||||
|
||||
return {
|
||||
supabase: {
|
||||
anon_key: credentials?.supabase_anon_key || '',
|
||||
service_role_key: credentials?.supabase_service_role_key || '',
|
||||
url: credentials?.supabase_url || ''
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
23
dashboard/server/api/integrations/credentials/update.post.ts
Normal file
23
dashboard/server/api/integrations/credentials/update.post.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IntegrationsCredentialsModel } from "@schema/integrations/IntegrationsCredentialsSchema";
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
const res = await IntegrationsCredentialsModel.updateOne({ project_id }, {
|
||||
supabase_anon_key: body.supabase_anon_key || '',
|
||||
supabase_service_role_key: body.supabase_service_role_key || '',
|
||||
supabase_url: body.supabase_url || '',
|
||||
}, { upsert: true });
|
||||
|
||||
return { ok: res.acknowledged };
|
||||
|
||||
});
|
||||
29
dashboard/server/api/integrations/supabase/add.post.ts
Normal file
29
dashboard/server/api/integrations/supabase/add.post.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { SupabaseIntegrationModel } from "@schema/integrations/SupabaseIntegrationSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const { chart_type, table_name, xField, yMode, from, to, slice, name } = await readBody(event);
|
||||
|
||||
if (!project.premium) {
|
||||
const supabaseIntegrationsCount = await SupabaseIntegrationModel.countDocuments({ project_id });
|
||||
if (supabaseIntegrationsCount > 0) return setResponseStatus(event, 400, 'LIMIT_REACHED');
|
||||
}
|
||||
|
||||
await SupabaseIntegrationModel.create({
|
||||
name,
|
||||
project_id, chart_type,
|
||||
table_name, xField, yMode,
|
||||
from, to, slice,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
|
||||
});
|
||||
18
dashboard/server/api/integrations/supabase/get.ts
Normal file
18
dashboard/server/api/integrations/supabase/get.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { SupabaseIntegrationModel } from '@schema/integrations/SupabaseIntegrationSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const integration_id = getHeader(event, 'x-integration');
|
||||
|
||||
const integration = await SupabaseIntegrationModel.findOne({ _id: integration_id });
|
||||
return integration;
|
||||
|
||||
});
|
||||
16
dashboard/server/api/integrations/supabase/list.ts
Normal file
16
dashboard/server/api/integrations/supabase/list.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { SupabaseIntegrationModel } from '@schema/integrations/SupabaseIntegrationSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const integrations = await SupabaseIntegrationModel.find({ project_id });
|
||||
return integrations;
|
||||
|
||||
});
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
|
||||
|
||||
broker:
|
||||
image: litlyx-broker
|
||||
image: litlyx-consumer
|
||||
restart: always
|
||||
ports:
|
||||
- "3999:3999"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const props = defineProps<{ placeholder?: string, modelValue: string }>();
|
||||
const props = defineProps<{ placeholder?: string, modelValue: string, type?: string }>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void
|
||||
@@ -18,8 +18,7 @@ const handleChange = (event: Event) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input class="bg-lyx-widget-light text-lyx-text-dark poppins rounded-md outline outline-[1px] outline-lyx-widget-lighter" type="text"
|
||||
:placeholder="props.placeholder"
|
||||
:value="props.modelValue"
|
||||
@input="handleChange">
|
||||
<input
|
||||
class="bg-lyx-widget-light text-lyx-text-dark poppins rounded-md outline outline-[1px] outline-lyx-widget-lighter"
|
||||
:type="props.type ?? 'text'" :placeholder="props.placeholder" :value="props.modelValue" @input="handleChange">
|
||||
</template>
|
||||
18
shared/schema/integrations/IntegrationsCredentialsSchema.ts
Normal file
18
shared/schema/integrations/IntegrationsCredentialsSchema.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { model, Schema, Types } from 'mongoose';
|
||||
|
||||
export type TIntegrationsCredentials = {
|
||||
_id: Schema.Types.ObjectId,
|
||||
project_id: Schema.Types.ObjectId,
|
||||
supabase_url: string,
|
||||
supabase_anon_key: string,
|
||||
supabase_service_role_key: string,
|
||||
}
|
||||
|
||||
const IntegrationsCredentialsSchema = new Schema<TIntegrationsCredentials>({
|
||||
project_id: { type: Types.ObjectId, index: 1 },
|
||||
supabase_url: { type: String },
|
||||
supabase_anon_key: { type: String },
|
||||
supabase_service_role_key: { type: String },
|
||||
});
|
||||
|
||||
export const IntegrationsCredentialsModel = model<TIntegrationsCredentials>('integrations_credentials', IntegrationsCredentialsSchema);
|
||||
28
shared/schema/integrations/SupabaseIntegrationSchema.ts
Normal file
28
shared/schema/integrations/SupabaseIntegrationSchema.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { model, Schema, Types } from 'mongoose';
|
||||
|
||||
export type TSupabaseIntegration = {
|
||||
_id: Schema.Types.ObjectId,
|
||||
project_id: Schema.Types.ObjectId,
|
||||
name: string,
|
||||
chart_type: string,
|
||||
table_name: string,
|
||||
xField: string,
|
||||
yMode: string,
|
||||
from: Date,
|
||||
to: Date,
|
||||
slice: string
|
||||
}
|
||||
|
||||
const SupabaseIntegrationSchema = new Schema<TSupabaseIntegration>({
|
||||
project_id: { type: Types.ObjectId, index: 1 },
|
||||
name: { type: String, required: true },
|
||||
chart_type: { type: String, required: true },
|
||||
table_name: { type: String, required: true },
|
||||
xField: { type: String, required: true },
|
||||
yMode: { type: String, required: true },
|
||||
from: { type: Date, required: true },
|
||||
to: { type: Date, required: true },
|
||||
slice: { type: String, required: true }
|
||||
});
|
||||
|
||||
export const SupabaseIntegrationModel = model<TSupabaseIntegration>('supabase_integrations', SupabaseIntegrationSchema);
|
||||
Reference in New Issue
Block a user