@miguelmorales13/nestkit 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -6
- package/dist/auth/index.cjs +12 -70
- package/dist/auth/index.js +11 -69
- package/dist/auth/oauth/http.d.ts +38 -0
- package/dist/auth/oauth/index.cjs +610 -0
- package/dist/auth/oauth/index.d.ts +12 -0
- package/dist/auth/oauth/index.js +610 -0
- package/dist/auth/oauth/oauth-auth.module.d.ts +28 -0
- package/dist/auth/oauth/oauth-auth.service.d.ts +101 -0
- package/dist/auth/oauth/oauth-callback.filter.d.ts +20 -0
- package/dist/auth/oauth/oauth-provider.d.ts +73 -0
- package/dist/auth/oauth/oauth.controller.d.ts +38 -0
- package/dist/auth/oauth/oauth.options.d.ts +89 -0
- package/dist/auth/oauth/oauth.ports.d.ts +86 -0
- package/dist/bootstrap/index.cjs +3 -3
- package/dist/bootstrap/index.js +2 -2
- package/dist/chunk-A3B2EY4V.js +75 -0
- package/dist/chunk-NVCI3CQI.cjs +75 -0
- package/dist/database/typeorm/index.cjs +79 -0
- package/dist/database/typeorm/index.d.ts +3 -0
- package/dist/database/typeorm/index.js +79 -0
- package/dist/database/typeorm/tenant-scope.d.ts +18 -0
- package/dist/database/typeorm/typeorm.module.d.ts +35 -0
- package/dist/index.cjs +17 -17
- package/dist/index.js +24 -24
- package/dist/umami/index.cjs +109 -0
- package/dist/umami/index.d.ts +4 -0
- package/dist/umami/index.js +109 -0
- package/dist/umami/umami.module.d.ts +6 -0
- package/dist/umami/umami.options.d.ts +20 -0
- package/dist/umami/umami.service.d.ts +40 -0
- package/dist/umami/umami.types.d.ts +24 -0
- package/package.json +22 -2
- package/dist/{chunk-AOCF5QCZ.js → chunk-ANQ3YPDI.js} +3 -3
- package/dist/{chunk-54ZXIB5T.cjs → chunk-YARLPYG5.cjs} +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
# @miguelmorales13/nestkit
|
|
2
2
|
|
|
3
|
-
Bloques de infraestructura reutilizables para servicios NestJS
|
|
4
|
-
respuestas estandarizadas, tracking de requests, entidades base, CRUD genérico, y conexión a
|
|
5
|
-
Postgres/Supabase (con el contrato listo para Mongo a futuro). Pensado para eliminar el boilerplate
|
|
3
|
+
Bloques de infraestructura reutilizables para servicios NestJS. Pensado para eliminar el boilerplate
|
|
6
4
|
que se repite al arrancar cada backend nuevo.
|
|
7
5
|
|
|
6
|
+
**El esqueleto**: entidades base, `Repository<T>` y CRUD genérico, respuestas estandarizadas, manejo
|
|
7
|
+
de errores, tracking de requests, i18n, y un `applyNestKitDefaults` que lo cablea todo en una línea.
|
|
8
|
+
|
|
9
|
+
**Las conexiones**: Postgres (con aislamiento por tenant vía RLS) y Supabase — el contrato de Mongo
|
|
10
|
+
está listo, la implementación no.
|
|
11
|
+
|
|
12
|
+
**Las integraciones**, cada una en su subpath y sin arrastrar dependencias a quien no la use: acceso
|
|
13
|
+
con Google o Facebook (`auth/oauth`) o con email y contraseña (`auth`), Stripe, Telegram, WhatsApp,
|
|
14
|
+
Instagram y Facebook Pages (`meta`), subida de archivos (`storage`), email (Resend o Nodemailer) y
|
|
15
|
+
lectura de analítica (`umami`).
|
|
16
|
+
|
|
8
17
|
## ¿Para qué proyectos es esto?
|
|
9
18
|
|
|
10
19
|
**Sí**: cualquier proyecto NestJS nuevo que arranque con el patrón hexagonal/ports-and-adapters
|
|
@@ -31,16 +40,30 @@ tu proyecto realmente use:
|
|
|
31
40
|
|---------------------|------------------|
|
|
32
41
|
| `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, `rxjs` | `pg` → `./database/postgres` |
|
|
33
42
|
| | `@supabase/supabase-js` → `./database/supabase` |
|
|
34
|
-
| | `helmet`, `@nestjs/swagger` → `./bootstrap`
|
|
43
|
+
| | `helmet`, `@nestjs/swagger` → `./bootstrap` |
|
|
44
|
+
| | `telegraf` → `./telegram` |
|
|
45
|
+
| | `stripe` → `./stripe` |
|
|
46
|
+
| | `resend` → `./email/resend` |
|
|
47
|
+
| | `nodemailer` → `./email/nodemailer` |
|
|
48
|
+
| | `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, `@aws-sdk/s3-request-presigner` → `./storage` (solo el adaptador S3) |
|
|
35
49
|
|
|
36
50
|
```bash
|
|
37
51
|
bun add @nestjs/common @nestjs/core reflect-metadata rxjs
|
|
38
52
|
bun add pg # si usas ./database/postgres
|
|
39
53
|
bun add @supabase/supabase-js # si usas ./database/supabase
|
|
40
54
|
bun add helmet @nestjs/swagger # si usas ./bootstrap
|
|
55
|
+
bun add telegraf # si usas ./telegram
|
|
56
|
+
bun add stripe # si usas ./stripe
|
|
41
57
|
bun add class-validator class-transformer # requeridos en runtime por ValidationPipe
|
|
42
58
|
```
|
|
43
59
|
|
|
60
|
+
`meta`, `media`, `umami` y `auth/oauth` no piden nada extra: hablan con sus APIs por el `fetch` del
|
|
61
|
+
runtime. `auth/oauth` sí necesita cookies en el adaptador HTTP de tu app —`@fastify/cookie` o
|
|
62
|
+
`cookie-parser`—, que es tuyo, no del paquete.
|
|
63
|
+
|
|
64
|
+
`bcryptjs` y `jsonwebtoken` (los usan `auth` y `auth/oauth`) van como dependencias normales, ocultas:
|
|
65
|
+
no los instalas ni los importas vos.
|
|
66
|
+
|
|
44
67
|
## Quickstart (`main.ts`)
|
|
45
68
|
|
|
46
69
|
```ts
|
|
@@ -209,6 +232,31 @@ hace que Row-Level-Security de Postgres vea el usuario correcto: abre una transa
|
|
|
209
232
|
commit/rollback — **cualquier query que dependa de una policy RLS basada en `auth.uid()` debe pasar
|
|
210
233
|
por aquí**, `pool.query()` directo no aplica el `set_config` y verá (o no verá) filas incorrectamente.
|
|
211
234
|
|
|
235
|
+
### `database/typeorm` — DataSource + RLS por tenant, para el que quiere ORM
|
|
236
|
+
|
|
237
|
+
Mismo modelo que `database/postgres`, pero con **TypeORM** para quien prefiere entidades,
|
|
238
|
+
repositorios y migraciones del ORM. `TypeOrmDbModule.forRoot({ entities, migrations })` provee un
|
|
239
|
+
`DataSource` inicializado bajo el token `DATA_SOURCE` (`type: 'postgres'` fijo, `synchronize`
|
|
240
|
+
siempre apagado — los cambios de esquema van por migraciones, jamás por auto-sync). Funciona con
|
|
241
|
+
cualquier Postgres, incluido **Neon**.
|
|
242
|
+
|
|
243
|
+
Lo importante para multi-tenant: el pool del ORM no tiene sesión por-request, así que la RLS no
|
|
244
|
+
vería el tenant. Por eso este subpath trae su propio **`withTenantScope(dataSource, session, fn)`**,
|
|
245
|
+
que abre un QueryRunner, corre `set_config(name, value, true)` por cada variable de sesión, y te pasa
|
|
246
|
+
el `EntityManager` **atado a esa conexión y transacción** — dentro, `manager.getRepository(X).find()`
|
|
247
|
+
corre con la RLS aplicada; fuera, no. Es el equivalente en TypeORM del `withTenantScope` de `pg`, y
|
|
248
|
+
está probado: con una policy sobre un GUC, cada tenant ve solo lo suyo y sin scope no ve nada.
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
await withTenantScope(dataSource, {
|
|
252
|
+
'request.jwt.claim.sub': userId,
|
|
253
|
+
'request.jwt.claim.tenant': tenantId,
|
|
254
|
+
}, (manager) => manager.getRepository(Order).find());
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Peer dep opcional: `typeorm` (solo si se usa este módulo). Elegí ESTE o `database/postgres`, no los
|
|
258
|
+
dos a la vez — son dos formas de lo mismo.
|
|
259
|
+
|
|
212
260
|
### `database/supabase` — clientes anon y service-role
|
|
213
261
|
|
|
214
262
|
```ts
|
|
@@ -449,6 +497,44 @@ razonable para la Cloud API, que de todos modos es un REST plano, así que `What
|
|
|
449
497
|
`fetch` global de Node (18+) directo. Solo manda mensajes de texto (`sendTextMessage`) — sin
|
|
450
498
|
templates, sin media, sin manejo de mensajes entrantes.
|
|
451
499
|
|
|
500
|
+
### `meta` — publicar en Instagram y Facebook Pages
|
|
501
|
+
|
|
502
|
+
Cliente de salida sobre la Graph API, solo para publicar: no lee insights, ni maneja webhooks, ni
|
|
503
|
+
comentarios. Mismo alcance que `telegram`.
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
import { MetaModule } from '@miguelmorales13/nestkit/meta';
|
|
507
|
+
|
|
508
|
+
@Module({ imports: [MetaModule.forRoot()] }) // lee META_ACCESS_TOKEN, META_PAGE_ID, META_IG_USER_ID
|
|
509
|
+
export class AppModule {}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
await meta.publishToInstagram(url, 'Texto del pie', 'feed'); // 'feed' | 'stories' | 'reels'
|
|
514
|
+
await meta.publishToPage(url, 'Texto');
|
|
515
|
+
await meta.publishStoryToPage(url);
|
|
516
|
+
await meta.describe(); // a qué cuenta apunta el token, para diagnosticar
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Dos cosas de esta API mandan sobre todo lo demás, y conviene saberlas antes:
|
|
520
|
+
|
|
521
|
+
**Las imágenes nunca se suben.** Se pasa una URL pública y Meta la descarga. Solo la necesita
|
|
522
|
+
durante la publicación —después guarda su propia copia y sirve el archivo desde su CDN—, así que
|
|
523
|
+
alcanza con un hosting efímero. Para eso está `media`, en este mismo paquete.
|
|
524
|
+
|
|
525
|
+
**Publicar en una Página exige un Page token, no un user token.** Con un token de usuario Meta
|
|
526
|
+
asume que estás posteando como persona y pide `publish_actions`, un permiso que eliminó en 2018, así
|
|
527
|
+
que el error que devuelve no apunta ni de cerca a la causa real.
|
|
528
|
+
|
|
529
|
+
Permisos: `instagram_basic` e `instagram_content_publish` para Instagram, `pages_manage_posts` para
|
|
530
|
+
Facebook. Publicar en tus propias Páginas no necesita App Review; hacerlo en nombre de terceros sí.
|
|
531
|
+
|
|
532
|
+
Instagram publica en dos pasos (crear contenedor, publicar) y el servicio espera solo a que el
|
|
533
|
+
contenedor esté listo — no hace falta encadenarlo a mano.
|
|
534
|
+
|
|
535
|
+
`forRoot()` sin argumentos toma las credenciales del entorno; pasándoselas explícitas se puede tener
|
|
536
|
+
un proceso publicando en varias cuentas.
|
|
537
|
+
|
|
452
538
|
### `stripe` — cliente de la SDK oficial
|
|
453
539
|
|
|
454
540
|
```ts
|
|
@@ -628,6 +714,81 @@ necesitás otra lógica de roles al alta (ej. el primer usuario de un tenant nue
|
|
|
628
714
|
`BaseAuthService` y sobreescribí `register()`, o llamá `users.create()` directo con los roles que
|
|
629
715
|
quieras desde tu propio controller.
|
|
630
716
|
|
|
717
|
+
### `auth/oauth` — entrar con Google o Facebook, sin Passport
|
|
718
|
+
|
|
719
|
+
Lo de arriba es email + contraseña. Esto es lo otro: alta y acceso delegados a un proveedor,
|
|
720
|
+
vinculación de cuentas, y sesiones con refresh rotativo de un solo uso guardado hasheado. Son
|
|
721
|
+
módulos separados y se pueden usar los dos, uno, o ninguno.
|
|
722
|
+
|
|
723
|
+
**No usa Passport, a propósito.** Su paso de redirección llama a `res.setHeader()`, que no existe
|
|
724
|
+
en el reply de Fastify, así que el inicio del flujo hay que armarlo a mano igual; y su mapeo de
|
|
725
|
+
perfiles es una tabla de traducción no documentada que reenvía tal cual los nombres de campo que no
|
|
726
|
+
reconoce —así es como `photos.type(large)` llega a la Graph API de Facebook pidiendo los álbumes
|
|
727
|
+
subidos por la persona, que necesitan un permiso que la app no tiene, y el callback muere con un
|
|
728
|
+
500. El flujo entero son unas cuarenta líneas de `fetch` y cada campo que se manda se ve en
|
|
729
|
+
`oauth-provider.ts`.
|
|
730
|
+
|
|
731
|
+
Un proveedor es un objeto, no una clase que heredar: agregar Apple o GitHub es declarar cuatro URLs
|
|
732
|
+
y una función que mapee su JSON a `OAuthProfile`.
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
import {
|
|
736
|
+
OAuthAuthModule, OAUTH_STORE, REFRESH_TOKEN_STORE,
|
|
737
|
+
googleProvider, facebookProvider, createOAuthController,
|
|
738
|
+
} from '@miguelmorales13/nestkit/auth/oauth';
|
|
739
|
+
|
|
740
|
+
@Module({
|
|
741
|
+
imports: [
|
|
742
|
+
MiStoreModule, // exporta la clase que implementa los puertos
|
|
743
|
+
OAuthAuthModule.forRoot({
|
|
744
|
+
imports: [MiStoreModule],
|
|
745
|
+
providers: [
|
|
746
|
+
{ provide: OAUTH_STORE, useExisting: MiStore },
|
|
747
|
+
{ provide: REFRESH_TOKEN_STORE, useExisting: MiStore },
|
|
748
|
+
],
|
|
749
|
+
options: {
|
|
750
|
+
providers: [googleProvider(), facebookProvider()],
|
|
751
|
+
webUrl: process.env.WEB_URL!,
|
|
752
|
+
cookie: { path: '/api/auth' }, // acotá la cookie a las rutas que la usan
|
|
753
|
+
mapUser: (u) => ({ id: u.id, email: u.email, name: u.name }),
|
|
754
|
+
},
|
|
755
|
+
}),
|
|
756
|
+
],
|
|
757
|
+
controllers: [class AuthController extends createOAuthController() {}],
|
|
758
|
+
})
|
|
759
|
+
export class AuthModule {}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Eso da `GET /auth/:provider`, `GET /auth/:provider/callback`, `POST /auth/refresh`,
|
|
763
|
+
`POST /auth/logout`, `GET /auth/me`, `GET /auth/linked-accounts` y `POST /auth/link-intent`. El
|
|
764
|
+
proveedor va como parámetro de ruta y no una pareja de rutas por proveedor, así que sumar Apple
|
|
765
|
+
después toca solo configuración.
|
|
766
|
+
|
|
767
|
+
**Los dos puertos van separados** (`OAuthStorePort` para identidades, `RefreshTokenStorePort` para
|
|
768
|
+
sesiones) porque responden preguntas distintas y un proyecto puede querer las sesiones en Redis. Si
|
|
769
|
+
van a la misma base, una sola clase implementa los dos y se atan con `useExisting`, como arriba.
|
|
770
|
+
|
|
771
|
+
`revokeIfActive` **tiene que ser atómico** —un UPDATE condicional, no un leer-y-escribir—: esa
|
|
772
|
+
fila es lo único que impide que un refresh token robado se use dos veces.
|
|
773
|
+
|
|
774
|
+
Personalizable sin tocar la librería: nombre y path de la cookie, TTLs, `sameSite`, a dónde vuelve
|
|
775
|
+
el navegador tras entrar / fallar / vincular, si un login cierra las demás sesiones
|
|
776
|
+
(`singleSession`, por defecto sí), qué campos del usuario salen por la API (`mapUser`), el guard de
|
|
777
|
+
las rutas con sesión (`guard`, si la app ya tiene el suyo) y todos los textos que ve el usuario.
|
|
778
|
+
|
|
779
|
+
`syncProfile` es la opción que casi siempre hay que pasar en cuanto la app deja elegir foto de
|
|
780
|
+
perfil: por defecto resincroniza nombre y foto del proveedor en cada login, lo que deshace en
|
|
781
|
+
silencio la elección de la persona la próxima vez que entre.
|
|
782
|
+
|
|
783
|
+
Sin email no hay alta. Facebook puede legítimamente no darlo —cuenta registrada con teléfono, o
|
|
784
|
+
permiso retirado en la pantalla de consentimiento—, y en ese caso se corta con un mensaje que la
|
|
785
|
+
persona puede accionar, no con un fallo interno. `OAuthCallbackFilter` se encarga de que cualquier
|
|
786
|
+
error del callback devuelva al navegador a la pantalla de acceso: sin él la persona se queda
|
|
787
|
+
mirando un JSON en una página en blanco, porque no llegó ahí por una petición de la app sino
|
|
788
|
+
redirigida desde el proveedor.
|
|
789
|
+
|
|
790
|
+
Requiere cookies en el adaptador HTTP: `@fastify/cookie` o `cookie-parser`.
|
|
791
|
+
|
|
631
792
|
### `email/resend` y `email/nodemailer` — dos transportes, elegís uno por proyecto
|
|
632
793
|
|
|
633
794
|
Mismo patrón que `database/postgres` vs `database/supabase`: dos módulos independientes, no una
|
|
@@ -724,6 +885,82 @@ Bunny y local no requieren nada extra. Variables de entorno reconocidas: `STORAG
|
|
|
724
885
|
Storage (no hay firma que oculte la contraseña de la zona); en esos casos se sube por la API, o se
|
|
725
886
|
usa Bunny Stream/TUS para video grande.
|
|
726
887
|
|
|
888
|
+
### `media` — hosting efímero para APIs que descargan por URL
|
|
889
|
+
|
|
890
|
+
No es almacenamiento. Es el complemento de `meta`: existe porque hay APIs —la de Meta es el caso
|
|
891
|
+
típico— que **rechazan el binario** y exigen bajarse el archivo ellas mismas, pero solo lo necesitan
|
|
892
|
+
durante la llamada. Sin algo así, el apaño habitual es commitear las imágenes al repositorio, que
|
|
893
|
+
las conserva para siempre por una URL que importó un minuto.
|
|
894
|
+
|
|
895
|
+
```ts
|
|
896
|
+
import { MediaModule } from '@miguelmorales13/nestkit/media';
|
|
897
|
+
|
|
898
|
+
MediaModule.forRoot({
|
|
899
|
+
publicUrl: 'https://api.tuapp.com/api', // ¡con el prefijo global!
|
|
900
|
+
uploadSecret: process.env.MEDIA_SECRET,
|
|
901
|
+
ttlMs: 60 * 60 * 1000, // por defecto una hora
|
|
902
|
+
})
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
Expone `POST /media` (devuelve `{ url }`) y `GET /media/:id`. La subida va protegida con un secreto
|
|
906
|
+
compartido en la cabecera `x-media-secret`, no con un JWT, porque quien llama es un job de CI y no
|
|
907
|
+
una persona con sesión; sin él cualquiera podría alojar contenido arbitrario en tu dominio. La
|
|
908
|
+
descarga es pública a propósito: los servicios que consumen estas URLs las piden sin credenciales.
|
|
909
|
+
|
|
910
|
+
**Vive en memoria, a propósito.** Entre guardar y publicar pasan segundos, así que un reinicio en el
|
|
911
|
+
medio solo significa reintentar, y no queda nada en disco. Si necesitás que los archivos sobrevivan
|
|
912
|
+
a un reinicio, esta es la herramienta equivocada: usá `storage`.
|
|
913
|
+
|
|
914
|
+
Dos errores que cuestan una tarde:
|
|
915
|
+
|
|
916
|
+
- `publicUrl` **tiene que incluir el prefijo global** de la app. Sin él la URL cae en lo que sirva el
|
|
917
|
+
frontend, que responde 200 con una página HTML, y el consumidor se descarga eso en vez del archivo.
|
|
918
|
+
- Fastify solo parsea JSON de fábrica. Sin registrar un parser para los tipos que vas a aceptar, la
|
|
919
|
+
subida responde 415:
|
|
920
|
+
|
|
921
|
+
```ts
|
|
922
|
+
app.getHttpAdapter().getInstance().addContentTypeParser(
|
|
923
|
+
/^image\//, { parseAs: 'buffer' }, (_req, body, done) => done(null, body),
|
|
924
|
+
)
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### `umami` — leer la API de analítica
|
|
928
|
+
|
|
929
|
+
Solo lee. Los eventos los manda el script del navegador; un backend escribiéndolos estaría
|
|
930
|
+
reportando tráfico que nunca ocurrió.
|
|
931
|
+
|
|
932
|
+
```ts
|
|
933
|
+
import { UmamiModule, UmamiService } from '@miguelmorales13/nestkit/umami';
|
|
934
|
+
|
|
935
|
+
UmamiModule.forRoot({
|
|
936
|
+
apiKey: process.env.UMAMI_API_KEY, // Umami Cloud: Settings → API Keys
|
|
937
|
+
websiteId: process.env.UMAMI_WEBSITE_ID,
|
|
938
|
+
timezone: 'America/Mexico_City',
|
|
939
|
+
// baseUrl: 'https://tu-host/api', // solo si es self-hosted
|
|
940
|
+
})
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
```ts
|
|
944
|
+
const rango = UmamiService.lastDays(7);
|
|
945
|
+
await umami.stats(rango); // visitantes, pageviews, visitas, rebotes
|
|
946
|
+
await umami.metrics('referrer', rango, 5); // top 5 de dónde llegan
|
|
947
|
+
await umami.eventCount('registro', rango); // cuántas veces disparó un evento
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
**Todos los métodos devuelven `null` en vez de lanzar** cuando la llamada falla. Un resumen suele
|
|
951
|
+
pedir varias cosas a la vez, y que un endpoint esté caído no debería llevarse el reporte entero; quien
|
|
952
|
+
llama decide qué significa un bloque ausente. Los fallos se loguean, así que uno persistente sigue
|
|
953
|
+
siendo visible.
|
|
954
|
+
|
|
955
|
+
`eventCount` distingue `0` de `null` a propósito: uno es «nadie lo hizo», el otro es «no sabemos».
|
|
956
|
+
|
|
957
|
+
`timezone` no es opcional en la práctica. Umami agrupa por día en esa zona, y dejarlo sin poner
|
|
958
|
+
reporta días UTC en silencio — que fuera de UTC significa un «día» que empieza a media tarde.
|
|
959
|
+
|
|
960
|
+
Deliberadamente no opina sobre qué significan los números: qué eventos forman un embudo, cómo
|
|
961
|
+
llamarlos y cómo redactar un reporte son decisiones de producto que cambian por proyecto. Esto solo
|
|
962
|
+
trae los datos.
|
|
963
|
+
|
|
727
964
|
## Subpaths disponibles
|
|
728
965
|
|
|
729
966
|
| Subpath | Qué trae |
|
|
@@ -735,12 +972,17 @@ usa Bunny Stream/TUS para video grande.
|
|
|
735
972
|
| `@miguelmorales13/nestkit/tracking` | `RequestContext`, `RequestIdMiddleware`, `TrackingModule` |
|
|
736
973
|
| `@miguelmorales13/nestkit/crud` | `Repository<T>`, `BaseCrudService`, `createCrudController` |
|
|
737
974
|
| `@miguelmorales13/nestkit/database/postgres` | `PgModule`, `PG_POOL`, `withTenantScope` |
|
|
975
|
+
| `@miguelmorales13/nestkit/database/typeorm` | `TypeOrmDbModule`, `DATA_SOURCE`, `withTenantScope` |
|
|
738
976
|
| `@miguelmorales13/nestkit/database/supabase` | `SupabaseModule`, `SUPABASE_ANON_CLIENT`, `SUPABASE_SERVICE_ROLE_CLIENT` |
|
|
739
977
|
| `@miguelmorales13/nestkit/database/mongo` | Solo el contrato `MongoRepositoryPort` — sin implementación |
|
|
740
978
|
| `@miguelmorales13/nestkit/telegram` | `TelegramModule`, `TELEGRAM_BOT` |
|
|
741
979
|
| `@miguelmorales13/nestkit/whatsapp` | `WhatsAppModule`, `WHATSAPP_CLIENT`, `WhatsAppClient` |
|
|
980
|
+
| `@miguelmorales13/nestkit/meta` | `MetaModule`, `MetaService` — publicar en Instagram y Facebook Pages |
|
|
981
|
+
| `@miguelmorales13/nestkit/media` | `MediaModule`, `MediaService` — hosting efímero en memoria |
|
|
742
982
|
| `@miguelmorales13/nestkit/stripe` | `StripeModule`, `STRIPE_CLIENT`, `createStripeWebhookController` |
|
|
743
983
|
| `@miguelmorales13/nestkit/auth` | `AuthUserPort`, `BaseAuthService`, `createAuthController`, `TokenService`, `JwtAuthGuard`, `RolesGuard`, `RequireTenantGuard`, `CurrentUser`, `CurrentTenant`, `Roles` |
|
|
984
|
+
| `@miguelmorales13/nestkit/auth/oauth` | `OAuthAuthModule`, `OAuthAuthService`, `createOAuthController`, `googleProvider`, `facebookProvider`, `OAuthStorePort`, `RefreshTokenStorePort` |
|
|
985
|
+
| `@miguelmorales13/nestkit/umami` | `UmamiModule`, `UmamiService` — lectura de la API de analítica |
|
|
744
986
|
| `@miguelmorales13/nestkit/storage` | `StorageModule`, `STORAGE`, `StoragePort`, adaptadores `LocalStorage`/`S3Storage`/`BunnyStorage` |
|
|
745
987
|
| `@miguelmorales13/nestkit/email/resend` | `ResendModule`, `RESEND_CLIENT` |
|
|
746
988
|
| `@miguelmorales13/nestkit/email/nodemailer` | `NodemailerModule`, `NODEMAILER_TRANSPORT` |
|
|
@@ -749,5 +991,7 @@ usa Bunny Stream/TUS para video grande.
|
|
|
749
991
|
|
|
750
992
|
## Estado del paquete
|
|
751
993
|
|
|
752
|
-
`0.
|
|
753
|
-
|
|
994
|
+
`0.7.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
|
|
995
|
+
|
|
996
|
+
`auth/oauth` y `umami` sí tienen un consumidor real: **nutrimx** corre su acceso con Google y
|
|
997
|
+
Facebook sobre este módulo en producción. El resto sigue sin ejercitarse de verdad.
|
package/dist/auth/index.cjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _chunkNVCI3CQIcjs = require('../chunk-NVCI3CQI.cjs');
|
|
6
|
+
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
|
|
@@ -19,65 +24,8 @@ function verifyPassword(plain, passwordHash) {
|
|
|
19
24
|
return _bcryptjs.compare.call(void 0, plain, passwordHash);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
// src/auth/token.service.ts
|
|
23
|
-
var _jsonwebtoken = require('jsonwebtoken'); var _jsonwebtoken2 = _interopRequireDefault(_jsonwebtoken);
|
|
24
|
-
function requireEnv(name) {
|
|
25
|
-
const value = process.env[name];
|
|
26
|
-
if (!value) {
|
|
27
|
-
throw new Error(`TokenService: ${name} environment variable is not set`);
|
|
28
|
-
}
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
|
-
var TokenService = class {
|
|
32
|
-
signAccessToken(payload) {
|
|
33
|
-
return _jsonwebtoken2.default.sign(payload, requireEnv("JWT_ACCESS_SECRET"), {
|
|
34
|
-
expiresIn: _nullishCoalesce(process.env.JWT_ACCESS_EXPIRES_IN, () => ( "15m"))
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
signRefreshToken(payload) {
|
|
38
|
-
return _jsonwebtoken2.default.sign(payload, requireEnv("JWT_REFRESH_SECRET"), {
|
|
39
|
-
expiresIn: _nullishCoalesce(process.env.JWT_REFRESH_EXPIRES_IN, () => ( "30d"))
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
verifyAccessToken(token) {
|
|
43
|
-
try {
|
|
44
|
-
return _jsonwebtoken2.default.verify(token, requireEnv("JWT_ACCESS_SECRET"));
|
|
45
|
-
} catch (e) {
|
|
46
|
-
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid or expired access token");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
verifyRefreshToken(token) {
|
|
50
|
-
try {
|
|
51
|
-
return _jsonwebtoken2.default.verify(token, requireEnv("JWT_REFRESH_SECRET"));
|
|
52
|
-
} catch (e2) {
|
|
53
|
-
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid or expired refresh token");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// src/auth/jwt-auth.guard.ts
|
|
59
|
-
var _common = require('@nestjs/common');
|
|
60
|
-
var JwtAuthGuard = class {
|
|
61
|
-
constructor() {
|
|
62
|
-
this.tokens = new TokenService();
|
|
63
|
-
}
|
|
64
|
-
canActivate(context) {
|
|
65
|
-
const req = context.switchToHttp().getRequest();
|
|
66
|
-
const header = req.headers.authorization;
|
|
67
|
-
const token = _optionalChain([header, 'optionalAccess', _2 => _2.startsWith, 'call', _3 => _3("Bearer ")]) ? header.slice("Bearer ".length) : void 0;
|
|
68
|
-
if (!token) {
|
|
69
|
-
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Missing Authorization bearer token");
|
|
70
|
-
}
|
|
71
|
-
req.user = this.tokens.verifyAccessToken(token);
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
JwtAuthGuard = exports.JwtAuthGuard = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
76
|
-
_common.Injectable.call(void 0, )
|
|
77
|
-
], JwtAuthGuard);
|
|
78
|
-
|
|
79
27
|
// src/auth/roles.decorator.ts
|
|
80
|
-
|
|
28
|
+
var _common = require('@nestjs/common');
|
|
81
29
|
var ROLES_KEY = "nestkit:roles";
|
|
82
30
|
var Roles = (...roles) => _common.SetMetadata.call(void 0, ROLES_KEY, roles);
|
|
83
31
|
|
|
@@ -95,7 +43,7 @@ var RolesGuard = class {
|
|
|
95
43
|
]);
|
|
96
44
|
if (!required || required.length === 0) return true;
|
|
97
45
|
const req = context.switchToHttp().getRequest();
|
|
98
|
-
const has = _nullishCoalesce(_optionalChain([req, 'access',
|
|
46
|
+
const has = _nullishCoalesce(_optionalChain([req, 'access', _2 => _2.user, 'optionalAccess', _3 => _3.roles, 'access', _4 => _4.some, 'call', _5 => _5((role) => required.includes(role))]), () => ( false));
|
|
99
47
|
if (!has) {
|
|
100
48
|
throw new (0, _chunkFDNGAYTZcjs.ForbiddenAppException)("Insufficient role");
|
|
101
49
|
}
|
|
@@ -106,16 +54,10 @@ RolesGuard = exports.RolesGuard = _chunk2REOCMUDcjs.__decorateClass.call(void 0,
|
|
|
106
54
|
_common.Injectable.call(void 0, )
|
|
107
55
|
], RolesGuard);
|
|
108
56
|
|
|
109
|
-
// src/auth/current-user.decorator.ts
|
|
110
|
-
|
|
111
|
-
var CurrentUser = _common.createParamDecorator.call(void 0,
|
|
112
|
-
(_, ctx) => ctx.switchToHttp().getRequest().user
|
|
113
|
-
);
|
|
114
|
-
|
|
115
57
|
// src/auth/current-tenant.decorator.ts
|
|
116
58
|
|
|
117
59
|
var CurrentTenant = _common.createParamDecorator.call(void 0,
|
|
118
|
-
(_, ctx) => _optionalChain([ctx, 'access',
|
|
60
|
+
(_, ctx) => _optionalChain([ctx, 'access', _6 => _6.switchToHttp, 'call', _7 => _7(), 'access', _8 => _8.getRequest, 'call', _9 => _9(), 'access', _10 => _10.user, 'optionalAccess', _11 => _11.tenantId])
|
|
119
61
|
);
|
|
120
62
|
|
|
121
63
|
// src/auth/require-tenant.guard.ts
|
|
@@ -123,7 +65,7 @@ var CurrentTenant = _common.createParamDecorator.call(void 0,
|
|
|
123
65
|
var RequireTenantGuard = class {
|
|
124
66
|
canActivate(context) {
|
|
125
67
|
const req = context.switchToHttp().getRequest();
|
|
126
|
-
if (!_optionalChain([req, 'access',
|
|
68
|
+
if (!_optionalChain([req, 'access', _12 => _12.user, 'optionalAccess', _13 => _13.tenantId])) {
|
|
127
69
|
throw new (0, _chunkFDNGAYTZcjs.ForbiddenAppException)("This action requires a company account");
|
|
128
70
|
}
|
|
129
71
|
return true;
|
|
@@ -164,7 +106,7 @@ _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
|
164
106
|
|
|
165
107
|
// src/auth/auth.service.ts
|
|
166
108
|
var BaseAuthService = class {
|
|
167
|
-
constructor(users, tokens = new TokenService()) {
|
|
109
|
+
constructor(users, tokens = new (0, _chunkNVCI3CQIcjs.TokenService)()) {
|
|
168
110
|
this.users = users;
|
|
169
111
|
this.tokens = tokens;
|
|
170
112
|
}
|
|
@@ -262,4 +204,4 @@ function createAuthController(serviceToken, options = {}) {
|
|
|
262
204
|
|
|
263
205
|
|
|
264
206
|
|
|
265
|
-
exports.BaseAuthService = BaseAuthService; exports.CurrentTenant = CurrentTenant; exports.CurrentUser = CurrentUser; exports.JwtAuthGuard = JwtAuthGuard; exports.LoginDto = LoginDto; exports.ROLES_KEY = ROLES_KEY; exports.RefreshDto = RefreshDto; exports.RegisterDto = RegisterDto; exports.RequireTenantGuard = RequireTenantGuard; exports.Roles = Roles; exports.RolesGuard = RolesGuard; exports.TokenService = TokenService; exports.createAuthController = createAuthController; exports.hashPassword = hashPassword; exports.verifyPassword = verifyPassword;
|
|
207
|
+
exports.BaseAuthService = BaseAuthService; exports.CurrentTenant = CurrentTenant; exports.CurrentUser = _chunkNVCI3CQIcjs.CurrentUser; exports.JwtAuthGuard = _chunkNVCI3CQIcjs.JwtAuthGuard; exports.LoginDto = LoginDto; exports.ROLES_KEY = ROLES_KEY; exports.RefreshDto = RefreshDto; exports.RegisterDto = RegisterDto; exports.RequireTenantGuard = RequireTenantGuard; exports.Roles = Roles; exports.RolesGuard = RolesGuard; exports.TokenService = _chunkNVCI3CQIcjs.TokenService; exports.createAuthController = createAuthController; exports.hashPassword = hashPassword; exports.verifyPassword = verifyPassword;
|
package/dist/auth/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CurrentUser,
|
|
3
|
+
JwtAuthGuard,
|
|
4
|
+
TokenService
|
|
5
|
+
} from "../chunk-A3B2EY4V.js";
|
|
1
6
|
import {
|
|
2
7
|
ConflictAppException,
|
|
3
8
|
ForbiddenAppException,
|
|
@@ -19,70 +24,13 @@ function verifyPassword(plain, passwordHash) {
|
|
|
19
24
|
return compare(plain, passwordHash);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
// src/auth/token.service.ts
|
|
23
|
-
import jwt from "jsonwebtoken";
|
|
24
|
-
function requireEnv(name) {
|
|
25
|
-
const value = process.env[name];
|
|
26
|
-
if (!value) {
|
|
27
|
-
throw new Error(`TokenService: ${name} environment variable is not set`);
|
|
28
|
-
}
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
|
-
var TokenService = class {
|
|
32
|
-
signAccessToken(payload) {
|
|
33
|
-
return jwt.sign(payload, requireEnv("JWT_ACCESS_SECRET"), {
|
|
34
|
-
expiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? "15m"
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
signRefreshToken(payload) {
|
|
38
|
-
return jwt.sign(payload, requireEnv("JWT_REFRESH_SECRET"), {
|
|
39
|
-
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? "30d"
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
verifyAccessToken(token) {
|
|
43
|
-
try {
|
|
44
|
-
return jwt.verify(token, requireEnv("JWT_ACCESS_SECRET"));
|
|
45
|
-
} catch {
|
|
46
|
-
throw new UnauthorizedAppException("Invalid or expired access token");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
verifyRefreshToken(token) {
|
|
50
|
-
try {
|
|
51
|
-
return jwt.verify(token, requireEnv("JWT_REFRESH_SECRET"));
|
|
52
|
-
} catch {
|
|
53
|
-
throw new UnauthorizedAppException("Invalid or expired refresh token");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// src/auth/jwt-auth.guard.ts
|
|
59
|
-
import { Injectable } from "@nestjs/common";
|
|
60
|
-
var JwtAuthGuard = class {
|
|
61
|
-
constructor() {
|
|
62
|
-
this.tokens = new TokenService();
|
|
63
|
-
}
|
|
64
|
-
canActivate(context) {
|
|
65
|
-
const req = context.switchToHttp().getRequest();
|
|
66
|
-
const header = req.headers.authorization;
|
|
67
|
-
const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : void 0;
|
|
68
|
-
if (!token) {
|
|
69
|
-
throw new UnauthorizedAppException("Missing Authorization bearer token");
|
|
70
|
-
}
|
|
71
|
-
req.user = this.tokens.verifyAccessToken(token);
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
JwtAuthGuard = __decorateClass([
|
|
76
|
-
Injectable()
|
|
77
|
-
], JwtAuthGuard);
|
|
78
|
-
|
|
79
27
|
// src/auth/roles.decorator.ts
|
|
80
28
|
import { SetMetadata } from "@nestjs/common";
|
|
81
29
|
var ROLES_KEY = "nestkit:roles";
|
|
82
30
|
var Roles = (...roles) => SetMetadata(ROLES_KEY, roles);
|
|
83
31
|
|
|
84
32
|
// src/auth/roles.guard.ts
|
|
85
|
-
import { Injectable
|
|
33
|
+
import { Injectable } from "@nestjs/common";
|
|
86
34
|
import "@nestjs/core";
|
|
87
35
|
var RolesGuard = class {
|
|
88
36
|
constructor(reflector) {
|
|
@@ -103,23 +51,17 @@ var RolesGuard = class {
|
|
|
103
51
|
}
|
|
104
52
|
};
|
|
105
53
|
RolesGuard = __decorateClass([
|
|
106
|
-
|
|
54
|
+
Injectable()
|
|
107
55
|
], RolesGuard);
|
|
108
56
|
|
|
109
|
-
// src/auth/current-user.decorator.ts
|
|
110
|
-
import { createParamDecorator } from "@nestjs/common";
|
|
111
|
-
var CurrentUser = createParamDecorator(
|
|
112
|
-
(_, ctx) => ctx.switchToHttp().getRequest().user
|
|
113
|
-
);
|
|
114
|
-
|
|
115
57
|
// src/auth/current-tenant.decorator.ts
|
|
116
|
-
import { createParamDecorator
|
|
117
|
-
var CurrentTenant =
|
|
58
|
+
import { createParamDecorator } from "@nestjs/common";
|
|
59
|
+
var CurrentTenant = createParamDecorator(
|
|
118
60
|
(_, ctx) => ctx.switchToHttp().getRequest().user?.tenantId
|
|
119
61
|
);
|
|
120
62
|
|
|
121
63
|
// src/auth/require-tenant.guard.ts
|
|
122
|
-
import { Injectable as
|
|
64
|
+
import { Injectable as Injectable2 } from "@nestjs/common";
|
|
123
65
|
var RequireTenantGuard = class {
|
|
124
66
|
canActivate(context) {
|
|
125
67
|
const req = context.switchToHttp().getRequest();
|
|
@@ -130,7 +72,7 @@ var RequireTenantGuard = class {
|
|
|
130
72
|
}
|
|
131
73
|
};
|
|
132
74
|
RequireTenantGuard = __decorateClass([
|
|
133
|
-
|
|
75
|
+
Injectable2()
|
|
134
76
|
], RequireTenantGuard);
|
|
135
77
|
|
|
136
78
|
// src/auth/auth.dto.ts
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The little the OAuth controller needs from the HTTP layer, described
|
|
3
|
+
* structurally so the module works on Fastify and Express without depending
|
|
4
|
+
* on either.
|
|
5
|
+
*
|
|
6
|
+
* Cookie support is not built in on either adapter — the app has to register
|
|
7
|
+
* `@fastify/cookie` or `cookie-parser`. Fastify names the methods
|
|
8
|
+
* `setCookie`/`clearCookie`, Express names them `cookie`/`clearCookie`; the
|
|
9
|
+
* helpers below paper over exactly that difference and nothing else.
|
|
10
|
+
*/
|
|
11
|
+
export interface CookieOptions {
|
|
12
|
+
httpOnly?: boolean;
|
|
13
|
+
secure?: boolean;
|
|
14
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
15
|
+
path?: string;
|
|
16
|
+
expires?: Date;
|
|
17
|
+
}
|
|
18
|
+
export interface ReplyLike {
|
|
19
|
+
redirect: ((url: string, statusCode?: number) => unknown) & ((...args: never[]) => unknown);
|
|
20
|
+
setCookie?(name: string, value: string, options?: CookieOptions): unknown;
|
|
21
|
+
cookie?(name: string, value: string, options?: CookieOptions): unknown;
|
|
22
|
+
clearCookie?(name: string, options?: CookieOptions): unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface RequestLike {
|
|
25
|
+
cookies?: Record<string, string | undefined>;
|
|
26
|
+
query?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
export declare function setCookie(reply: ReplyLike, name: string, value: string, options: CookieOptions): void;
|
|
29
|
+
export declare function clearCookie(reply: ReplyLike, name: string, options: CookieOptions): void;
|
|
30
|
+
/**
|
|
31
|
+
* Redirects with an explicit 302.
|
|
32
|
+
*
|
|
33
|
+
* Not optional: Nest's Fastify adapter answers `reply.redirect(url)` with a
|
|
34
|
+
* 200 carrying a Location header, which no browser follows — the sign-in
|
|
35
|
+
* simply stops on a blank page. Express ignores the second argument when the
|
|
36
|
+
* first is a URL, so the same call is correct on both.
|
|
37
|
+
*/
|
|
38
|
+
export declare function redirect(reply: ReplyLike, url: string): void;
|