@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 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, // 15 minutos
23
- max: 1000, // Máximo 1000 peticiones por IP
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
- // Inicializar la configuración
36
- this.config = {
37
- ...this.config,
38
- ...config,
39
- };
40
- // Inicializar el router
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
- // Inicializar morgan
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
- // Middlewares
65
- (this.config.middlewares || []).forEach(middleware => this.app.use(middleware));
66
- // Registrar las rutas
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
- else {
73
- this.app.use((err, _, res, __) => {
74
- res.status(err.status || 500).json({
75
- success: false,
76
- message: err.message || 'Error interno del servidor'
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?.(); // Ejecutar el listener si se proporciona
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,5 @@
1
+ import { Endpoint, type ApiResponse } from "../index";
2
+ import type { Request, Response } from "express";
3
+ export declare class TokenEndpoint extends Endpoint {
4
+ protected POST(req: Request, res: Response): Promise<ApiResponse>;
5
+ }
@@ -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;
@@ -1,23 +1,17 @@
1
1
  import { Request, Response } from "express";
2
- import { PrismaClient, type ApiResponse } from "../index";
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
- /** Metodos envoltorios */
9
+ protected VALIDATE(_req: Request): Promise<void>;
9
10
  protected BEFORE(_req: Request): Promise<BeforeResult>;
10
- protected AFTER(_result: ApiResponse): Promise<ApiResponse>;
11
- /**
12
- * Métodos HTTP opcionales - las clases hijas solo implementan los que necesiten
13
- * Si no se implementan, se lanza un error por defecto
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
  }
@@ -1,67 +1,59 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Endpoint = void 0;
4
- const index_1 = require("../index");
4
+ const Client_1 = require("../prisma/Client");
5
+ const Formatter_1 = require("../utils/Formatter");
5
6
  class Endpoint {
6
- prismaClient = new index_1.PrismaClient();
7
- methodMap = {};
7
+ prismaClient = new Client_1.PrismaClient();
8
8
  path = '';
9
+ methodMap;
9
10
  constructor(path) {
10
- this.path = path || 'default';
11
+ this.path = path ?? 'default';
11
12
  this.methodMap = {
12
- 'GET': this.GET.bind(this),
13
- 'POST': this.POST.bind(this),
14
- 'PUT': this.PUT.bind(this),
15
- 'DELETE': this.DELETE.bind(this),
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
- /** Metodos envoltorios */
19
+ VALIDATE(_req) {
20
+ return Promise.resolve();
21
+ }
19
22
  BEFORE(_req) {
20
23
  return Promise.resolve({});
21
24
  }
22
- AFTER(_result) {
23
- return Promise.resolve(_result);
25
+ AFTER(_req, result) {
26
+ return Promise.resolve(result);
24
27
  }
25
- /**
26
- * Métodos HTTP opcionales - las clases hijas solo implementan los que necesiten
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, _beforeParams) {
33
- throw { message: `Método POST no implementado para ${this.path}` };
31
+ async POST(_req, _res, _before) {
32
+ throw { message: `POST no implementado para ${this.path}` };
34
33
  }
35
- async PUT(_req, _res, _beforeParams) {
36
- throw { message: `Método PUT no implementado para ${this.path}` };
34
+ async PUT(_req, _res, _before) {
35
+ throw { message: `PUT no implementado para ${this.path}` };
37
36
  }
38
- async DELETE(_req, _res, _beforeParams) {
39
- throw { message: `Método DELETE no implementado para ${this.path}` };
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
- try {
46
- const method = req.method;
47
- const handler = this.methodMap[method];
48
- if (!handler) {
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
- catch (error) {
60
- res.status(500).json({
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 ? error.message : error
54
+ message: error.message || 'Error interno del servidor',
63
55
  });
64
- }
56
+ });
65
57
  }
66
58
  }
67
59
  exports.Endpoint = Endpoint;
@@ -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(`${endpoint.path}`, endpoint.execute.bind(endpoint));
13
+ this.router.all(endpoint.path, endpoint.execute.bind(endpoint));
16
14
  });
17
15
  }
18
16
  getRoutes() {
package/dist/index.d.ts CHANGED
@@ -7,4 +7,6 @@ export * from "./types/api";
7
7
  export * from "./types/app";
8
8
  export * from "./types/odata";
9
9
  export * from "./utils/Formatter";
10
+ export * from "./auth/ApiKeys";
11
+ export * from "./auth/ApiKeysEndpoint";
10
12
  export * from "./app";
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);
@@ -1,9 +1,10 @@
1
- import { Endpoint, type ApiResponse, type BeforeResult } from "../index";
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 BEFORE({ params: { endpoint, id }, path }: Request): Promise<BeforeResult>;
5
- protected GET(req: Request, _res: Response, { entity, id }: BeforeResult): Promise<ApiResponse>;
6
- protected POST({ body }: Request, _res: Response, { entity }: BeforeResult): Promise<ApiResponse>;
7
- protected PUT({ body }: Request, _res: Response, { entity, id }: BeforeResult): Promise<ApiResponse>;
8
- protected DELETE(_req: Request, _res: Response, { entity, id }: BeforeResult): Promise<ApiResponse>;
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
  }
@@ -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
- BEFORE({ params: { endpoint, id }, path }) {
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
- return Promise.resolve({ entity, id: Number(id) });
13
+ this.entity = entity;
14
+ return Promise.resolve();
13
15
  }
14
- async GET(req, _res, { entity, id }) {
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, { entity }) {
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 }, _res, { entity, id }) {
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(_req, _res, { entity, id }) {
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 "../index";
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
- orderBy: any;
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
- ...this.buildFilter(query.filter)
102
- },
103
- include: this.buildExpand(query.expand),
104
- take: Number(query.top || 10) + Number(query.skip || 0),
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
  }
@@ -1,4 +1,4 @@
1
- import type { Endpoint } from "../http/Endpoint";
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
  };
@@ -6,7 +6,3 @@ export type QueryParams = {
6
6
  skip?: string;
7
7
  orderby?: string;
8
8
  };
9
- export type BeforeResult = {
10
- entity: any;
11
- id: number;
12
- };
@@ -1,4 +1,6 @@
1
+ import type { ApiResponse } from "../index";
1
2
  export declare class Formatter {
2
3
  static price(value: number): string;
3
4
  static logDate(date?: Date): string;
5
+ static sanitizeAndParse(result: ApiResponse): ApiResponse;
4
6
  }
@@ -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.0",
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
  }