langaro-api 1.2.2 → 1.2.4

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.
@@ -0,0 +1,238 @@
1
+ # Middlewares
2
+
3
+ ## Role
4
+
5
+ Middlewares intercept requests before they reach controllers. The primary middleware is the **auth middleware** which handles authentication and authorization.
6
+
7
+ **What middlewares do:**
8
+ - Authenticate requests (JWT, API key)
9
+ - Check user roles and permissions
10
+ - Enrich `req` with user data
11
+ - Gate access to routes
12
+
13
+ **What middlewares do NOT do:**
14
+ - Execute business logic
15
+ - Return final responses (except for auth failures)
16
+ - Access the database for business operations
17
+
18
+ ---
19
+
20
+ ## Location & Naming
21
+
22
+ ```
23
+ src/middlewares/
24
+ ├── auth.middleware.js # Authentication & authorization
25
+ ├── webhooks.middleware.js # Webhook signature validation (if needed)
26
+ └── ...
27
+ ```
28
+
29
+ **Naming convention:** `{name}.middleware.js`
30
+
31
+ ---
32
+
33
+ ## Auth Middleware (Core)
34
+
35
+ The auth middleware is the most critical middleware. It's created by `langaro-api init` and customized per project.
36
+
37
+ ### Factory Pattern
38
+
39
+ ```javascript
40
+ const jwt = require('jsonwebtoken');
41
+ const { ApiError, StatusCodes } = require('@/utils');
42
+
43
+ /** @param {ServicesMap} services @generated-types */
44
+ module.exports = function (services) {
45
+ return function auth(permissionsRequired, authorizedRoles, companyIdRequired) {
46
+ return async (req, res, next) => {
47
+ // Authentication and authorization logic
48
+ };
49
+ };
50
+ };
51
+ ```
52
+
53
+ **Structure:** Factory → returns configurator → returns Express middleware
54
+
55
+ ### Authentication Strategies
56
+
57
+ **1. JWT Bearer Token:**
58
+ ```
59
+ Header: Authorization: Bearer <jwt-token>
60
+ ```
61
+ - Decoded with `jwt.verify(token, process.env.JWT_SECRET)`
62
+ - Payload contains `user_id` (and optionally `email`)
63
+ - Token invalidated if issued before last password change
64
+
65
+ **2. API Key:**
66
+ ```
67
+ Header: api_key: <key>
68
+ ```
69
+ - Looked up against the users table
70
+ - Useful for programmatic access / integrations
71
+
72
+ ### Authorization Flow
73
+
74
+ ```
75
+ 1. Extract token from Authorization header OR api_key header
76
+ 2. Validate token/key → get user
77
+ 3. Check user.role against authorizedRoles
78
+ 4. If permissionsRequired specified:
79
+ a. Load user's permissions (from users_permissions table)
80
+ b. For each required { subject, action }:
81
+ - Find matching permission in user's list
82
+ - Wildcard action '*' matches any action
83
+ 5. If companyIdRequired:
84
+ a. Read company_id from request headers
85
+ b. Verify user has access to that company
86
+ 6. Set req.user_id, req.email, req.role, req.company_id, etc.
87
+ 7. Call next()
88
+ ```
89
+
90
+ ### Request Properties Set
91
+
92
+ After successful authentication, the middleware sets:
93
+
94
+ ```javascript
95
+ req.user_id // User's UUID
96
+ req.email // User's email
97
+ req.role // 'user' or 'admin'
98
+ req.company_id // Company ID (from header, if required)
99
+ req.user_permissions // Array of { subject, action }
100
+ req.token // Raw JWT token string
101
+ req.user // { id, email, segment } for Sentry
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Permission System
107
+
108
+ Permissions follow a **subject + action** pattern (Attribute-Based Access Control):
109
+
110
+ ```javascript
111
+ // Permission record in database
112
+ {
113
+ user_id: 'uuid-123',
114
+ subject: 'invoices',
115
+ action: '*' // Wildcard: all actions on 'invoices'
116
+ }
117
+
118
+ // Or specific action
119
+ {
120
+ user_id: 'uuid-123',
121
+ subject: 'invoices',
122
+ action: 'delete' // Only delete permission
123
+ }
124
+ ```
125
+
126
+ ### In Routes
127
+
128
+ ```javascript
129
+ // Require any permission on subject
130
+ auth([{ subject: 'invoices' }])
131
+
132
+ // Require specific action
133
+ auth([{ subject: 'invoices', action: 'delete' }])
134
+
135
+ // Require one of multiple permissions (OR logic between entries)
136
+ auth([{ subject: 'invoices' }, { subject: 'admin' }])
137
+
138
+ // No permission check, just authentication
139
+ auth([])
140
+ ```
141
+
142
+ A user with `action: '*'` on a subject automatically passes any action check for that subject.
143
+
144
+ ---
145
+
146
+ ## Creating Custom Middleware
147
+
148
+ Follow the same factory pattern:
149
+
150
+ ```javascript
151
+ /** @param {ServicesMap} services @generated-types */
152
+ module.exports = function (services) {
153
+ return async (req, res, next) => {
154
+ try {
155
+ // Custom logic (e.g., rate limiting, logging, feature flags)
156
+ const feature = await services.FeaturesServices.getWhere(
157
+ 'name', 'new-dashboard', { firstOnly: true }
158
+ );
159
+
160
+ req.featureFlags = { newDashboard: feature?.enabled || false };
161
+ next();
162
+ } catch (error) {
163
+ next(error);
164
+ }
165
+ };
166
+ };
167
+ ```
168
+
169
+ ### Webhook Signature Validation
170
+
171
+ ```javascript
172
+ module.exports = function (services) {
173
+ return async (req, res, next) => {
174
+ const secret = req.headers.authorization;
175
+
176
+ if (process.env.NODE_ENV === 'development') {
177
+ return next(); // Skip in development
178
+ }
179
+
180
+ if (secret !== process.env.ENDPOINT_SECRET) {
181
+ ApiError(StatusCodes.FORBIDDEN, 'Invalid webhook signature', undefined, next);
182
+ return;
183
+ }
184
+
185
+ next();
186
+ };
187
+ };
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Creating from Scratch
193
+
194
+ 1. **Create the file:** `src/middlewares/{name}.middleware.js`
195
+ 2. **Use the template:**
196
+ ```javascript
197
+ /** @param {ServicesMap} services @generated-types */
198
+ module.exports = function (services) {
199
+ return async (req, res, next) => {
200
+ try {
201
+ // Middleware logic
202
+ next();
203
+ } catch (error) {
204
+ next(error);
205
+ }
206
+ };
207
+ };
208
+ ```
209
+ 3. **Apply in routes:**
210
+ ```javascript
211
+ const customMiddleware = require('@/middlewares/custom.middleware')(services);
212
+ router.get('/', customMiddleware, controller.getAll.bind(controller));
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Anti-patterns
218
+
219
+ - **Do NOT put business logic in middleware.** Keep middleware focused on cross-cutting concerns.
220
+ - **Do NOT forget to call `next()`** — missing `next()` hangs the request.
221
+ - **Do NOT catch errors without re-throwing or calling `next(error)`.**
222
+ - **Do NOT duplicate auth logic.** Use the auth middleware factory, don't create parallel auth checks.
223
+ - **Do NOT access `req.body` for auth decisions.** Auth should use headers only.
224
+
225
+ ---
226
+
227
+ ## Checklist
228
+
229
+ When creating or modifying middleware:
230
+
231
+ - [ ] File is named `{name}.middleware.js`
232
+ - [ ] File is in `src/middlewares/`
233
+ - [ ] Exports a factory function `(services) => middleware`
234
+ - [ ] Has correct `@generated-types` JSDoc annotation
235
+ - [ ] Always calls `next()` or `next(error)`
236
+ - [ ] Error paths use `ApiError()` with appropriate status codes
237
+ - [ ] Does not contain business logic
238
+ - [ ] Auth middleware correctly validates both JWT and API key paths
@@ -0,0 +1,332 @@
1
+ # Integrations
2
+
3
+ ## Role
4
+
5
+ Integrations are **wrappers around external services**. They abstract third-party APIs (Stripe, AWS, Postmark, Google, etc.) into clean internal interfaces.
6
+
7
+ **What integrations do:**
8
+ - Encapsulate external API calls
9
+ - Normalize error handling
10
+ - Manage API credentials via environment variables
11
+ - Provide reusable methods for common operations
12
+
13
+ **What integrations do NOT do:**
14
+ - Contain business logic (that's services)
15
+ - Handle HTTP requests (that's controllers)
16
+ - Get auto-loaded by the framework (they're manually imported)
17
+
18
+ ---
19
+
20
+ ## Location & Naming
21
+
22
+ ```
23
+ src/integrations/
24
+ ├── stripe.js # Payment processor
25
+ ├── aws.js # S3 file storage
26
+ ├── google.js # OAuth integration
27
+ ├── cnpj.js # External data lookup
28
+ ├── openai.js # AI API
29
+ ├── postmark/ # Complex integration (directory)
30
+ │ ├── index.js # Main Postmark class
31
+ │ └── templates/ # Email templates
32
+ │ ├── welcome.html
33
+ │ └── invoice.html
34
+ ├── focus-nfe/ # Another complex integration
35
+ │ ├── index.js
36
+ │ └── schemas/
37
+ └── apps/ # Webhook handlers for multiple platforms
38
+ ├── webhooks/
39
+ │ ├── stripe.js
40
+ │ ├── shopify.js
41
+ │ └── ...
42
+ └── schemas/
43
+ ```
44
+
45
+ **Naming convention:** `{service-name}.js` for simple integrations, `{service-name}/index.js` for complex ones with related files.
46
+
47
+ ---
48
+
49
+ ## Standard Patterns
50
+
51
+ ### Class-Based Integration (most common)
52
+
53
+ ```javascript
54
+ const axios = require('axios');
55
+
56
+ class StripeIntegration {
57
+ constructor() {
58
+ this.stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
59
+ }
60
+
61
+ async createCustomer(email, name) {
62
+ return this.stripe.customers.create({ email, name });
63
+ }
64
+
65
+ async createPaymentIntent(amount, currency = 'brl') {
66
+ return this.stripe.paymentIntents.create({
67
+ amount,
68
+ currency,
69
+ automatic_payment_methods: { enabled: true },
70
+ });
71
+ }
72
+ }
73
+
74
+ module.exports = new StripeIntegration();
75
+ ```
76
+
77
+ ### Singleton Export
78
+
79
+ Most integrations export a singleton instance:
80
+
81
+ ```javascript
82
+ module.exports = new StripeIntegration();
83
+
84
+ // Used as:
85
+ const stripe = require('@/integrations/stripe');
86
+ await stripe.createCustomer(email, name);
87
+ ```
88
+
89
+ ### AWS S3 Pattern
90
+
91
+ ```javascript
92
+ const { S3Client } = require('@aws-sdk/client-s3');
93
+ const { Upload } = require('@aws-sdk/lib-storage');
94
+
95
+ class AWS {
96
+ constructor() {
97
+ this.s3 = new S3Client({
98
+ region: process.env.AWS_REGION,
99
+ credentials: {
100
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
101
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
102
+ },
103
+ });
104
+ }
105
+
106
+ async uploadToS3(bucket, { filename, file }) {
107
+ const upload = new Upload({
108
+ client: this.s3,
109
+ params: {
110
+ Bucket: bucket,
111
+ Key: filename,
112
+ Body: file,
113
+ ACL: 'public-read',
114
+ },
115
+ });
116
+
117
+ const { Location } = await upload.done();
118
+ return Location; // Returns the public URL
119
+ }
120
+
121
+ async uploadToS3FromUrl(bucket, url, filename) {
122
+ const response = await axios.get(url, {
123
+ responseType: 'arraybuffer',
124
+ timeout: 240000,
125
+ });
126
+ return this.uploadToS3(bucket, { filename, file: response.data });
127
+ }
128
+ }
129
+
130
+ module.exports = new AWS();
131
+ ```
132
+
133
+ ### Email Service Pattern (Postmark)
134
+
135
+ ```javascript
136
+ const postmark = require('postmark');
137
+
138
+ class Postmark {
139
+ constructor() {
140
+ this.client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);
141
+ }
142
+
143
+ filterInvalidRecipients(recipientsList) {
144
+ return recipientsList.filter(r => r.Email || r.email);
145
+ }
146
+
147
+ async sendMail(templateName, recipientsList) {
148
+ recipientsList = this.filterInvalidRecipients(recipientsList);
149
+ if (!recipientsList.length) return [];
150
+
151
+ const Messages = recipientsList.map(r => ({
152
+ From: 'contact@yourdomain.com',
153
+ To: r.Email || r.email,
154
+ TemplateAlias: templateName,
155
+ TemplateModel: r.TemplateModel || r.variables || {},
156
+ Attachments: r.Attachments || r.attachments || [],
157
+ }));
158
+
159
+ // Chunk sending (Postmark limit: 500 per batch)
160
+ const chunkSize = 400;
161
+ const responses = [];
162
+ for (let i = 0; i < Messages.length; i += chunkSize) {
163
+ const chunk = Messages.slice(i, i + chunkSize);
164
+ const response = await this.client.sendEmailBatchWithTemplates(chunk);
165
+ responses.push(...response);
166
+ }
167
+
168
+ return responses;
169
+ }
170
+ }
171
+
172
+ module.exports = new Postmark();
173
+ ```
174
+
175
+ ### External Database Connection
176
+
177
+ ```javascript
178
+ const knex = require('knex');
179
+
180
+ const externalDb = knex({
181
+ client: 'mysql2',
182
+ connection: {
183
+ host: process.env.EXTERNAL_DB_HOST,
184
+ user: process.env.EXTERNAL_DB_USER,
185
+ password: process.env.EXTERNAL_DB_PASSWORD,
186
+ database: process.env.EXTERNAL_DB_NAME,
187
+ port: process.env.EXTERNAL_DB_PORT,
188
+ },
189
+ pool: { min: 2, max: 10 },
190
+ });
191
+
192
+ module.exports = externalDb;
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Usage in Services and Controllers
198
+
199
+ Integrations are imported directly where needed:
200
+
201
+ ```javascript
202
+ // In a service
203
+ const aws = require('@/integrations/aws');
204
+ const postmark = require('@/integrations/postmark');
205
+
206
+ module.exports = (model, models, io) => class extends model {
207
+ async createAndNotify(data) {
208
+ const record = await this.create(data);
209
+
210
+ // Upload file
211
+ if (data.file) {
212
+ const url = await aws.uploadToS3(process.env.S3_BUCKET, {
213
+ filename: `${record.data.id}/file.pdf`,
214
+ file: data.file,
215
+ });
216
+ await this.updateWhere('id', record.data.id, { file_url: url });
217
+ }
218
+
219
+ // Send notification
220
+ await postmark.sendMail('record-created', [{
221
+ Email: data.email,
222
+ TemplateModel: { name: data.name },
223
+ }]);
224
+
225
+ return record;
226
+ }
227
+ };
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Error Handling
233
+
234
+ Wrap external calls to provide clean error messages:
235
+
236
+ ```javascript
237
+ async createCustomer(email) {
238
+ try {
239
+ return await this.stripe.customers.create({ email });
240
+ } catch (error) {
241
+ // Log the original error for debugging
242
+ console.error('Stripe error:', error.message);
243
+
244
+ // Throw a clean error for the service layer
245
+ throw new Error(`Failed to create Stripe customer: ${error.message}`);
246
+ }
247
+ }
248
+ ```
249
+
250
+ For errors that should stop the request with a user-facing message:
251
+
252
+ ```javascript
253
+ const { ApiError, StatusCodes } = require('@/utils');
254
+
255
+ async lookupCNPJ(cnpj) {
256
+ try {
257
+ const { data } = await axios.get(`https://api.example.com/cnpj/${cnpj}`);
258
+ return data;
259
+ } catch (error) {
260
+ if (error.response?.status === 404) {
261
+ ApiError(StatusCodes.NOT_FOUND, 'CNPJ not found');
262
+ }
263
+ throw error; // Let other errors bubble up to Sentry
264
+ }
265
+ }
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Environment Variables
271
+
272
+ Every integration should use environment variables for credentials:
273
+
274
+ ```
275
+ # .env
276
+ STRIPE_SECRET_KEY="sk_live_..."
277
+ AWS_ACCESS_KEY_ID="AKIA..."
278
+ AWS_SECRET_ACCESS_KEY="..."
279
+ AWS_REGION="us-east-1"
280
+ POSTMARK_API_KEY="..."
281
+ GOOGLE_CLIENT_ID="..."
282
+ GOOGLE_CLIENT_SECRET="..."
283
+ ```
284
+
285
+ Never hardcode API keys, URLs, or credentials in integration files.
286
+
287
+ ---
288
+
289
+ ## Creating from Scratch
290
+
291
+ 1. **Create the file:** `src/integrations/{service-name}.js`
292
+ 2. **Define the class:**
293
+ ```javascript
294
+ class ServiceName {
295
+ constructor() {
296
+ // Initialize client with env vars
297
+ }
298
+
299
+ async methodName(params) {
300
+ // Wrap API call with error handling
301
+ }
302
+ }
303
+
304
+ module.exports = new ServiceName();
305
+ ```
306
+ 3. **Add environment variables** to `.env` and `.env.example`
307
+ 4. **Import in services/controllers** as needed
308
+
309
+ ---
310
+
311
+ ## Anti-patterns
312
+
313
+ - **Do NOT hardcode credentials.** Always use environment variables.
314
+ - **Do NOT put business logic in integrations.** They are pure API wrappers.
315
+ - **Do NOT expose raw external errors to clients.** Catch and wrap with clean messages.
316
+ - **Do NOT create integrations that import services.** Integrations are lower-level — services import integrations, not the reverse.
317
+ - **Do NOT forget timeout configuration** for HTTP calls to external services.
318
+
319
+ ---
320
+
321
+ ## Checklist
322
+
323
+ When creating a new integration:
324
+
325
+ - [ ] File is in `src/integrations/`
326
+ - [ ] Uses environment variables for all credentials
327
+ - [ ] Env vars documented in `.env.example`
328
+ - [ ] Exports a singleton instance (or class for multiple-instance use)
329
+ - [ ] Error handling wraps external errors cleanly
330
+ - [ ] HTTP calls have timeout configuration
331
+ - [ ] No business logic — pure API abstraction
332
+ - [ ] No circular dependencies with services