create-rebe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/bin/create-rebe.js +310 -0
- package/package.json +40 -0
- package/template/.env.example +141 -0
- package/template/.gitattributes +16 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +10 -0
- package/template/.prettierrc +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +150 -0
- package/template/SECURITY.md +133 -0
- package/template/app/hooks/register.hook.js +22 -0
- package/template/app/http/controllers/auth.controller.js +34 -0
- package/template/app/http/middlewares/auth.middleware.js +25 -0
- package/template/app/http/middlewares/register.middleware.js +18 -0
- package/template/app/http/validators/auth.validator.js +8 -0
- package/template/app/jobs/register.job.js +17 -0
- package/template/app/queue/register.queue.js +11 -0
- package/template/app/routes/api.route.js +18 -0
- package/template/app/routes/register.route.js +8 -0
- package/template/app/routes/web.route.js +7 -0
- package/template/app/socket/register.socket.js +15 -0
- package/template/config/app.config.js +12 -0
- package/template/config/bcrypt.config.js +7 -0
- package/template/config/cache.config.js +7 -0
- package/template/config/cors.config.js +37 -0
- package/template/config/cron.config.js +9 -0
- package/template/config/database.config.js +51 -0
- package/template/config/express.config.js +37 -0
- package/template/config/index.js +19 -0
- package/template/config/jwt.config.js +10 -0
- package/template/config/logger.config.js +10 -0
- package/template/config/mail.config.js +14 -0
- package/template/config/queue.config.js +11 -0
- package/template/config/redis.config.js +32 -0
- package/template/config/runtime.config.js +11 -0
- package/template/config/socket.config.js +15 -0
- package/template/config/storage.config.js +10 -0
- package/template/core/bootstrap.core.js +92 -0
- package/template/core/common/array.js +144 -0
- package/template/core/common/cache.js +100 -0
- package/template/core/common/collection.js +173 -0
- package/template/core/common/crypt.js +69 -0
- package/template/core/common/date.js +254 -0
- package/template/core/common/hash.js +61 -0
- package/template/core/common/object.js +155 -0
- package/template/core/common/path.js +80 -0
- package/template/core/common/storage.js +97 -0
- package/template/core/common/string.js +137 -0
- package/template/core/common/url.js +81 -0
- package/template/core/common.core.js +93 -0
- package/template/core/cron.core.js +141 -0
- package/template/core/database.core.js +113 -0
- package/template/core/error.core.js +83 -0
- package/template/core/express.core.js +161 -0
- package/template/core/hooks.core.js +47 -0
- package/template/core/jwt.core.js +81 -0
- package/template/core/logger.core.js +100 -0
- package/template/core/mailer.core.js +65 -0
- package/template/core/queue.core.js +226 -0
- package/template/core/redis.core.js +75 -0
- package/template/core/register.core.js +91 -0
- package/template/core/runtime.core.js +27 -0
- package/template/core/socket.core.js +93 -0
- package/template/core/validator.core.js +34 -0
- package/template/database/.gitkeep +0 -0
- package/template/database/models/post.model.js +26 -0
- package/template/database/models/user.model.js +30 -0
- package/template/docs/Bootstrap.md +50 -0
- package/template/docs/Common.md +48 -0
- package/template/docs/Cron.md +47 -0
- package/template/docs/Database.md +61 -0
- package/template/docs/Express.md +63 -0
- package/template/docs/Hooks.md +48 -0
- package/template/docs/Jwt.md +63 -0
- package/template/docs/Logger.md +60 -0
- package/template/docs/Mailer.md +50 -0
- package/template/docs/Queue.md +57 -0
- package/template/docs/README.md +32 -0
- package/template/docs/Redis.md +54 -0
- package/template/docs/Register.md +52 -0
- package/template/docs/Socket.md +55 -0
- package/template/docs/Validator.md +45 -0
- package/template/ecosystem.config.js +54 -0
- package/template/index.js +6 -0
- package/template/jest.config.js +27 -0
- package/template/jsconfig.json +37 -0
- package/template/logs/.gitkeep +0 -0
- package/template/nodemon.json +23 -0
- package/template/package-lock.json +7033 -0
- package/template/package.json +82 -0
- package/template/scripts/cli.js +258 -0
- package/template/storage/.gitkeep +0 -0
- package/template/tests/common.test.js +45 -0
- package/template/tests/crypt-hash-storage.test.js +44 -0
- package/template/tests/jwt.test.js +45 -0
- package/template/tests/register.test.js +55 -0
- package/template/tests/setup.js +11 -0
- package/template/tests/validator.test.js +65 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# rebe
|
|
2
|
+
|
|
3
|
+
Ready-to-use Node.js backend framework. One process serves a JSON API, Socket.IO,
|
|
4
|
+
cron jobs, and an in-process queue on a clean four-layer architecture
|
|
5
|
+
(config → core → app) with centralized configuration.
|
|
6
|
+
|
|
7
|
+
## Create a project
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm create rebe@latest my-api
|
|
11
|
+
cd my-api
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Flags (all optional): `--name <name>`, `--pm <npm|pnpm|yarn|bun>`, `--install`,
|
|
17
|
+
`--git`, `--no-env`, `--force`, `-y`. Unless `--no-env` is set, a `.env` is generated
|
|
18
|
+
with `APP_NAME` set to the project name and fresh `APP_KEY` / JWT secrets.
|
|
19
|
+
|
|
20
|
+
## What's inside
|
|
21
|
+
|
|
22
|
+
- **HTTP** — Express 5 + Laravel-style routing, helmet, CORS, compression, rate
|
|
23
|
+
limiting, body limits, multipart uploads.
|
|
24
|
+
- **Realtime** — Socket.IO on the same port, optional Redis adapter.
|
|
25
|
+
- **Background** — node-cron scheduler and an in-process job queue (optional DB
|
|
26
|
+
persistence).
|
|
27
|
+
- **Data** — Sequelize (MySQL/MariaDB/Postgres/MSSQL/SQLite), enabled on demand.
|
|
28
|
+
- **Auth** — JWT (access + refresh, HS256-pinned), bcrypt.
|
|
29
|
+
- **Ops** — Winston logging with daily rotation, lifecycle hooks, graceful shutdown,
|
|
30
|
+
PM2 config.
|
|
31
|
+
- **Optional Redis** — standalone; each feature opts in and falls back cleanly.
|
|
32
|
+
|
|
33
|
+
## Manual setup (from a clone)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npm run cli -- setup # copy .env, refresh deps, generate keys
|
|
38
|
+
npm run dev # nodemon, development
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`setup` copies `.env.example` to `.env` and generates `APP_KEY`, `JWT_SECRET`, and
|
|
42
|
+
`JWT_REFRESH_SECRET`. Generate keys individually with `npm run cli -- key:generate`
|
|
43
|
+
and `npm run cli -- key:jwt [--refresh]`.
|
|
44
|
+
|
|
45
|
+
## Development vs production
|
|
46
|
+
|
|
47
|
+
| | Development | Production |
|
|
48
|
+
| --------------- | -------------------------------- | ------------------------------------------- |
|
|
49
|
+
| Start | `npm run dev` (nodemon) | `npm start` (`node .`) or PM2 |
|
|
50
|
+
| `APP_ENV` | `development` | `production` |
|
|
51
|
+
| Logs | colored console + rotating files | console filtered, rotating files in `logs/` |
|
|
52
|
+
| Error responses | include stack | stack hidden |
|
|
53
|
+
|
|
54
|
+
Production with PM2 (single fork instance):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pm2 start ecosystem.config.js
|
|
58
|
+
pm2 logs <APP_NAME>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> Run a **single** instance (`exec_mode: 'fork', instances: 1`). State is kept
|
|
62
|
+
> in-process (Socket.IO rooms, the in-process queue, node-cron schedules). Cluster
|
|
63
|
+
> mode would duplicate cron ticks, split socket rooms, and break queue state. For
|
|
64
|
+
> horizontal scale, enable Redis (`REDIS_USE_SOCKET`/`REDIS_USE_QUEUE`) first.
|
|
65
|
+
|
|
66
|
+
## Architecture
|
|
67
|
+
|
|
68
|
+
Four layers with a strict, one-directional dependency arrow:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Entry (index.js -> bootstrap.core)
|
|
72
|
+
-> Application (app/: http, routes, jobs, queue, socket, hooks; database/ models)
|
|
73
|
+
-> Core (core/: infrastructure, static classes, no business logic)
|
|
74
|
+
-> Config (config/: reads .env via Common.getEnv*, exposes `config`)
|
|
75
|
+
-> .env
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The core layer never imports application code statically. Each core loads a single
|
|
79
|
+
application entry point at runtime (`register.*.js`) through the hardened register
|
|
80
|
+
loader — Inversion of Control keeps the arrow pointing one way.
|
|
81
|
+
|
|
82
|
+
## Project structure
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
.
|
|
86
|
+
├── config/ # Centralized config (one file per domain) -> @config
|
|
87
|
+
├── core/ # Core layer (static classes) -> @core
|
|
88
|
+
│ ├── common.core.js + common/ # Shared utils + env readers
|
|
89
|
+
│ ├── runtime · logger · error · register
|
|
90
|
+
│ ├── database · cron · queue
|
|
91
|
+
│ ├── jwt · mailer · hooks · redis
|
|
92
|
+
│ ├── express · socket · validator
|
|
93
|
+
│ └── bootstrap.core.js # Boot orchestrator
|
|
94
|
+
├── app/ # Application layer -> @app
|
|
95
|
+
│ ├── http/controllers|middlewares|validators
|
|
96
|
+
│ ├── routes/ # web.route, api.route, register.route
|
|
97
|
+
│ ├── jobs/register.job.js # (Cron) => {}
|
|
98
|
+
│ ├── queue/register.queue.js # (Queue) => {}
|
|
99
|
+
│ ├── socket/register.socket.js # (io, Socket) => {}
|
|
100
|
+
│ └── hooks/register.hook.js # { before, after, shutdown }
|
|
101
|
+
├── database/ # Sequelize models -> @database
|
|
102
|
+
├── storage/ # Local file storage / uploads -> @storage
|
|
103
|
+
├── tests/ # Jest test suite
|
|
104
|
+
├── create-rebe/ # The `npm create rebe` scaffolder package
|
|
105
|
+
├── docs/ # Per-core API reference
|
|
106
|
+
├── scripts/cli.js # Project CLI (npm run cli)
|
|
107
|
+
├── index.js # Entry: dotenv -> module-alias -> runtime -> Bootstrap.run()
|
|
108
|
+
└── ecosystem.config.js # PM2 (fork, single instance)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Boot flow
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
index.js -> dotenv -> module-alias -> runtime.core -> Bootstrap.run()
|
|
115
|
+
1. error handlers 2. Hooks.before 3. Redis.connect (if enabled)
|
|
116
|
+
4. Database.connect (if enabled) 5. Express.create -> { app, server }
|
|
117
|
+
6. Socket.attach 7. Cron + Queue 8. Express.listen 9. Hooks.after
|
|
118
|
+
10. SIGINT/SIGTERM -> graceful shutdown
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Response envelope
|
|
122
|
+
|
|
123
|
+
There is no `response.core`. Every API handler returns a manual envelope:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
res.json({ status: true, code: 200, message: 'OK', data: null, meta: null })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Validation failures add an `errors` array (422). Unhandled errors and 404s are
|
|
130
|
+
formatted by `error.core` (JSON or HTML).
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
- [docs/](./docs/README.md) — per-core API reference (one file per core).
|
|
135
|
+
- [SECURITY.md](./SECURITY.md) — security model, hardening checklist, audit findings.
|
|
136
|
+
- `.env.example` — every configuration variable with its default.
|
|
137
|
+
|
|
138
|
+
## Scripts
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm run dev # nodemon (development)
|
|
142
|
+
npm start # node . (production)
|
|
143
|
+
npm test # jest
|
|
144
|
+
npm run format # prettier --write .
|
|
145
|
+
npm run cli -- help
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# SECURITY
|
|
2
|
+
|
|
3
|
+
Security model, audit findings, and production hardening guidance for `rebe`.
|
|
4
|
+
|
|
5
|
+
This document records a code- and dependency-level audit of the framework. Findings
|
|
6
|
+
are rated Critical / High / Medium / Low / Info and mapped to CWE. Items marked
|
|
7
|
+
**Fixed** are already addressed in the codebase; **Guidance** items are safe defaults
|
|
8
|
+
that you should review against your threat model before deploying.
|
|
9
|
+
|
|
10
|
+
## Audit summary
|
|
11
|
+
|
|
12
|
+
| ID | Severity | CWE | Issue | Status |
|
|
13
|
+
| ---- | -------- | -------- | -------------------------------------------------------------------------------------------------- | ------------------- |
|
|
14
|
+
| F-01 | Medium | CWE-79 | Reflected XSS in error/404 HTML pages | Fixed |
|
|
15
|
+
| F-02 | Medium | CWE-942 | CORS wildcard origin combined with credentials | Fixed |
|
|
16
|
+
| F-03 | Medium | CWE-440 | `common/*` utils required a non-existent module (`@app/config`), crashing Crypt/Hash/Storage/Cache | Fixed |
|
|
17
|
+
| F-04 | Low | CWE-1395 | Transitive `uuid` bounds-check advisory via `sequelize` | Accepted / monitor |
|
|
18
|
+
| F-05 | Medium | CWE-434 | File-type enforcement relies on client MIME; active content (SVG/HTML) could be stored XSS | Guidance |
|
|
19
|
+
| F-06 | Info | CWE-345 | JWT algorithm confusion | Mitigated by design |
|
|
20
|
+
| F-07 | Low | CWE-400 | Large default JSON/body limit (10 MB) | Guidance |
|
|
21
|
+
| F-08 | Info | CWE-352 | No CSRF protection | N/A for bearer API |
|
|
22
|
+
| F-09 | Low | CWE-798 | Demo credentials in sample code; secrets in local `.env` | Guidance |
|
|
23
|
+
| F-10 | Info | CWE-770 | Rate limit / trust-proxy defaults | Guidance |
|
|
24
|
+
|
|
25
|
+
## Findings
|
|
26
|
+
|
|
27
|
+
### F-01 — Reflected XSS in error pages (Fixed)
|
|
28
|
+
|
|
29
|
+
`error.core` rendered `req.originalUrl` and error messages directly into HTML 404/500
|
|
30
|
+
responses. A crafted URL could inject markup. The CSP (`default-src 'self'`) blocks
|
|
31
|
+
inline script execution, but the values are now HTML-escaped (`Common.Str.escapeHtml`)
|
|
32
|
+
as defense in depth. JSON responses were never affected.
|
|
33
|
+
|
|
34
|
+
### F-02 — CORS wildcard + credentials (Fixed)
|
|
35
|
+
|
|
36
|
+
The previous config used `origin: ['*']` with `credentials: true`. Reflecting any
|
|
37
|
+
origin with credentials enabled is unsafe, and browsers reject the combination.
|
|
38
|
+
`cors.config.js` now reads `CORS_ORIGIN` / `SOCKET_CORS_ORIGIN` as an allowlist and
|
|
39
|
+
**forces credentials off** whenever the origin is `*`. Set explicit origins in
|
|
40
|
+
production to allow credentialed cross-origin requests.
|
|
41
|
+
|
|
42
|
+
### F-03 — Broken module reference in shared utils (Fixed)
|
|
43
|
+
|
|
44
|
+
`crypt.js`, `hash.js`, `storage.js`, and `cache.js` required `@app/config`, which does
|
|
45
|
+
not resolve (`@app` → the application folder; config lives at `@config`). Any use of
|
|
46
|
+
`Common.Crypt`, `Common.Hash`, `Common.Storage`, or the config-default path of
|
|
47
|
+
`Common.Cache` would throw `MODULE_NOT_FOUND`. All references now use `@config/index`.
|
|
48
|
+
This is primarily a reliability bug, but a crash in a crypto/hash helper invoked on a
|
|
49
|
+
request path is a denial-of-service vector.
|
|
50
|
+
|
|
51
|
+
### F-04 — Transitive uuid advisory (Accepted / monitor)
|
|
52
|
+
|
|
53
|
+
`npm audit` reports a moderate advisory (GHSA-w5hq-g745-h8pq) for `uuid` pulled in by
|
|
54
|
+
`sequelize`. The only automated fix downgrades `sequelize` to a 3.x release (a major
|
|
55
|
+
breaking change), so it is not applied. Real-world impact here is low: the affected
|
|
56
|
+
code path requires an attacker-controlled `buf` argument, which the framework never
|
|
57
|
+
passes, and the database is disabled by default. Re-run `npm audit` periodically and
|
|
58
|
+
bump `sequelize` once an upstream fix ships.
|
|
59
|
+
|
|
60
|
+
### F-05 — File upload type enforcement (Guidance)
|
|
61
|
+
|
|
62
|
+
`express.core.upload()` filters by `file.mimetype`, which the client controls, and by
|
|
63
|
+
`STORAGE_UPLOAD_ALLOWED_TYPES`. The default allowlist (`image/jpeg,image/png,
|
|
64
|
+
image/webp,application/pdf`) is safe, and `/uploads` is served with
|
|
65
|
+
`X-Content-Type-Options: nosniff`. If you broaden the allowlist:
|
|
66
|
+
|
|
67
|
+
- Do **not** allow `image/svg+xml` or `text/html` for files served back to browsers —
|
|
68
|
+
they can carry script (stored XSS). Serve such files with
|
|
69
|
+
`Content-Disposition: attachment`, from a separate origin, or convert/sanitize them.
|
|
70
|
+
- Consider validating magic bytes, not just the MIME header.
|
|
71
|
+
|
|
72
|
+
### F-06 — JWT algorithm (Mitigated by design)
|
|
73
|
+
|
|
74
|
+
`jwt.core` pins HS256 on both sign and verify, so forged `alg: none` or RS/HS-swap
|
|
75
|
+
tokens are rejected. Keep access-token TTLs short and store only non-sensitive claims
|
|
76
|
+
(the payload is signed, not encrypted).
|
|
77
|
+
|
|
78
|
+
### F-07 — Body size limit (Guidance)
|
|
79
|
+
|
|
80
|
+
`EXPRESS_BODY_LIMIT` defaults to `10mb`. For pure-JSON APIs, lower it (e.g. `256kb`)
|
|
81
|
+
to reduce the memory-exhaustion surface; raise it only for endpoints that genuinely
|
|
82
|
+
need large payloads.
|
|
83
|
+
|
|
84
|
+
### F-08 — CSRF (N/A for bearer auth)
|
|
85
|
+
|
|
86
|
+
The sample auth uses `Authorization: Bearer` tokens, which are not sent automatically
|
|
87
|
+
by browsers, so CSRF does not apply. If you switch to cookie-based sessions, add CSRF
|
|
88
|
+
protection (e.g. double-submit token) and set cookies `HttpOnly`, `Secure`,
|
|
89
|
+
`SameSite`.
|
|
90
|
+
|
|
91
|
+
### F-09 — Demo credentials and local secrets (Guidance)
|
|
92
|
+
|
|
93
|
+
`auth.controller.js` ships an in-memory demo user (`demo@example.com` /
|
|
94
|
+
`password123`) so the sample runs without a database — replace it with a real model
|
|
95
|
+
lookup before production. `.env` is git-ignored; never commit it, and rotate any
|
|
96
|
+
secret that was ever committed. The scaffolder (`create-rebe`) generates fresh
|
|
97
|
+
`APP_KEY` and JWT secrets per project.
|
|
98
|
+
|
|
99
|
+
### F-10 — Rate limit & trust proxy (Guidance)
|
|
100
|
+
|
|
101
|
+
A global rate limit is on by default (`100` / 15 min / IP). Behind a proxy or load
|
|
102
|
+
balancer, set `EXPRESS_TRUST_PROXY=1` (or the real hop count) so the limiter and logs
|
|
103
|
+
see the true client IP; leaving it at `0` while behind a proxy lets clients share one
|
|
104
|
+
bucket. Do **not** set an overly permissive trust-proxy value when directly exposed —
|
|
105
|
+
it would let clients spoof `X-Forwarded-For`.
|
|
106
|
+
|
|
107
|
+
## Positive controls
|
|
108
|
+
|
|
109
|
+
- `helmet` with a restrictive CSP; `x-powered-by` disabled.
|
|
110
|
+
- Only `./public` and `/uploads` are served statically — the full `storage/` root is
|
|
111
|
+
not exposed.
|
|
112
|
+
- `Common.Hash.equals` uses `crypto.timingSafeEqual`; `Crypt` uses AES-256-GCM
|
|
113
|
+
(authenticated) with a random IV and salt per message.
|
|
114
|
+
- `Common.Storage` resolves every path inside the storage root and rejects traversal.
|
|
115
|
+
- Graceful shutdown drains the queue and closes connections.
|
|
116
|
+
|
|
117
|
+
## Production hardening checklist
|
|
118
|
+
|
|
119
|
+
- [ ] `APP_ENV=production` (hides stack traces).
|
|
120
|
+
- [ ] Set explicit `CORS_ORIGIN` / `SOCKET_CORS_ORIGIN` (no `*`).
|
|
121
|
+
- [ ] Strong, unique `APP_KEY`, `JWT_SECRET`, `JWT_REFRESH_SECRET`; never committed.
|
|
122
|
+
- [ ] `EXPRESS_TRUST_PROXY` matches your proxy topology.
|
|
123
|
+
- [ ] Review `EXPRESS_BODY_LIMIT` and the rate-limit window/max.
|
|
124
|
+
- [ ] Replace demo auth with real user storage and password hashing.
|
|
125
|
+
- [ ] Restrict upload types; treat SVG/HTML as attachments.
|
|
126
|
+
- [ ] Run behind TLS; enable `MAIL_SECURE`/`REDIS_TLS` where applicable.
|
|
127
|
+
- [ ] `npm audit` clean (or risks accepted and tracked).
|
|
128
|
+
- [ ] Single PM2 fork instance, or Redis-backed for multi-instance.
|
|
129
|
+
|
|
130
|
+
## Reporting
|
|
131
|
+
|
|
132
|
+
Report vulnerabilities privately to the maintainer rather than via public issues.
|
|
133
|
+
Include a description, affected version, and reproduction steps.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Application lifecycle hooks, invoked by the bootstrap core:
|
|
4
|
+
// before — config is loaded; services have not started yet
|
|
5
|
+
// after — the HTTP server is listening
|
|
6
|
+
// shutdown — a termination signal was received (cleanup here)
|
|
7
|
+
//
|
|
8
|
+
// Each stage receives a context object ({ config }). Keep them fast and resilient;
|
|
9
|
+
// a thrown error in `before`/`after` aborts boot.
|
|
10
|
+
module.exports = {
|
|
11
|
+
async before(ctx) {
|
|
12
|
+
// e.g. warm caches, assert required external services are reachable
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async after(ctx) {
|
|
16
|
+
// e.g. emit a "ready" event, register health probes
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async shutdown(ctx) {
|
|
20
|
+
// e.g. flush buffers, close third-party clients
|
|
21
|
+
},
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const bcrypt = require('bcrypt')
|
|
4
|
+
|
|
5
|
+
const config = require('@config/index')
|
|
6
|
+
const Jwt = require('@core/jwt.core')
|
|
7
|
+
|
|
8
|
+
// In-memory demo user so the sample runs without a database. The password hash is
|
|
9
|
+
// computed once at load. Replace this with a real model lookup
|
|
10
|
+
// (e.g. Database.model('User').findOne(...)) in a production app.
|
|
11
|
+
const DEMO_USER = { id: 1, email: 'demo@example.com', name: 'Demo User' }
|
|
12
|
+
const DEMO_PASSWORD_HASH = bcrypt.hashSync('password123', config.bcrypt.saltRounds)
|
|
13
|
+
|
|
14
|
+
class AuthController {
|
|
15
|
+
// Input is already validated by Validator.make(loginSchema) on the route.
|
|
16
|
+
static async login({ req, res }) {
|
|
17
|
+
const { email, password } = req.body
|
|
18
|
+
|
|
19
|
+
const ok = email === DEMO_USER.email && (await bcrypt.compare(password, DEMO_PASSWORD_HASH))
|
|
20
|
+
if (!ok) {
|
|
21
|
+
return res.status(401).json({ status: false, code: 401, message: 'Invalid credentials', data: null, meta: null })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tokens = Jwt.issue({ sub: DEMO_USER.id, email: DEMO_USER.email })
|
|
25
|
+
return res.json({ status: true, code: 200, message: 'Login successful', data: { user: DEMO_USER, ...tokens }, meta: null })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Protected by the auth middleware, which sets req.user from the access token.
|
|
29
|
+
static async me({ req, res }) {
|
|
30
|
+
return res.json({ status: true, code: 200, message: 'OK', data: req.user, meta: null })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = AuthController
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Jwt = require('@core/jwt.core')
|
|
4
|
+
|
|
5
|
+
// Route middleware: require a valid Bearer access token and expose the decoded
|
|
6
|
+
// payload as req.user. Use per-route as a handle-based middleware:
|
|
7
|
+
// Routes.get('auth/me', AuthController.me, [Auth])
|
|
8
|
+
class Auth {
|
|
9
|
+
static handle({ req, res, next }) {
|
|
10
|
+
const token = Jwt.fromHeader(req.headers.authorization)
|
|
11
|
+
if (!token) {
|
|
12
|
+
return res.status(401).json({ status: false, code: 401, message: 'Missing bearer token', data: null, meta: null })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { valid, payload } = Jwt.tryVerify(token)
|
|
16
|
+
if (!valid) {
|
|
17
|
+
return res.status(401).json({ status: false, code: 401, message: 'Invalid or expired token', data: null, meta: null })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
req.user = payload
|
|
21
|
+
next()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = Auth
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const logger = require('@core/logger.core')
|
|
4
|
+
|
|
5
|
+
// Global application middleware. The Express core calls this with the app instance
|
|
6
|
+
// during create(), after the security/parsing stack and before routes. Register
|
|
7
|
+
// cross-cutting middleware here.
|
|
8
|
+
module.exports = (app) => {
|
|
9
|
+
// Lightweight access log: method, URL, status, and latency once the response
|
|
10
|
+
// has been sent.
|
|
11
|
+
app.use((req, res, next) => {
|
|
12
|
+
const start = Date.now()
|
|
13
|
+
res.on('finish', () => {
|
|
14
|
+
logger.http(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`)
|
|
15
|
+
})
|
|
16
|
+
next()
|
|
17
|
+
})
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const logger = require('@core/logger.core')
|
|
4
|
+
|
|
5
|
+
// Cron job definitions. The Cron core calls this function during boot and
|
|
6
|
+
// passes the Cron facade. Declare jobs with Cron.define(name, expression, fn).
|
|
7
|
+
//
|
|
8
|
+
// Expression format (node-cron): second? minute hour day-of-month month day-of-week
|
|
9
|
+
// '*/5 * * * *' -> every 5 minutes
|
|
10
|
+
// '0 0 * * *' -> every day at midnight
|
|
11
|
+
// '0 */6 * * *' -> every 6 hours
|
|
12
|
+
module.exports = (Cron) => {
|
|
13
|
+
// Heartbeat — handy while developing to confirm the scheduler is alive.
|
|
14
|
+
Cron.define('heartbeat', '*/30 * * * *', async () => {
|
|
15
|
+
logger.debug('Cron heartbeat tick')
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Queue worker definitions. The Queue core calls this function during boot and
|
|
4
|
+
// passes the Queue facade. Register a processor per queue name; dispatch jobs
|
|
5
|
+
// from anywhere with Queue.dispatch('emails', { ... }).
|
|
6
|
+
module.exports = (Queue) => {
|
|
7
|
+
// sample
|
|
8
|
+
// Queue.define('name', async (payload) => {
|
|
9
|
+
// // action here
|
|
10
|
+
// }, { concurrency: 2, maxRetries: 3 })
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Routes = require('@refkinscallv/express-routing')
|
|
4
|
+
|
|
5
|
+
const Validator = require('@core/validator.core')
|
|
6
|
+
const AuthController = require('@http/controllers/auth.controller')
|
|
7
|
+
const Auth = require('@http/middlewares/auth.middleware')
|
|
8
|
+
const { loginSchema } = require('@http/validators/auth.validator')
|
|
9
|
+
|
|
10
|
+
Routes.group('api', () => {
|
|
11
|
+
Routes.get('status', ({ res }) => {
|
|
12
|
+
res.json({ status: true, code: 200, message: 'OK', data: null, meta: null })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// Sample auth flow (runs without a database; see auth.controller.js).
|
|
16
|
+
Routes.post('auth/login', AuthController.login, [Validator.make(loginSchema)])
|
|
17
|
+
Routes.get('auth/me', AuthController.me, [Auth])
|
|
18
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const logger = require('@core/logger.core')
|
|
4
|
+
|
|
5
|
+
// Socket.IO connection handlers. The socket core calls this with the io server and
|
|
6
|
+
// the Socket facade after the server is attached. Register namespaces, rooms, and
|
|
7
|
+
// event listeners here.
|
|
8
|
+
module.exports = (io, Socket) => {
|
|
9
|
+
io.on('connection', (socket) => {
|
|
10
|
+
// Example: echo a ping back to the sender.
|
|
11
|
+
socket.on('ping', (payload) => {
|
|
12
|
+
socket.emit('pong', { received: payload, at: Date.now() })
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Common = require('@core/common.core')
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
env: Common.getEnv('APP_ENV', 'development'),
|
|
7
|
+
name: Common.getEnv('APP_NAME', 'rebe'),
|
|
8
|
+
url: Common.getEnv('APP_URL', 'http://localhost:3000'),
|
|
9
|
+
port: Common.getEnvInt('APP_PORT', 3000),
|
|
10
|
+
timezone: Common.getEnv('APP_TIMEZONE', 'UTC'),
|
|
11
|
+
key: Common.getEnv('APP_KEY'),
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cors = require('cors')
|
|
4
|
+
|
|
5
|
+
const Common = require('@core/common.core')
|
|
6
|
+
|
|
7
|
+
// Allowed origins are read from the environment so deployments can lock CORS down
|
|
8
|
+
// without code changes. A literal '*' means "any origin"; any other value is parsed
|
|
9
|
+
// as a comma-separated allowlist.
|
|
10
|
+
//
|
|
11
|
+
// Security note: the browser CORS spec forbids combining a wildcard origin with
|
|
12
|
+
// `credentials: true`. When the origin is '*' we therefore force credentials off.
|
|
13
|
+
// Set explicit origins (e.g. CORS_ORIGIN=https://app.example.com) to enable
|
|
14
|
+
// credentialed cross-origin requests.
|
|
15
|
+
const expressOrigins = Common.getEnvArray('CORS_ORIGIN', ['*'])
|
|
16
|
+
const socketOrigins = Common.getEnvArray('SOCKET_CORS_ORIGIN', ['*'])
|
|
17
|
+
|
|
18
|
+
const expressWildcard = expressOrigins.length === 1 && expressOrigins[0] === '*'
|
|
19
|
+
const socketWildcard = socketOrigins.length === 1 && socketOrigins[0] === '*'
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
express: cors({
|
|
23
|
+
origin: expressWildcard ? '*' : expressOrigins,
|
|
24
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
|
|
25
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
26
|
+
credentials: Common.getEnvBool('CORS_CREDENTIALS', !expressWildcard),
|
|
27
|
+
maxAge: Common.getEnvInt('CORS_MAX_AGE', 86400),
|
|
28
|
+
}),
|
|
29
|
+
socket: {
|
|
30
|
+
cors: {
|
|
31
|
+
origin: socketWildcard ? '*' : socketOrigins,
|
|
32
|
+
methods: ['GET', 'POST'],
|
|
33
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
34
|
+
credentials: Common.getEnvBool('SOCKET_CORS_CREDENTIALS', !socketWildcard),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Common = require('@core/common.core')
|
|
4
|
+
const dialect = Common.getEnv('DB_DIALECT', 'mysql')
|
|
5
|
+
|
|
6
|
+
const dialectOptions = {
|
|
7
|
+
mysql: { charset: 'utf8mb4', connectTimeout: Common.getEnvInt('DB_CONNECT_TIMEOUT', 10000) },
|
|
8
|
+
mariadb: { charset: 'utf8mb4', connectTimeout: Common.getEnvInt('DB_CONNECT_TIMEOUT', 10000) },
|
|
9
|
+
postgres: { ssl: Common.getEnvBool('DB_SSL', false) ? { rejectUnauthorized: false } : false },
|
|
10
|
+
mssql: { options: { encrypt: Common.getEnvBool('DB_SSL', false), trustServerCertificate: Common.getEnvBool('DB_TRUST_CERT', true), requestTimeout: Common.getEnvInt('DB_REQUEST_TIMEOUT', 30000) } },
|
|
11
|
+
sqlite: {},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
enabled: Common.getEnvBool('DB_ENABLED', false),
|
|
16
|
+
dialect,
|
|
17
|
+
host: Common.getEnv('DB_HOST', 'localhost'),
|
|
18
|
+
port: Common.getEnvInt('DB_PORT', getDefaultPort(dialect)),
|
|
19
|
+
username: Common.getEnv('DB_USER', null),
|
|
20
|
+
password: Common.getEnv('DB_PASS', null),
|
|
21
|
+
database: Common.getEnv('DB_NAME', null),
|
|
22
|
+
timezone: Common.getEnv('DB_TIMEZONE', '+00:00'),
|
|
23
|
+
logging: Common.getEnvBool('DB_LOGGING', false),
|
|
24
|
+
benchmark: Common.getEnvBool('DB_BENCHMARK', false),
|
|
25
|
+
logQueryParameters: Common.getEnvBool('DB_LOG_PARAMS', false),
|
|
26
|
+
define: {
|
|
27
|
+
underscored: Common.getEnvBool('DB_UNDERSCORED', false),
|
|
28
|
+
freezeTableName: Common.getEnvBool('DB_FREEZE_TABLE', false),
|
|
29
|
+
timestamps: Common.getEnvBool('DB_TIMESTAMPS', true),
|
|
30
|
+
paranoid: Common.getEnvBool('DB_PARANOID', false),
|
|
31
|
+
},
|
|
32
|
+
sync: {
|
|
33
|
+
force: Common.getEnvBool('DB_FORCE', false),
|
|
34
|
+
alter: Common.getEnvBool('DB_ALTER', false),
|
|
35
|
+
},
|
|
36
|
+
pool: {
|
|
37
|
+
max: Common.getEnvInt('DB_POOL_MAX', 10),
|
|
38
|
+
min: Common.getEnvInt('DB_POOL_MIN', 0),
|
|
39
|
+
acquire: Common.getEnvInt('DB_POOL_ACQUIRE', 30000),
|
|
40
|
+
idle: Common.getEnvInt('DB_POOL_IDLE', 10000),
|
|
41
|
+
},
|
|
42
|
+
retry: {
|
|
43
|
+
max: Common.getEnvInt('DB_RETRY_MAX', 3),
|
|
44
|
+
},
|
|
45
|
+
dialectOptions: dialectOptions[dialect] ?? {},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getDefaultPort(dialect) {
|
|
49
|
+
const ports = { mysql: 3306, mariadb: 3306, postgres: 5432, mssql: 1433, sqlite: null }
|
|
50
|
+
return ports[dialect] ?? 3306
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Common = require('@core/common.core')
|
|
4
|
+
|
|
5
|
+
// HTTP layer configuration consumed by express.core.
|
|
6
|
+
//
|
|
7
|
+
// - bodyLimit: max request body size for json/urlencoded parsers (DoS guard).
|
|
8
|
+
// - trustProxy: number of proxy hops to trust, or false. Required for correct
|
|
9
|
+
// client IPs behind a load balancer / reverse proxy; also makes
|
|
10
|
+
// express-rate-limit key on the real client IP. Leave 0/false
|
|
11
|
+
// when the app is directly exposed.
|
|
12
|
+
// - rateLimit: global request throttle. Disable per-route logic in app code.
|
|
13
|
+
module.exports = {
|
|
14
|
+
bodyLimit: Common.getEnv('EXPRESS_BODY_LIMIT', '10mb'),
|
|
15
|
+
trustProxy: Common.getEnvInt('EXPRESS_TRUST_PROXY', 0),
|
|
16
|
+
rateLimit: {
|
|
17
|
+
enabled: Common.getEnvBool('EXPRESS_RATE_LIMIT_ENABLED', true),
|
|
18
|
+
windowMs: Common.getEnvInt('EXPRESS_RATE_LIMIT_WINDOW_MS', 900000),
|
|
19
|
+
max: Common.getEnvInt('EXPRESS_RATE_LIMIT_MAX', 100),
|
|
20
|
+
standardHeaders: true,
|
|
21
|
+
legacyHeaders: false,
|
|
22
|
+
message: { status: false, code: 429, message: 'Too many requests', data: null, meta: null },
|
|
23
|
+
},
|
|
24
|
+
helmet: {
|
|
25
|
+
referrerPolicy: {
|
|
26
|
+
policy: 'origin',
|
|
27
|
+
},
|
|
28
|
+
contentSecurityPolicy: {
|
|
29
|
+
directives: {
|
|
30
|
+
defaultSrc: ["'self'"],
|
|
31
|
+
scriptSrc: ["'self'"],
|
|
32
|
+
imgSrc: ["'self'", 'data:'],
|
|
33
|
+
connectSrc: ["'self'"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|