-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Filipe Forattini
committed
Oct 6, 2024
0 parents
commit 03ed3c5
Showing
13 changed files
with
3,533 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# config | ||
|
||
x-service: &service | ||
image: node:21-alpine | ||
restart: always | ||
user: root | ||
working_dir: /home/app | ||
entrypoint: yarn | ||
command: dev | ||
dns: | ||
- 8.8.8.8 | ||
expose: | ||
- 8000 | ||
environment: | ||
- PORT=8000 | ||
- APP_ENV=local | ||
- NODE_ENV=local | ||
- TZ=America/Sao_Paulo | ||
|
||
|
||
services: | ||
|
||
# apps | ||
|
||
api: | ||
<<: *service | ||
volumes: | ||
- .:/home/app | ||
ports: | ||
- "8000:8000" | ||
|
||
# dependencies | ||
|
||
minio: | ||
image: bitnami/minio:latest | ||
volumes: | ||
- minio_data:/bitnami/minio/data | ||
ports: | ||
- "9000:9000" | ||
- "9001:9001" | ||
environment: | ||
MINIO_FORCE_NEW_KEYS: yes | ||
MINIO_SCHEME: http | ||
MINIO_DEFAULT_BUCKETS: fshortner | ||
MINIO_ROOT_USER: fshortner | ||
MINIO_ROOT_PASSWORD: thisissecret | ||
|
||
volumes: | ||
minio_data: {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { config } from "dotenv"; | ||
config() | ||
|
||
import { App } from "./src/app.js"; | ||
App.create().then(() => App.start()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"name": "api", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"type": "module", | ||
"license": "MIT", | ||
"scripts": { | ||
"start": "node index.js", | ||
"dev": "nodemon --ignore tmp/ index.js" | ||
}, | ||
"dependencies": { | ||
"@whiskeysockets/baileys": "^6.7.8", | ||
"bullmq": "^5.15.0", | ||
"compression": "^1.7.4", | ||
"cors": "^2.8.5", | ||
"dotenv": "^16.4.5", | ||
"express": "^4.21.0", | ||
"express-rate-limit": "^7.4.1", | ||
"express-status-monitor": "^1.3.4", | ||
"express-validator": "^7.2.0", | ||
"helmet": "^8.0.0", | ||
"hpp": "^0.2.3", | ||
"http-errors": "^2.0.0", | ||
"ioredis": "^5.4.1", | ||
"lowdb": "^7.0.1", | ||
"morgan": "^1.10.0", | ||
"node-cron": "^3.0.3", | ||
"qrcode": "^1.5.4", | ||
"s3db.js": "^3.0.2" | ||
}, | ||
"devDependencies": { | ||
"nodemon": "^3.1.7" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { addRoutes } from "./routes.js" | ||
import { startCrons } from "./crons.js" | ||
import { createDB, createServer } from "./resources/index.js" | ||
|
||
export const App = { | ||
resources: {}, | ||
env: process.env, | ||
|
||
async create() { | ||
this.resources.db = await createDB(this) | ||
this.resources.server = await createServer(this) | ||
}, | ||
|
||
async start() { | ||
addRoutes(this) | ||
startCrons(this) | ||
|
||
this.resources.server.listen(this.env.PORT, () => console.info(`server is running on port ${this.env.PORT}`)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"Version": "2012-10-17", | ||
"Statement": [ | ||
{ | ||
"Effect": "Allow", | ||
"Action": [ | ||
"s3:*" | ||
], | ||
"Resource": [ | ||
"arn:aws:s3:::*", | ||
"arn:aws:s3:::*/*" | ||
] | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import cron from 'node-cron' | ||
|
||
export function startCrons(App) { | ||
const { db } = App.resources | ||
|
||
cron.schedule('*/10 * * * * *', async () => { | ||
const items = await db.resource('report-items').page(1) | ||
|
||
if (!items.length) return | ||
else console.log(' clicks :: calculating...') | ||
|
||
const report = items.reduce((acc, item) => { | ||
if (!acc[item.urlId]) acc[item.urlId] = 0 | ||
acc[item.urlId]++ | ||
return acc | ||
}, {}) | ||
|
||
for (const [urlId, clicks] of Object.entries(report)) { | ||
const url = await db.resource('urls').get(urlId) | ||
await db.resource('urls').update(urlId, { clicks: url.clicks + clicks }) | ||
} | ||
|
||
await db.resource('report-items').deleteMany(items.map(item => item.id)) | ||
console.log(' clicks :: added:', items.length); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { S3db } from 's3db.js' | ||
|
||
export async function createDB (App) { | ||
const db = new S3db({ | ||
connectionString: App.env.BUCKET_CONNECTION_STRING, | ||
}) | ||
|
||
await db.connect() | ||
|
||
await db.createResource({ | ||
name: 'urls', | ||
attributes: { | ||
link: 'string', | ||
ip: 'string', | ||
clicks: 'number|optional|min:0', | ||
}, | ||
}) | ||
|
||
const clicksAttributes = { | ||
urlId: 'string', | ||
ip: 'string', | ||
utm: { | ||
$$type: 'object', | ||
source: 'string|optional', | ||
medium: 'string|optional', | ||
campaign: 'string|optional', | ||
content: 'string|optional', | ||
term: 'string|optional', | ||
} | ||
} | ||
|
||
await db.createResource({ | ||
name: 'clicks', | ||
attributes: clicksAttributes, | ||
}) | ||
|
||
await db.createResource({ | ||
name: 'report-items', | ||
attributes: clicksAttributes, | ||
}) | ||
|
||
return db | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './db.js' | ||
export * from './server.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import hpp from 'hpp' | ||
import cors from 'cors' | ||
import helmet from 'helmet' | ||
import morgan from 'morgan' | ||
import Express from 'express'; | ||
import compression from 'compression' | ||
import rateLimit from 'express-rate-limit' | ||
import statusMonitor from 'express-status-monitor' | ||
|
||
export async function createServer (App) { | ||
const server = Express(); | ||
|
||
server.use(statusMonitor()); | ||
server.use(Express.json()); | ||
server.use(Express.urlencoded({ extended: true })); | ||
server.use(cors()); | ||
server.use(hpp()); | ||
server.use(helmet()); | ||
server.use(compression()); | ||
server.use(morgan('dev')); | ||
server.use(rateLimit({ | ||
max: 100, | ||
windowMs: 15 * 60 * 1000, | ||
})); | ||
|
||
return server | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import createError from 'http-errors'; | ||
import { body, validationResult } from 'express-validator' | ||
|
||
export function addRoutes(App) { | ||
const { server, db } = App.resources | ||
|
||
// create | ||
server.post('/v1/urls', [ | ||
body('link').isURL(), | ||
], async (req, res, next) => { | ||
const errors = validationResult(req); | ||
|
||
if (!errors.isEmpty()) { | ||
const err = createError(400, 'invalid data') | ||
err.details = errors.array() | ||
return next(err); | ||
} | ||
|
||
const url = await db.resource('urls').insert({ | ||
...req.body, | ||
ip: req.ip, | ||
createdAt: Date.now(), | ||
}) | ||
|
||
delete url.ip | ||
|
||
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; | ||
url.shareble = new URL(fullUrl) | ||
url.shareble.pathname = `/${url.id}` | ||
url.shareble = url.shareble.toString() | ||
|
||
return res.json(url); | ||
}) | ||
|
||
// show | ||
server.get('/v1/urls/:id', async (req, res, next) => { | ||
try { | ||
const url = await db.resource('urls').get(req.params.id) | ||
delete url.ip | ||
|
||
return res.json(url) | ||
} catch (error) { | ||
const err = createError(404, 'url not found') | ||
err.details = error | ||
return next(err); | ||
} | ||
}) | ||
|
||
// redirect | ||
server.get('/:id', async (req, res, next) => { | ||
try { | ||
const url = await db.resource('urls').get(req.params.id) | ||
|
||
const click = { | ||
urlId: url.id, | ||
ip: req.ip, | ||
utm: { | ||
source: req.query.utm_source, | ||
medium: req.query.utm_medium, | ||
campaign: req.query.utm_campaign, | ||
content: req.query.utm_content, | ||
term: req.query.utm_term, | ||
}, | ||
} | ||
|
||
await Promise.all([ | ||
db.resource('clicks').insert(click), | ||
db.resource('report-items').insert(click), | ||
]) | ||
|
||
return res.redirect(302, url.link) | ||
} catch (error) { | ||
const err = createError(404, 'url not found') | ||
err.details = error | ||
return next(err); | ||
} | ||
}) | ||
|
||
server.use((req, res, next) => { | ||
next(createError(404, 'route not found')); | ||
}); | ||
|
||
server.use((err, req, res, next) => { | ||
res.status(err.status || 500); | ||
|
||
const errorResponse = { | ||
status: err.status || 500, | ||
message: err.message, | ||
}; | ||
|
||
if (err.details) { | ||
errorResponse.details = err.details; | ||
} | ||
|
||
res.json(errorResponse); | ||
}); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export function deepMerge(target, source) { | ||
Object.keys(source).forEach(key => { | ||
const sourceValue = source[key]; | ||
const targetValue = target[key]; | ||
|
||
if (sourceValue && typeof sourceValue === 'object') { | ||
if (!targetValue || typeof targetValue !== 'object') { | ||
target[key] = Array.isArray(sourceValue) ? [] : {}; | ||
} | ||
deepMerge(target[key], sourceValue); | ||
} else { | ||
target[key] = sourceValue; | ||
} | ||
}); | ||
return target; | ||
}; |
Oops, something went wrong.