@nano-app/backend 0.6.0 → 0.6.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.
- package/dist/app.js +25 -38
- package/dist/auth/ApiKeys.d.ts +25 -0
- package/dist/auth/ApiKeys.js +146 -0
- package/dist/auth/ApiKeysEndpoint.d.ts +19 -0
- package/dist/auth/ApiKeysEndpoint.js +36 -0
- package/dist/auth/ClientCredentials.d.ts +15 -0
- package/dist/auth/ClientCredentials.js +132 -0
- package/dist/auth/TokenEndpoint.d.ts +5 -0
- package/dist/auth/TokenEndpoint.js +33 -0
- package/dist/http/Endpoint.d.ts +9 -15
- package/dist/http/Endpoint.js +35 -43
- package/dist/http/Router.js +1 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/odata/Entities.d.ts +8 -7
- package/dist/odata/Entities.js +17 -15
- package/dist/odata/QueryBuilder.d.ts +5 -28
- package/dist/odata/QueryBuilder.js +50 -99
- package/dist/types/app.d.ts +13 -1
- package/dist/types/odata.d.ts +0 -4
- package/dist/utils/Formatter.d.ts +2 -0
- package/dist/utils/Formatter.js +12 -0
- package/package.json +6 -2
package/dist/app.js
CHANGED
|
@@ -11,6 +11,8 @@ const compression_1 = __importDefault(require("compression"));
|
|
|
11
11
|
const morgan_1 = __importDefault(require("morgan"));
|
|
12
12
|
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
13
13
|
const index_1 = require("./index");
|
|
14
|
+
const ApiKeys_1 = require("./auth/ApiKeys");
|
|
15
|
+
const ApiKeysEndpoint_1 = require("./auth/ApiKeysEndpoint");
|
|
14
16
|
class NanoApp {
|
|
15
17
|
app = (0, express_1.default)();
|
|
16
18
|
router;
|
|
@@ -19,12 +21,11 @@ class NanoApp {
|
|
|
19
21
|
enableEntities: true,
|
|
20
22
|
port: 3000,
|
|
21
23
|
rateLimit: {
|
|
22
|
-
windowMs: 15 * 60 * 1000,
|
|
23
|
-
max: 1000,
|
|
24
|
-
message: 'Too many requests, please try again later.'
|
|
24
|
+
windowMs: 15 * 60 * 1000,
|
|
25
|
+
max: 1000,
|
|
26
|
+
message: 'Too many requests, please try again later.',
|
|
25
27
|
},
|
|
26
28
|
morgan: {
|
|
27
|
-
tokens: {},
|
|
28
29
|
format: 'combined',
|
|
29
30
|
},
|
|
30
31
|
expressConfig: {
|
|
@@ -32,55 +33,41 @@ class NanoApp {
|
|
|
32
33
|
},
|
|
33
34
|
};
|
|
34
35
|
constructor(config) {
|
|
35
|
-
|
|
36
|
-
this.config
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.router = new index_1.Router(this.config.endpoints.concat(this.config.enableEntities ? [new index_1.Entities('/:endpoint{/:id}')] : []));
|
|
42
|
-
// Inicializar setters
|
|
36
|
+
this.config = { ...this.config, ...config, apiKeys: config.apiKeys };
|
|
37
|
+
const apiKeysPath = this.config.apiKeys?.path ?? '/api-keys';
|
|
38
|
+
const apiKeyEndpoints = this.config.apiKeys?.enabled ? [new ApiKeysEndpoint_1.ApiKeysEndpoint(apiKeysPath)] : [];
|
|
39
|
+
const entityEndpoints = this.config.enableEntities ? [new index_1.Entities('/:endpoint{/:id}')] : [];
|
|
40
|
+
console.log('Endpoints registered:');
|
|
41
|
+
this.router = new index_1.Router([...apiKeyEndpoints, ...this.config.endpoints, ...entityEndpoints]);
|
|
43
42
|
if (this.config.setters) {
|
|
44
|
-
Object.entries(this.config.setters).forEach(([key, value]) =>
|
|
45
|
-
this.app.set(key, value);
|
|
46
|
-
});
|
|
43
|
+
Object.entries(this.config.setters).forEach(([key, value]) => this.app.set(key, value));
|
|
47
44
|
}
|
|
48
|
-
|
|
49
|
-
Object.entries(this.config.morgan?.tokens || {}).forEach(([key, value]) => {
|
|
50
|
-
morgan_1.default.token(key, value);
|
|
51
|
-
});
|
|
45
|
+
Object.entries(this.config.morgan?.tokens ?? {}).forEach(([key, value]) => morgan_1.default.token(key, value));
|
|
52
46
|
this.app.use((0, morgan_1.default)(this.config.morgan?.format ?? 'combined'));
|
|
53
|
-
// Inicializar seguridad y optimizacion
|
|
54
47
|
this.app.use((0, cors_1.default)());
|
|
55
48
|
this.app.use((0, helmet_1.default)());
|
|
56
49
|
this.app.use((0, compression_1.default)());
|
|
57
|
-
// Inicializar proteccion contra ataques
|
|
58
50
|
this.app.use((0, express_rate_limit_1.default)(this.config.rateLimit));
|
|
59
|
-
// Inicializar limite de tamaño del body
|
|
60
51
|
this.app.use(express_1.default.json(this.config.expressConfig));
|
|
52
|
+
this.app.use(express_1.default.urlencoded({ extended: false }));
|
|
61
53
|
}
|
|
62
54
|
getApp() { return this.app; }
|
|
63
55
|
async start(listener) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.app.use('/', this.router.getRoutes());
|
|
68
|
-
// Middleware de error (debe ir al final)
|
|
69
|
-
if (this.config.errorMiddleware) {
|
|
70
|
-
this.app.use(this.config.errorMiddleware);
|
|
56
|
+
if (this.config.apiKeys?.enabled) {
|
|
57
|
+
await ApiKeys_1.ApiKeys.init();
|
|
58
|
+
this.app.use(ApiKeys_1.ApiKeys.buildMiddleware(this.config.apiKeys.middleware));
|
|
71
59
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
60
|
+
(this.config.middlewares ?? []).forEach(middleware => this.app.use(middleware));
|
|
61
|
+
this.app.use('/', this.router.getRoutes());
|
|
62
|
+
this.app.use(this.config.errorMiddleware ?? ((err, _, res, __) => {
|
|
63
|
+
res.status(err.status || 500).json({
|
|
64
|
+
success: false,
|
|
65
|
+
message: err.message || 'Error interno del servidor',
|
|
78
66
|
});
|
|
79
|
-
}
|
|
80
|
-
// Iniciar el servidor
|
|
67
|
+
}));
|
|
81
68
|
this.app.listen(Number(this.config.port), () => {
|
|
82
69
|
console.log(`Server running on port ${this.config.port}`);
|
|
83
|
-
listener?.();
|
|
70
|
+
listener?.();
|
|
84
71
|
});
|
|
85
72
|
}
|
|
86
73
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
export declare class ApiKeys {
|
|
3
|
+
private static pool;
|
|
4
|
+
private static initialized;
|
|
5
|
+
static init(): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Construye el middleware de autenticación:
|
|
8
|
+
* - Sin customMiddleware → valida solo x-api-key.
|
|
9
|
+
* - Con customMiddleware → intenta primero el middleware custom.
|
|
10
|
+
* Si falla, intenta x-api-key. Si ambos fallan → 401.
|
|
11
|
+
*/
|
|
12
|
+
static buildMiddleware(customMiddleware?: RequestHandler): RequestHandler;
|
|
13
|
+
static create(label?: string): Promise<string>;
|
|
14
|
+
static validate(key: string): Promise<boolean>;
|
|
15
|
+
static list(): Promise<{
|
|
16
|
+
id: number;
|
|
17
|
+
label: string | null;
|
|
18
|
+
created_at: Date;
|
|
19
|
+
}[]>;
|
|
20
|
+
static revoke(id: number): Promise<void>;
|
|
21
|
+
private static validateApiKey;
|
|
22
|
+
private static runMiddleware;
|
|
23
|
+
private static printInitialKey;
|
|
24
|
+
private static assertInitialized;
|
|
25
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ApiKeys = void 0;
|
|
37
|
+
const bcrypt = __importStar(require("bcrypt"));
|
|
38
|
+
const pg_1 = require("pg");
|
|
39
|
+
const crypto_1 = require("crypto");
|
|
40
|
+
const SCHEMA = "nano_auth";
|
|
41
|
+
const TABLE = `${SCHEMA}.api_keys`;
|
|
42
|
+
class ApiKeys {
|
|
43
|
+
static pool;
|
|
44
|
+
static initialized = false;
|
|
45
|
+
static async init() {
|
|
46
|
+
if (ApiKeys.initialized)
|
|
47
|
+
return;
|
|
48
|
+
const connectionString = process.env.DATABASE_URL;
|
|
49
|
+
if (!connectionString)
|
|
50
|
+
throw new Error("DATABASE_URL no está configurada");
|
|
51
|
+
ApiKeys.pool = new pg_1.Pool({ connectionString });
|
|
52
|
+
ApiKeys.initialized = true;
|
|
53
|
+
await ApiKeys.pool.query(`CREATE SCHEMA IF NOT EXISTS ${SCHEMA}`);
|
|
54
|
+
await ApiKeys.pool.query(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
56
|
+
id SERIAL PRIMARY KEY,
|
|
57
|
+
label TEXT,
|
|
58
|
+
key_hash TEXT NOT NULL,
|
|
59
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
60
|
+
)
|
|
61
|
+
`);
|
|
62
|
+
await ApiKeys.printInitialKey();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Construye el middleware de autenticación:
|
|
66
|
+
* - Sin customMiddleware → valida solo x-api-key.
|
|
67
|
+
* - Con customMiddleware → intenta primero el middleware custom.
|
|
68
|
+
* Si falla, intenta x-api-key. Si ambos fallan → 401.
|
|
69
|
+
*/
|
|
70
|
+
static buildMiddleware(customMiddleware) {
|
|
71
|
+
return async (req, res, next) => {
|
|
72
|
+
if (!customMiddleware) {
|
|
73
|
+
return ApiKeys.validateApiKey(req, res, next);
|
|
74
|
+
}
|
|
75
|
+
const customResult = await ApiKeys.runMiddleware(customMiddleware, req, res);
|
|
76
|
+
if (customResult.passed)
|
|
77
|
+
return next();
|
|
78
|
+
const key = req.headers['x-api-key'];
|
|
79
|
+
if (key && await ApiKeys.validate(key).catch(() => false)) {
|
|
80
|
+
req.apiKeyAuth = true;
|
|
81
|
+
return next();
|
|
82
|
+
}
|
|
83
|
+
return next(customResult.error ?? { status: 401, message: "No autorizado" });
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
static async create(label) {
|
|
87
|
+
ApiKeys.assertInitialized();
|
|
88
|
+
const key = `nk_${(0, crypto_1.randomBytes)(32).toString("hex")}`;
|
|
89
|
+
await ApiKeys.pool.query(`INSERT INTO ${TABLE} (label, key_hash) VALUES ($1, $2)`, [label ?? null, await bcrypt.hash(key, 10)]);
|
|
90
|
+
return key;
|
|
91
|
+
}
|
|
92
|
+
static async validate(key) {
|
|
93
|
+
ApiKeys.assertInitialized();
|
|
94
|
+
if (!key?.startsWith("nk_"))
|
|
95
|
+
return false;
|
|
96
|
+
const { rows } = await ApiKeys.pool.query(`SELECT key_hash FROM ${TABLE}`);
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
if (await bcrypt.compare(key, row.key_hash))
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
static async list() {
|
|
104
|
+
ApiKeys.assertInitialized();
|
|
105
|
+
const { rows } = await ApiKeys.pool.query(`SELECT id, label, created_at FROM ${TABLE} ORDER BY created_at DESC`);
|
|
106
|
+
return rows;
|
|
107
|
+
}
|
|
108
|
+
static async revoke(id) {
|
|
109
|
+
ApiKeys.assertInitialized();
|
|
110
|
+
await ApiKeys.pool.query(`DELETE FROM ${TABLE} WHERE id = $1`, [id]);
|
|
111
|
+
}
|
|
112
|
+
static async validateApiKey(req, _res, next) {
|
|
113
|
+
const key = req.headers['x-api-key'];
|
|
114
|
+
if (!key)
|
|
115
|
+
return next({ status: 401, message: "No autorizado" });
|
|
116
|
+
const valid = await ApiKeys.validate(key).catch(() => false);
|
|
117
|
+
if (valid) {
|
|
118
|
+
req.apiKeyAuth = true;
|
|
119
|
+
return next();
|
|
120
|
+
}
|
|
121
|
+
return next({ status: 401, message: "API key inválida" });
|
|
122
|
+
}
|
|
123
|
+
static async runMiddleware(middleware, req, res) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
middleware(req, res, (err) => {
|
|
126
|
+
resolve(err ? { passed: false, error: err } : { passed: true });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
static async printInitialKey() {
|
|
131
|
+
const { rows } = await ApiKeys.pool.query(`SELECT id FROM ${TABLE} LIMIT 1`);
|
|
132
|
+
if (rows.length > 0)
|
|
133
|
+
return;
|
|
134
|
+
const key = await ApiKeys.create('default');
|
|
135
|
+
console.log("\n══════════════════════════════════════════════════════");
|
|
136
|
+
console.log(" NANO-APP: API Key inicial generada ");
|
|
137
|
+
console.log(" Guarda esta key, no se volverá a mostrar. ");
|
|
138
|
+
console.log("══════════════════════════════════════════════════════");
|
|
139
|
+
console.log(`\n x-api-key: ${key}\n`);
|
|
140
|
+
}
|
|
141
|
+
static assertInitialized() {
|
|
142
|
+
if (!ApiKeys.initialized)
|
|
143
|
+
throw new Error("ApiKeys no inicializado.");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.ApiKeys = ApiKeys;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import { Endpoint } from "../http/Endpoint";
|
|
3
|
+
import type { ApiResponse } from "../types/api";
|
|
4
|
+
/**
|
|
5
|
+
* Endpoint predefinido para gestión de API keys.
|
|
6
|
+
* Registrado automáticamente por NanoApp en /api-keys.
|
|
7
|
+
*
|
|
8
|
+
* GET /api-keys — lista todas las keys (id, label, created_at)
|
|
9
|
+
* POST /api-keys — crea una nueva key { label?: string }
|
|
10
|
+
* DELETE /api-keys — revoca una key por { id: number }
|
|
11
|
+
*
|
|
12
|
+
* Para deshabilitar estos endpoints, usa apiKeys: { enabled: false } en NanoAppConfig.
|
|
13
|
+
* Para protegerlos con tu propia lógica, subclasifica y sobreescribe VALIDATE o BEFORE.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ApiKeysEndpoint extends Endpoint {
|
|
16
|
+
protected GET(_req: Request, _res: Response): Promise<ApiResponse>;
|
|
17
|
+
protected POST(req: Request, _res: Response): Promise<ApiResponse>;
|
|
18
|
+
protected DELETE(req: Request, _res: Response): Promise<ApiResponse>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiKeysEndpoint = void 0;
|
|
4
|
+
const Endpoint_1 = require("../http/Endpoint");
|
|
5
|
+
const ApiKeys_1 = require("./ApiKeys");
|
|
6
|
+
/**
|
|
7
|
+
* Endpoint predefinido para gestión de API keys.
|
|
8
|
+
* Registrado automáticamente por NanoApp en /api-keys.
|
|
9
|
+
*
|
|
10
|
+
* GET /api-keys — lista todas las keys (id, label, created_at)
|
|
11
|
+
* POST /api-keys — crea una nueva key { label?: string }
|
|
12
|
+
* DELETE /api-keys — revoca una key por { id: number }
|
|
13
|
+
*
|
|
14
|
+
* Para deshabilitar estos endpoints, usa apiKeys: { enabled: false } en NanoAppConfig.
|
|
15
|
+
* Para protegerlos con tu propia lógica, subclasifica y sobreescribe VALIDATE o BEFORE.
|
|
16
|
+
*/
|
|
17
|
+
class ApiKeysEndpoint extends Endpoint_1.Endpoint {
|
|
18
|
+
async GET(_req, _res) {
|
|
19
|
+
const keys = await ApiKeys_1.ApiKeys.list();
|
|
20
|
+
return { result: keys };
|
|
21
|
+
}
|
|
22
|
+
async POST(req, _res) {
|
|
23
|
+
const { label } = req.body ?? {};
|
|
24
|
+
const key = await ApiKeys_1.ApiKeys.create(label);
|
|
25
|
+
return { result: { api_key: key } };
|
|
26
|
+
}
|
|
27
|
+
async DELETE(req, _res) {
|
|
28
|
+
const { id } = req.body ?? {};
|
|
29
|
+
if (!id || typeof id !== "number") {
|
|
30
|
+
throw { status: 400, message: "El campo id es requerido y debe ser un número" };
|
|
31
|
+
}
|
|
32
|
+
await ApiKeys_1.ApiKeys.revoke(id);
|
|
33
|
+
return { result: { revoked: id } };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.ApiKeysEndpoint = ApiKeysEndpoint;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
import type { NanoAppAuthConfig } from "../index";
|
|
3
|
+
export declare class ClientCredentials {
|
|
4
|
+
private static config;
|
|
5
|
+
private static oauthSecret;
|
|
6
|
+
private static pool;
|
|
7
|
+
static init(config: NanoAppAuthConfig): Promise<void>;
|
|
8
|
+
static middleware: RequestHandler;
|
|
9
|
+
static generateToken(clientId: string, clientSecret: string): Promise<{
|
|
10
|
+
access_token: string;
|
|
11
|
+
token_type: string;
|
|
12
|
+
expires_in: number;
|
|
13
|
+
}>;
|
|
14
|
+
private static parseExpiry;
|
|
15
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ClientCredentials = void 0;
|
|
37
|
+
const jwt = __importStar(require("jsonwebtoken"));
|
|
38
|
+
const bcrypt = __importStar(require("bcrypt"));
|
|
39
|
+
const pg_1 = require("pg");
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
41
|
+
const SCHEMA = "nano_auth";
|
|
42
|
+
const TABLE = `${SCHEMA}.oauth_client`;
|
|
43
|
+
class ClientCredentials {
|
|
44
|
+
static config = {};
|
|
45
|
+
static oauthSecret;
|
|
46
|
+
static pool;
|
|
47
|
+
static async init(config) {
|
|
48
|
+
// Inicializar la configuración
|
|
49
|
+
const connectionString = process.env.DATABASE_URL ?? '';
|
|
50
|
+
ClientCredentials.config = config;
|
|
51
|
+
ClientCredentials.oauthSecret = process.env.OAUTH_SECRET ?? '';
|
|
52
|
+
// Crear el pool de conexiones
|
|
53
|
+
ClientCredentials.pool = new pg_1.Pool({ connectionString });
|
|
54
|
+
// Crear el schema si no existe
|
|
55
|
+
await ClientCredentials.pool.query(`CREATE SCHEMA IF NOT EXISTS ${SCHEMA}`);
|
|
56
|
+
await ClientCredentials.pool.query(`
|
|
57
|
+
CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
58
|
+
id SERIAL PRIMARY KEY,
|
|
59
|
+
client_id TEXT NOT NULL UNIQUE,
|
|
60
|
+
client_secret TEXT NOT NULL,
|
|
61
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
// Verificar si existen credenciales
|
|
65
|
+
const { rows } = await ClientCredentials.pool.query(`SELECT id FROM ${TABLE} LIMIT 1`);
|
|
66
|
+
if (rows.length === 0) {
|
|
67
|
+
// Generar credenciales
|
|
68
|
+
const clientId = (0, crypto_1.randomBytes)(16).toString("hex");
|
|
69
|
+
const clientSecret = (0, crypto_1.randomBytes)(32).toString("hex");
|
|
70
|
+
const hashed = await bcrypt.hash(clientSecret, 10);
|
|
71
|
+
// Insertar credenciales
|
|
72
|
+
await ClientCredentials.pool.query(`INSERT INTO ${TABLE} (client_id, client_secret) VALUES ($1, $2)`, [clientId, hashed]);
|
|
73
|
+
// Mostrar credenciales
|
|
74
|
+
console.log("\n══════════════════════════════════════════════════════");
|
|
75
|
+
console.log(" NANO-APP: OAuth Client Credentials ");
|
|
76
|
+
console.log(" Guarda estas credenciales, no se mostrarán más. ");
|
|
77
|
+
console.log("══════════════════════════════════════════════════════\n");
|
|
78
|
+
console.log(`client_id: ${clientId}`);
|
|
79
|
+
console.log(`client_secret: ${clientSecret}\n\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
static middleware = async (req, res, next) => {
|
|
83
|
+
const publicRoutes = ["/oauth/token", ...(ClientCredentials.config.publicRoutes ?? [])];
|
|
84
|
+
// Si la ruta es pública, continuar
|
|
85
|
+
if (publicRoutes.includes(req.path)) {
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
88
|
+
// Validar que el header de autenticación sea válido
|
|
89
|
+
const authHeader = req.headers.authorization;
|
|
90
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
91
|
+
return next({ status: 401, message: "Header de autenticación inválido" });
|
|
92
|
+
}
|
|
93
|
+
// Obtener el token y secret
|
|
94
|
+
const token = authHeader.slice(7);
|
|
95
|
+
// Verificar que el token sea válido
|
|
96
|
+
try {
|
|
97
|
+
const payload = jwt.verify(token, ClientCredentials.oauthSecret);
|
|
98
|
+
req.oauthClient = { clientId: payload.sub };
|
|
99
|
+
return next();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return next({ status: 401, message: "Sesión expirada" });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
static async generateToken(clientId, clientSecret) {
|
|
106
|
+
if (!ClientCredentials.pool) {
|
|
107
|
+
throw { status: 500, message: "ClientCredentials no inicializado" };
|
|
108
|
+
}
|
|
109
|
+
const { rows } = await ClientCredentials.pool.query(`SELECT client_id, client_secret FROM ${TABLE} WHERE client_id = $1 LIMIT 1`, [clientId]);
|
|
110
|
+
if (rows.length === 0) {
|
|
111
|
+
throw { status: 401, message: "Credenciales de cliente inválidas" };
|
|
112
|
+
}
|
|
113
|
+
const valid = await bcrypt.compare(clientSecret, rows[0].client_secret);
|
|
114
|
+
if (!valid) {
|
|
115
|
+
throw { status: 401, message: "Credenciales de cliente inválidas" };
|
|
116
|
+
}
|
|
117
|
+
const expiry = ClientCredentials.config.tokenExpiry ?? '1h';
|
|
118
|
+
const expiresInSeconds = ClientCredentials.parseExpiry(expiry);
|
|
119
|
+
const access_token = jwt.sign({ sub: rows[0].client_id }, ClientCredentials.oauthSecret, { expiresIn: expiry });
|
|
120
|
+
return { access_token, token_type: "Bearer", expires_in: expiresInSeconds };
|
|
121
|
+
}
|
|
122
|
+
static parseExpiry(expiry) {
|
|
123
|
+
const match = expiry.match(/^(\d+)(s|m|h|d)$/);
|
|
124
|
+
if (!match)
|
|
125
|
+
return 3600;
|
|
126
|
+
const value = parseInt(match[1], 10);
|
|
127
|
+
const unit = match[2];
|
|
128
|
+
const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
129
|
+
return value * (multipliers[unit] ?? 3600);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.ClientCredentials = ClientCredentials;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenEndpoint = void 0;
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
class TokenEndpoint extends index_1.Endpoint {
|
|
6
|
+
async POST(req, res) {
|
|
7
|
+
let clientId;
|
|
8
|
+
let clientSecret;
|
|
9
|
+
// Authorization: Basic <base64(clientId:clientSecret)>
|
|
10
|
+
const authHeader = req.headers.authorization;
|
|
11
|
+
if (authHeader?.startsWith("Basic ")) {
|
|
12
|
+
const decoded = Buffer.from(authHeader.slice(6), "base64").toString("utf-8");
|
|
13
|
+
const colon = decoded.indexOf(":");
|
|
14
|
+
if (colon !== -1) {
|
|
15
|
+
clientId = decoded.slice(0, colon);
|
|
16
|
+
clientSecret = decoded.slice(colon + 1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Body: client_id y client_secret (JSON o form-urlencoded)
|
|
20
|
+
if (!clientId || !clientSecret) {
|
|
21
|
+
clientId = req.body?.client_id;
|
|
22
|
+
clientSecret = req.body?.client_secret;
|
|
23
|
+
}
|
|
24
|
+
if (!clientId || !clientSecret) {
|
|
25
|
+
throw { status: 400, message: "client_id y client_secret son campos requeridos" };
|
|
26
|
+
}
|
|
27
|
+
const tokenData = await index_1.ClientCredentials.generateToken(clientId, clientSecret);
|
|
28
|
+
// RFC 6749 §5.1: la respuesta del token endpoint debe ser plana, sin wrappers
|
|
29
|
+
res.json(tokenData);
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.TokenEndpoint = TokenEndpoint;
|
package/dist/http/Endpoint.d.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import { Request, Response } from "express";
|
|
2
|
-
import { PrismaClient
|
|
2
|
+
import { PrismaClient } from "../prisma/Client";
|
|
3
|
+
import type { ApiResponse } from "../types/api";
|
|
3
4
|
export declare abstract class Endpoint<BeforeResult = any> {
|
|
4
5
|
protected prismaClient: PrismaClient;
|
|
5
|
-
private methodMap;
|
|
6
6
|
path: string;
|
|
7
|
+
private methodMap;
|
|
7
8
|
constructor(path?: string);
|
|
8
|
-
|
|
9
|
+
protected VALIDATE(_req: Request): Promise<void>;
|
|
9
10
|
protected BEFORE(_req: Request): Promise<BeforeResult>;
|
|
10
|
-
protected AFTER(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
protected GET(_req: Request, _res: Response, _beforeParams: BeforeResult): Promise<ApiResponse>;
|
|
16
|
-
protected POST(_req: Request, _res: Response, _beforeParams: BeforeResult): Promise<ApiResponse>;
|
|
17
|
-
protected PUT(_req: Request, _res: Response, _beforeParams: BeforeResult): Promise<ApiResponse>;
|
|
18
|
-
protected DELETE(_req: Request, _res: Response, _beforeParams: BeforeResult): Promise<ApiResponse>;
|
|
19
|
-
/**
|
|
20
|
-
* Ejecuta la lógica específica del endpoint
|
|
21
|
-
*/
|
|
11
|
+
protected AFTER(_req: Request, result: ApiResponse): Promise<ApiResponse>;
|
|
12
|
+
protected GET(_req: Request, _res: Response, _before: BeforeResult): Promise<ApiResponse>;
|
|
13
|
+
protected POST(_req: Request, _res: Response, _before: BeforeResult): Promise<ApiResponse>;
|
|
14
|
+
protected PUT(_req: Request, _res: Response, _before: BeforeResult): Promise<ApiResponse>;
|
|
15
|
+
protected DELETE(_req: Request, _res: Response, _before: BeforeResult): Promise<ApiResponse>;
|
|
22
16
|
execute(req: Request, res: Response): void;
|
|
23
17
|
}
|
package/dist/http/Endpoint.js
CHANGED
|
@@ -1,67 +1,59 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Endpoint = void 0;
|
|
4
|
-
const
|
|
4
|
+
const Client_1 = require("../prisma/Client");
|
|
5
|
+
const Formatter_1 = require("../utils/Formatter");
|
|
5
6
|
class Endpoint {
|
|
6
|
-
prismaClient = new
|
|
7
|
-
methodMap = {};
|
|
7
|
+
prismaClient = new Client_1.PrismaClient();
|
|
8
8
|
path = '';
|
|
9
|
+
methodMap;
|
|
9
10
|
constructor(path) {
|
|
10
|
-
this.path = path
|
|
11
|
+
this.path = path ?? 'default';
|
|
11
12
|
this.methodMap = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
GET: this.GET.bind(this),
|
|
14
|
+
POST: this.POST.bind(this),
|
|
15
|
+
PUT: this.PUT.bind(this),
|
|
16
|
+
DELETE: this.DELETE.bind(this),
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
VALIDATE(_req) {
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
19
22
|
BEFORE(_req) {
|
|
20
23
|
return Promise.resolve({});
|
|
21
24
|
}
|
|
22
|
-
AFTER(
|
|
23
|
-
return Promise.resolve(
|
|
25
|
+
AFTER(_req, result) {
|
|
26
|
+
return Promise.resolve(result);
|
|
24
27
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* Si no se implementan, se lanza un error por defecto
|
|
28
|
-
*/
|
|
29
|
-
async GET(_req, _res, _beforeParams) {
|
|
30
|
-
throw { message: `Método GET no implementado para ${this.path}` };
|
|
28
|
+
async GET(_req, _res, _before) {
|
|
29
|
+
throw { message: `GET no implementado para ${this.path}` };
|
|
31
30
|
}
|
|
32
|
-
async POST(_req, _res,
|
|
33
|
-
throw { message: `
|
|
31
|
+
async POST(_req, _res, _before) {
|
|
32
|
+
throw { message: `POST no implementado para ${this.path}` };
|
|
34
33
|
}
|
|
35
|
-
async PUT(_req, _res,
|
|
36
|
-
throw { message: `
|
|
34
|
+
async PUT(_req, _res, _before) {
|
|
35
|
+
throw { message: `PUT no implementado para ${this.path}` };
|
|
37
36
|
}
|
|
38
|
-
async DELETE(_req, _res,
|
|
39
|
-
throw { message: `
|
|
37
|
+
async DELETE(_req, _res, _before) {
|
|
38
|
+
throw { message: `DELETE no implementado para ${this.path}` };
|
|
40
39
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Ejecuta la lógica específica del endpoint
|
|
43
|
-
*/
|
|
44
40
|
execute(req, res) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
throw { message: `Método ${method} no soportado` };
|
|
50
|
-
}
|
|
51
|
-
this.BEFORE(req)
|
|
52
|
-
.then((beforeParams) => handler(req, res, beforeParams))
|
|
53
|
-
.then(result => this.AFTER(result))
|
|
54
|
-
.then(result => res.json({
|
|
55
|
-
success: true,
|
|
56
|
-
...result
|
|
57
|
-
}));
|
|
41
|
+
const handler = this.methodMap[req.method];
|
|
42
|
+
if (!handler) {
|
|
43
|
+
res.status(405).json({ success: false, message: `Método ${req.method} no soportado` });
|
|
44
|
+
return;
|
|
58
45
|
}
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
this.VALIDATE(req)
|
|
47
|
+
.then(() => this.BEFORE(req))
|
|
48
|
+
.then(before => handler(req, res, before))
|
|
49
|
+
.then(result => this.AFTER(req, Formatter_1.Formatter.sanitizeAndParse(result)))
|
|
50
|
+
.then(result => res.json({ success: true, ...result }))
|
|
51
|
+
.catch((error) => {
|
|
52
|
+
res.status(error.status || 500).json({
|
|
61
53
|
success: false,
|
|
62
|
-
message: error.message
|
|
54
|
+
message: error.message || 'Error interno del servidor',
|
|
63
55
|
});
|
|
64
|
-
}
|
|
56
|
+
});
|
|
65
57
|
}
|
|
66
58
|
}
|
|
67
59
|
exports.Endpoint = Endpoint;
|
package/dist/http/Router.js
CHANGED
|
@@ -8,11 +8,9 @@ const express_1 = __importDefault(require("express"));
|
|
|
8
8
|
class Router {
|
|
9
9
|
router = express_1.default.Router();
|
|
10
10
|
constructor(endpoints) {
|
|
11
|
-
// Registrar todos los endpoints
|
|
12
|
-
console.log(`Endpoints registered:`);
|
|
13
11
|
endpoints.forEach(endpoint => {
|
|
14
12
|
console.log(` - ${endpoint.path}`);
|
|
15
|
-
this.router.all(
|
|
13
|
+
this.router.all(endpoint.path, endpoint.execute.bind(endpoint));
|
|
16
14
|
});
|
|
17
15
|
}
|
|
18
16
|
getRoutes() {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -23,4 +23,6 @@ __exportStar(require("./types/api"), exports);
|
|
|
23
23
|
__exportStar(require("./types/app"), exports);
|
|
24
24
|
__exportStar(require("./types/odata"), exports);
|
|
25
25
|
__exportStar(require("./utils/Formatter"), exports);
|
|
26
|
+
__exportStar(require("./auth/ApiKeys"), exports);
|
|
27
|
+
__exportStar(require("./auth/ApiKeysEndpoint"), exports);
|
|
26
28
|
__exportStar(require("./app"), exports);
|
package/dist/odata/Entities.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Endpoint, type ApiResponse, type
|
|
1
|
+
import { Endpoint, type ApiResponse, type AppModels } from "../index";
|
|
2
2
|
import type { Request, Response } from 'express';
|
|
3
|
-
export declare class Entities extends Endpoint<BeforeResult> {
|
|
4
|
-
protected
|
|
5
|
-
protected
|
|
6
|
-
protected
|
|
7
|
-
protected
|
|
8
|
-
protected
|
|
3
|
+
export declare class Entities<BeforeResult = any> extends Endpoint<BeforeResult> {
|
|
4
|
+
protected entity: AppModels[keyof AppModels];
|
|
5
|
+
protected VALIDATE({ params: { endpoint }, path }: Request): Promise<void>;
|
|
6
|
+
protected GET(req: Request, _res: Response): Promise<ApiResponse>;
|
|
7
|
+
protected POST({ body }: Request, _res: Response): Promise<ApiResponse>;
|
|
8
|
+
protected PUT({ body, params: { id } }: Request, _res: Response): Promise<ApiResponse>;
|
|
9
|
+
protected DELETE(req: Request, _res: Response): Promise<ApiResponse>;
|
|
9
10
|
}
|
package/dist/odata/Entities.js
CHANGED
|
@@ -3,47 +3,49 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Entities = void 0;
|
|
4
4
|
const index_1 = require("../index");
|
|
5
5
|
class Entities extends index_1.Endpoint {
|
|
6
|
-
|
|
6
|
+
entity;
|
|
7
|
+
VALIDATE({ params: { endpoint }, path }) {
|
|
7
8
|
// Valida que la entidad exista
|
|
8
9
|
const entity = this.prismaClient.getModel(endpoint);
|
|
9
10
|
if (!entity) {
|
|
10
11
|
throw { message: `Endpoint ${path} no encontrado` };
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
this.entity = entity;
|
|
14
|
+
return Promise.resolve();
|
|
13
15
|
}
|
|
14
|
-
async GET(req, _res
|
|
16
|
+
async GET(req, _res) {
|
|
15
17
|
const query = req.query;
|
|
16
18
|
let result;
|
|
17
19
|
// Si se proporciona un ID, se busca un registro único
|
|
18
|
-
if (id) {
|
|
19
|
-
query.id = id.toString();
|
|
20
|
-
result = await entity.findUnique(index_1.QueryBuilder.build(query));
|
|
20
|
+
if (req.params.id) {
|
|
21
|
+
query.id = req.params.id.toString();
|
|
22
|
+
result = await this.entity.findUnique(index_1.QueryBuilder.build(query));
|
|
21
23
|
}
|
|
22
24
|
else {
|
|
23
|
-
result = await entity.findMany(index_1.QueryBuilder.build(query));
|
|
25
|
+
result = await this.entity.findMany(index_1.QueryBuilder.build(query));
|
|
24
26
|
}
|
|
25
27
|
return { result };
|
|
26
28
|
}
|
|
27
|
-
async POST({ body }, _res
|
|
28
|
-
const result = await entity.create({ data: body });
|
|
29
|
+
async POST({ body }, _res) {
|
|
30
|
+
const result = await this.entity.create({ data: body });
|
|
29
31
|
return { result, message: `Registro creado correctamente` };
|
|
30
32
|
}
|
|
31
|
-
async PUT({ body
|
|
33
|
+
async PUT({ body, params: { id } }, _res) {
|
|
32
34
|
if (!id) {
|
|
33
35
|
throw { message: "ID es requerido para actualizar" };
|
|
34
36
|
}
|
|
35
|
-
const result = await entity.update({
|
|
36
|
-
where: { id },
|
|
37
|
+
const result = await this.entity.update({
|
|
38
|
+
where: { id: Number(id) },
|
|
37
39
|
data: body
|
|
38
40
|
});
|
|
39
41
|
return { result, message: `Registro actualizado correctamente` };
|
|
40
42
|
}
|
|
41
|
-
async DELETE(
|
|
43
|
+
async DELETE(req, _res) {
|
|
42
44
|
let result;
|
|
43
|
-
if (!id) {
|
|
45
|
+
if (!req.params.id) {
|
|
44
46
|
throw { message: "ID es requerido para eliminar" };
|
|
45
47
|
}
|
|
46
|
-
result = await entity.delete({ where: { id } });
|
|
48
|
+
result = await this.entity.delete({ where: { id: Number(req.params.id) } });
|
|
47
49
|
return { result, message: `Registro eliminado correctamente` };
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -1,33 +1,10 @@
|
|
|
1
|
-
import type { QueryParams } from "../
|
|
1
|
+
import type { QueryParams } from "../types/odata";
|
|
2
2
|
export declare class QueryBuilder {
|
|
3
|
-
private static readonly OPERATORS;
|
|
4
|
-
private static parseValue;
|
|
5
|
-
/**
|
|
6
|
-
* Construye el objeto expand
|
|
7
|
-
*/
|
|
8
|
-
private static buildExpand;
|
|
9
|
-
/**
|
|
10
|
-
* Parsea una expresión de filtro individual
|
|
11
|
-
*/
|
|
12
|
-
private static parseExpression;
|
|
13
|
-
/**
|
|
14
|
-
* Construye el objeto filter
|
|
15
|
-
*/
|
|
16
|
-
private static buildFilter;
|
|
17
|
-
/**
|
|
18
|
-
* Construye un objeto de ordenamiento
|
|
19
|
-
* Soporta propiedades encadenadas como "subscription.description"
|
|
20
|
-
*/
|
|
21
|
-
private static buildOrderBy;
|
|
22
|
-
/**
|
|
23
|
-
* Construye el objeto query
|
|
24
|
-
*/
|
|
25
3
|
static build(query: QueryParams): {
|
|
26
|
-
where:
|
|
27
|
-
[x: string]: any;
|
|
28
|
-
};
|
|
4
|
+
where: Record<string, any>;
|
|
29
5
|
include: Record<string, boolean> | undefined;
|
|
30
|
-
take: number;
|
|
31
|
-
|
|
6
|
+
take: number | undefined;
|
|
7
|
+
skip: number | undefined;
|
|
8
|
+
orderBy: Record<string, string> | undefined;
|
|
32
9
|
};
|
|
33
10
|
}
|
|
@@ -1,108 +1,59 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QueryBuilder = void 0;
|
|
4
|
+
const OPERATORS = {
|
|
5
|
+
eq: (f, v) => ({ [f]: v }),
|
|
6
|
+
ne: (f, v) => ({ [f]: { not: v } }),
|
|
7
|
+
ge: (f, v) => ({ [f]: { gte: v } }),
|
|
8
|
+
le: (f, v) => ({ [f]: { lte: v } }),
|
|
9
|
+
contains: (f, v) => ({ [f]: { contains: v, mode: 'insensitive' } }),
|
|
10
|
+
startswith: (f, v) => ({ [f]: { startsWith: v, mode: 'insensitive' } }),
|
|
11
|
+
endswith: (f, v) => ({ [f]: { endsWith: v, mode: 'insensitive' } }),
|
|
12
|
+
};
|
|
13
|
+
function parseValue(value) {
|
|
14
|
+
if (!isNaN(Number(value)))
|
|
15
|
+
return Number(value);
|
|
16
|
+
if (value === 'true')
|
|
17
|
+
return true;
|
|
18
|
+
if (value === 'false')
|
|
19
|
+
return false;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function parseExpression(expr) {
|
|
23
|
+
const [field, op, ...rest] = expr.trim().split(' ');
|
|
24
|
+
if (!OPERATORS[op])
|
|
25
|
+
throw { message: `Operador '${op}' no soportado` };
|
|
26
|
+
return OPERATORS[op](field, parseValue(rest.join(' ')));
|
|
27
|
+
}
|
|
28
|
+
function buildFilter(filter) {
|
|
29
|
+
if (!filter)
|
|
30
|
+
return {};
|
|
31
|
+
if (filter.includes(' or '))
|
|
32
|
+
return { OR: filter.split(' or ').map(parseExpression) };
|
|
33
|
+
if (filter.includes(' and '))
|
|
34
|
+
return { AND: filter.split(' and ').map(parseExpression) };
|
|
35
|
+
return parseExpression(filter);
|
|
36
|
+
}
|
|
37
|
+
function buildExpand(expand) {
|
|
38
|
+
return expand?.split(',').reduce((acc, key) => ({ ...acc, [key]: true }), {});
|
|
39
|
+
}
|
|
40
|
+
function buildOrderBy(orderby) {
|
|
41
|
+
if (!orderby)
|
|
42
|
+
return undefined;
|
|
43
|
+
const [field, direction] = orderby.split(' ');
|
|
44
|
+
if (direction !== 'asc' && direction !== 'desc')
|
|
45
|
+
return undefined;
|
|
46
|
+
// Soporta campos anidados: "subscription.price desc" → { subscription: { price: 'desc' } }
|
|
47
|
+
return field.split('.').reduceRight((acc, part) => ({ [part]: acc }), direction);
|
|
48
|
+
}
|
|
4
49
|
class QueryBuilder {
|
|
5
|
-
static OPERATORS = {
|
|
6
|
-
eq: (f, v) => ({ [f]: v }),
|
|
7
|
-
ne: (f, v) => ({ [f]: { not: v } }),
|
|
8
|
-
ge: (f, v) => ({ [f]: { gte: v } }),
|
|
9
|
-
le: (f, v) => ({ [f]: { lte: v } }),
|
|
10
|
-
contains: (f, v) => ({
|
|
11
|
-
[f]: { contains: v, mode: 'insensitive' }
|
|
12
|
-
}),
|
|
13
|
-
startswith: (f, v) => ({
|
|
14
|
-
[f]: { startsWith: v, mode: 'insensitive' }
|
|
15
|
-
}),
|
|
16
|
-
endswith: (f, v) => ({
|
|
17
|
-
[f]: { endsWith: v, mode: 'insensitive' }
|
|
18
|
-
}),
|
|
19
|
-
};
|
|
20
|
-
static parseValue(value) {
|
|
21
|
-
if (value === undefined)
|
|
22
|
-
return value;
|
|
23
|
-
if (!isNaN(Number(value)))
|
|
24
|
-
return Number(value);
|
|
25
|
-
if (value === 'true')
|
|
26
|
-
return true;
|
|
27
|
-
if (value === 'false')
|
|
28
|
-
return false;
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Construye el objeto expand
|
|
33
|
-
*/
|
|
34
|
-
static buildExpand(expand) {
|
|
35
|
-
return expand?.split(',').reduce((acc, path) => {
|
|
36
|
-
acc[path] = true;
|
|
37
|
-
return acc;
|
|
38
|
-
}, {});
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Parsea una expresión de filtro individual
|
|
42
|
-
*/
|
|
43
|
-
static parseExpression(expr) {
|
|
44
|
-
const [field, op, ...rest] = expr.trim().split(' ');
|
|
45
|
-
if (!this.OPERATORS[op])
|
|
46
|
-
throw { message: `Operador '${op}' no soportado` };
|
|
47
|
-
const value = this.parseValue(rest.join(' '));
|
|
48
|
-
return this.OPERATORS[op](field, value);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Construye el objeto filter
|
|
52
|
-
*/
|
|
53
|
-
static buildFilter(filter) {
|
|
54
|
-
if (!filter)
|
|
55
|
-
return {};
|
|
56
|
-
if (filter.includes(' or ')) {
|
|
57
|
-
return {
|
|
58
|
-
OR: filter.split(' or ').map(this.parseExpression.bind(this))
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
if (filter.includes(' and ')) {
|
|
62
|
-
return {
|
|
63
|
-
AND: filter.split(' and ').map(this.parseExpression.bind(this))
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return this.parseExpression.bind(this)(filter);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Construye un objeto de ordenamiento
|
|
70
|
-
* Soporta propiedades encadenadas como "subscription.description"
|
|
71
|
-
*/
|
|
72
|
-
static buildOrderBy(orderby) {
|
|
73
|
-
if (!orderby)
|
|
74
|
-
return undefined;
|
|
75
|
-
// Obtiene el campo y la dirección
|
|
76
|
-
const [field, direction] = orderby.split(' ');
|
|
77
|
-
// Solo acepta asc o desc
|
|
78
|
-
if (direction !== 'asc' && direction !== 'desc')
|
|
79
|
-
return undefined;
|
|
80
|
-
// Construye el objeto de ordenamiento
|
|
81
|
-
const parts = field.split(".");
|
|
82
|
-
let result = {};
|
|
83
|
-
let current = result;
|
|
84
|
-
for (let i = 0; i < parts.length; i++) {
|
|
85
|
-
if (i === parts.length - 1) {
|
|
86
|
-
current[parts[i]] = direction;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
current[parts[i]] = {};
|
|
90
|
-
current = current[parts[i]];
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Construye el objeto query
|
|
97
|
-
*/
|
|
98
50
|
static build(query) {
|
|
99
51
|
return {
|
|
100
|
-
where:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
orderBy: this.buildOrderBy(query.orderby),
|
|
52
|
+
where: buildFilter(query.filter),
|
|
53
|
+
include: buildExpand(query.expand),
|
|
54
|
+
take: query.top ? Number(query.top) : undefined,
|
|
55
|
+
skip: query.skip ? Number(query.skip) : undefined,
|
|
56
|
+
orderBy: buildOrderBy(query.orderby),
|
|
106
57
|
};
|
|
107
58
|
}
|
|
108
59
|
}
|
package/dist/types/app.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Endpoint } from "../
|
|
1
|
+
import type { Endpoint } from "../index";
|
|
2
2
|
import type { ErrorRequestHandler, RequestHandler } from "express";
|
|
3
3
|
import type { Options } from "express-rate-limit";
|
|
4
4
|
export type NanoAppConfig = {
|
|
@@ -14,4 +14,16 @@ export type NanoAppConfig = {
|
|
|
14
14
|
tokens?: Record<string, any>;
|
|
15
15
|
format?: string;
|
|
16
16
|
};
|
|
17
|
+
apiKeys?: {
|
|
18
|
+
/** Habilita los endpoints y middleware de API keys. Por defecto: false */
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
/** Ruta base para los endpoints de gestión. Por defecto: /api-keys */
|
|
21
|
+
path?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Middleware de autenticación custom (ej: JWT de usuario).
|
|
24
|
+
* Si se provee, se ejecuta primero. Si falla, se intenta validar con API key.
|
|
25
|
+
* Si ambos fallan, retorna 401.
|
|
26
|
+
*/
|
|
27
|
+
middleware?: RequestHandler;
|
|
28
|
+
};
|
|
17
29
|
};
|
package/dist/types/odata.d.ts
CHANGED
package/dist/utils/Formatter.js
CHANGED
|
@@ -15,5 +15,17 @@ class Formatter {
|
|
|
15
15
|
second: "2-digit",
|
|
16
16
|
}).format(date);
|
|
17
17
|
}
|
|
18
|
+
static sanitizeAndParse(result) {
|
|
19
|
+
const jsonString = JSON.stringify(result);
|
|
20
|
+
// La expresión regular busca exactamente la clave "password": "cualquier_valor"
|
|
21
|
+
// donde:
|
|
22
|
+
// - "password" se encuentra entre comillas.
|
|
23
|
+
// - Puede haber espacios opcionales alrededor de los dos puntos.
|
|
24
|
+
// - El valor se asume estar entre comillas.
|
|
25
|
+
const regex = /("password"\s*:\s*)"[^"]*"/gi;
|
|
26
|
+
// Reemplazamos el valor por el literal JSON null (sin comillas)
|
|
27
|
+
const sanitizedString = jsonString.replace(regex, '$1null');
|
|
28
|
+
return JSON.parse(sanitizedString);
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
exports.Formatter = Formatter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano-app/backend",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Backend for nano-app architecture",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,20 +22,24 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@prisma/adapter-pg": "^7.4.0",
|
|
24
24
|
"@prisma/client": "^7.4.0",
|
|
25
|
+
"bcrypt": "^6.0.0",
|
|
25
26
|
"compression": "^1.8.1",
|
|
26
27
|
"cors": "^2.8.5",
|
|
27
28
|
"dotenv": "^17.2.3",
|
|
28
29
|
"express": "^5.2.1",
|
|
29
30
|
"express-rate-limit": "^8.2.1",
|
|
30
31
|
"helmet": "^8.1.0",
|
|
31
|
-
"morgan": "^1.10.1"
|
|
32
|
+
"morgan": "^1.10.1",
|
|
33
|
+
"pg": "^8.20.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
36
|
+
"@types/bcrypt": "^6.0.0",
|
|
34
37
|
"@types/compression": "^1.8.1",
|
|
35
38
|
"@types/cors": "^2.8.19",
|
|
36
39
|
"@types/express": "^5.0.0",
|
|
37
40
|
"@types/morgan": "^1.9.10",
|
|
38
41
|
"@types/node": "^20.0.0",
|
|
42
|
+
"@types/pg": "^8.18.0",
|
|
39
43
|
"tsx": "^4.7.0",
|
|
40
44
|
"typescript": "^5.3.0"
|
|
41
45
|
}
|