@aranzatech/aranza-auth 0.1.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/CHANGELOG.md +40 -0
- package/README.md +468 -0
- package/dist/auth-repository.interface-BMlJc-98.d.cts +90 -0
- package/dist/auth-repository.interface-BMlJc-98.d.ts +90 -0
- package/dist/chunk-DKYNHXY2.js +36 -0
- package/dist/chunk-DKYNHXY2.js.map +1 -0
- package/dist/index.cjs +681 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +127 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +618 -0
- package/dist/index.js.map +1 -0
- package/dist/mongo/index.cjs +256 -0
- package/dist/mongo/index.cjs.map +1 -0
- package/dist/mongo/index.d.cts +65 -0
- package/dist/mongo/index.d.ts +65 -0
- package/dist/mongo/index.js +226 -0
- package/dist/mongo/index.js.map +1 -0
- package/package.json +102 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-07-03
|
|
10
|
+
|
|
11
|
+
First public release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`AuthModule.forRoot` / `forRootAsync`** — módulo dinámico global con configuración JWT.
|
|
16
|
+
- **Endpoints REST** bajo `/auth`:
|
|
17
|
+
- `POST /auth/register` — alta de cuenta
|
|
18
|
+
- `POST /auth/login` — login con access + refresh token
|
|
19
|
+
- `POST /auth/refresh` — renovación de tokens
|
|
20
|
+
- `POST /auth/logout` — invalidar refresh token (Bearer)
|
|
21
|
+
- `GET /auth/me` — usuario autenticado (Bearer)
|
|
22
|
+
- `POST /auth/verify-email` — verificación de email (opt-in)
|
|
23
|
+
- `POST /auth/forgot-password` — solicitar reset (opt-in)
|
|
24
|
+
- `POST /auth/reset-password` — nueva contraseña con token (opt-in)
|
|
25
|
+
- **`AuthService`**, **`TokenService`**, **`JwtStrategy`**, **`JwtAuthGuard`**, decorador **`@CurrentUser()`**.
|
|
26
|
+
- **Feature flags** (`features`) — todo desactivado por defecto, ideal para POCs:
|
|
27
|
+
- `emailVerification` — bloquea login hasta verificar email
|
|
28
|
+
- `passwordReset` — habilita forgot/reset password
|
|
29
|
+
- `refreshTokenRotation` — rota hash del refresh token en DB (default: `true`)
|
|
30
|
+
- **`AuthHooks`** — extiende JWT payload, `/auth/me`, lifecycle y envío de emails.
|
|
31
|
+
- **`DefaultAuthHooks`** — implementación mínima incluida.
|
|
32
|
+
- **Adapter MongoDB** (`@aranzatech/aranza-auth/mongo`):
|
|
33
|
+
- `BaseAuthAccountSchema` — schema base extensible
|
|
34
|
+
- `MongoAuthRepository` — implementación de `IAuthRepository`
|
|
35
|
+
- `MongoAuthModule.forFeature()` — registro de schema + repository
|
|
36
|
+
- Tokens de verificación/reset hasheados (SHA-256) con TTL configurable.
|
|
37
|
+
- Build dual ESM/CJS con types (`tsup`), tests (`vitest`), CI GitHub Actions.
|
|
38
|
+
|
|
39
|
+
[Unreleased]: https://github.com/aapa96/aranza-auth/compare/v0.1.0...HEAD
|
|
40
|
+
[0.1.0]: https://github.com/aapa96/aranza-auth/releases/tag/v0.1.0
|
package/README.md
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# @aranzatech/aranza-auth
|
|
2
|
+
|
|
3
|
+
Módulo de autenticación **extensible** para NestJS. JWT, refresh tokens, register/login y adapter MongoDB — sin acoplar tu dominio (org, roles, users, etc.).
|
|
4
|
+
|
|
5
|
+
Ideal para reutilizar auth en todos tus proyectos AranzaTech: instalas, configuras, extiendes el modelo y listo.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tabla de contenidos
|
|
10
|
+
|
|
11
|
+
- [Instalación](#instalación)
|
|
12
|
+
- [Inicio rápido (5 minutos)](#inicio-rápido-5-minutos)
|
|
13
|
+
- [Variables de entorno](#variables-de-entorno)
|
|
14
|
+
- [Configuración del módulo](#configuración-del-módulo)
|
|
15
|
+
- [Feature flags](#feature-flags)
|
|
16
|
+
- [Endpoints](#endpoints)
|
|
17
|
+
- [Proteger rutas propias](#proteger-rutas-propias)
|
|
18
|
+
- [Extender con AuthHooks](#extender-con-authhooks)
|
|
19
|
+
- [Extender el schema MongoDB](#extender-el-schema-mongodb)
|
|
20
|
+
- [Flujos opcionales (email y password)](#flujos-opcionales-email-y-password)
|
|
21
|
+
- [Exports públicos](#exports-públicos)
|
|
22
|
+
- [Requisitos del proyecto consumidor](#requisitos-del-proyecto-consumidor)
|
|
23
|
+
- [Limitaciones conocidas (v0.1.0)](#limitaciones-conocidas-v010)
|
|
24
|
+
- [Roadmap](#roadmap)
|
|
25
|
+
- [Licencia](#licencia)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Instalación
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @aranzatech/aranza-auth
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Peer dependencies** (instalar en tu app Nest):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @nestjs/common @nestjs/core @nestjs/jwt @nestjs/passport \
|
|
39
|
+
@nestjs/mongoose mongoose passport passport-jwt bcryptjs \
|
|
40
|
+
class-validator class-transformer reflect-metadata
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> NestJS **11+** recomendado.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Inicio rápido (5 minutos)
|
|
48
|
+
|
|
49
|
+
### 1. Variables de entorno
|
|
50
|
+
|
|
51
|
+
```env
|
|
52
|
+
MONGODB_URI=mongodb://localhost:27017/myapp
|
|
53
|
+
JWT_SECRET=your-access-secret-min-32-chars
|
|
54
|
+
JWT_REFRESH_SECRET=your-refresh-secret-min-32-chars
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Conectar el módulo
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// app.module.ts
|
|
61
|
+
import { Module } from "@nestjs/common";
|
|
62
|
+
import { ConfigModule, ConfigService } from "@nestjs/config";
|
|
63
|
+
import { MongooseModule } from "@nestjs/mongoose";
|
|
64
|
+
import { AuthModule } from "@aranzatech/aranza-auth";
|
|
65
|
+
import { MongoAuthModule } from "@aranzatech/aranza-auth/mongo";
|
|
66
|
+
|
|
67
|
+
@Module({
|
|
68
|
+
imports: [
|
|
69
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
70
|
+
MongooseModule.forRoot(process.env.MONGODB_URI!),
|
|
71
|
+
MongoAuthModule.forFeature(),
|
|
72
|
+
AuthModule.forRootAsync({
|
|
73
|
+
imports: [ConfigModule],
|
|
74
|
+
inject: [ConfigService],
|
|
75
|
+
useFactory: (config: ConfigService) => ({
|
|
76
|
+
secret: config.getOrThrow("JWT_SECRET"),
|
|
77
|
+
refreshSecret: config.getOrThrow("JWT_REFRESH_SECRET"),
|
|
78
|
+
expiresIn: "1h",
|
|
79
|
+
refreshExpiresIn: "7d",
|
|
80
|
+
identifierField: "email",
|
|
81
|
+
}),
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
})
|
|
85
|
+
export class AppModule {}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Habilitar validación global (requerido)
|
|
89
|
+
|
|
90
|
+
Los DTOs usan `class-validator`. Activa el pipe global en `main.ts`:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { ValidationPipe } from "@nestjs/common";
|
|
94
|
+
|
|
95
|
+
app.useGlobalPipes(
|
|
96
|
+
new ValidationPipe({ whitelist: true, transform: true }),
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 4. Probar
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Register
|
|
104
|
+
curl -X POST http://localhost:3000/auth/register \
|
|
105
|
+
-H "Content-Type: application/json" \
|
|
106
|
+
-d '{"email":"user@example.com","password":"SecurePass123!"}'
|
|
107
|
+
|
|
108
|
+
# Login
|
|
109
|
+
curl -X POST http://localhost:3000/auth/login \
|
|
110
|
+
-H "Content-Type: application/json" \
|
|
111
|
+
-d '{"email":"user@example.com","password":"SecurePass123!"}'
|
|
112
|
+
|
|
113
|
+
# Me (usa el accessToken del login)
|
|
114
|
+
curl http://localhost:3000/auth/me \
|
|
115
|
+
-H "Authorization: Bearer <accessToken>"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Con eso ya tienes auth funcional para un POC.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Variables de entorno
|
|
123
|
+
|
|
124
|
+
| Variable | Requerida | Descripción |
|
|
125
|
+
|----------|-----------|-------------|
|
|
126
|
+
| `MONGODB_URI` | Sí | Connection string de MongoDB |
|
|
127
|
+
| `JWT_SECRET` | Sí | Secret para access tokens |
|
|
128
|
+
| `JWT_REFRESH_SECRET` | Sí | Secret para refresh tokens (distinto al anterior) |
|
|
129
|
+
| `FRONTEND_URL` | No | Base URL para links en emails (verify/reset) |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Configuración del módulo
|
|
134
|
+
|
|
135
|
+
`AuthModule.forRootAsync()` acepta un objeto `AuthModuleOptions`:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
AuthModule.forRootAsync({
|
|
139
|
+
imports: [ConfigModule],
|
|
140
|
+
inject: [ConfigService],
|
|
141
|
+
useFactory: (config: ConfigService) => ({
|
|
142
|
+
// ── JWT (requerido) ──────────────────────────────────
|
|
143
|
+
secret: config.getOrThrow("JWT_SECRET"),
|
|
144
|
+
refreshSecret: config.getOrThrow("JWT_REFRESH_SECRET"),
|
|
145
|
+
expiresIn: "1h", // access token (default: "1h")
|
|
146
|
+
refreshExpiresIn: "7d", // refresh token (default: "7d")
|
|
147
|
+
|
|
148
|
+
// ── Identificador de login ───────────────────────────
|
|
149
|
+
identifierField: "email", // "email" | "username" (default: "email")
|
|
150
|
+
|
|
151
|
+
// ── Features opcionales ──────────────────────────────
|
|
152
|
+
features: {
|
|
153
|
+
emailVerification: false, // default: false
|
|
154
|
+
passwordReset: false, // default: false
|
|
155
|
+
refreshTokenRotation: true, // default: true
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// ── TTL de tokens de email/reset ─────────────────────
|
|
159
|
+
emailVerificationTokenTtlMs: 24 * 60 * 60 * 1000, // 24h
|
|
160
|
+
passwordResetTokenTtlMs: 15 * 60 * 1000, // 15min
|
|
161
|
+
|
|
162
|
+
// ── Hooks personalizados ─────────────────────────────
|
|
163
|
+
hooks: AppAuthHooks, // default: DefaultAuthHooks
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Orden de imports
|
|
169
|
+
|
|
170
|
+
`MongoAuthModule.forFeature()` debe importarse **junto** con `AuthModule`. El orden recomendado:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
imports: [
|
|
174
|
+
MongooseModule.forRoot(...),
|
|
175
|
+
MongoAuthModule.forFeature(...), // provee AUTH_REPOSITORY
|
|
176
|
+
AuthModule.forRootAsync(...), // consume AUTH_REPOSITORY
|
|
177
|
+
]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Feature flags
|
|
183
|
+
|
|
184
|
+
Todo está **desactivado por defecto**. Sin declarar `features`, obtienes el mínimo para POCs.
|
|
185
|
+
|
|
186
|
+
| Flag | Default | Qué hace |
|
|
187
|
+
|------|---------|----------|
|
|
188
|
+
| `emailVerification` | `false` | Envía email al register, bloquea login hasta verificar |
|
|
189
|
+
| `passwordReset` | `false` | Habilita `forgot-password` y `reset-password` |
|
|
190
|
+
| `refreshTokenRotation` | `true` | Guarda hash del refresh token en DB y lo rota |
|
|
191
|
+
|
|
192
|
+
### Modo POC (solo login/register)
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// No declares features — o déjalo vacío:
|
|
196
|
+
AuthModule.forRootAsync({
|
|
197
|
+
useFactory: () => ({
|
|
198
|
+
secret: "...",
|
|
199
|
+
refreshSecret: "...",
|
|
200
|
+
}),
|
|
201
|
+
}),
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- Register crea cuenta con `emailVerified: true` → login inmediato.
|
|
205
|
+
- Endpoints `verify-email`, `forgot-password`, `reset-password` responden **404**.
|
|
206
|
+
|
|
207
|
+
### Modo producción
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
features: {
|
|
211
|
+
emailVerification: true,
|
|
212
|
+
passwordReset: true,
|
|
213
|
+
refreshTokenRotation: true,
|
|
214
|
+
},
|
|
215
|
+
hooks: AppAuthHooks, // debe implementar sendEmail
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> Si activas `emailVerification` o `passwordReset`, **debes** implementar `AuthHooks.sendEmail`. Si no, register/forgot fallará con un error claro.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Endpoints
|
|
223
|
+
|
|
224
|
+
| Método | Ruta | Auth | Feature | Body | Respuesta |
|
|
225
|
+
|--------|------|------|---------|------|-----------|
|
|
226
|
+
| `POST` | `/auth/register` | — | — | `{ email?, username?, password }` | `{ registered: true }` |
|
|
227
|
+
| `POST` | `/auth/login` | — | — | `{ email?, username?, password }` | `{ accessToken, refreshToken }` |
|
|
228
|
+
| `POST` | `/auth/refresh` | — | — | `{ refreshToken }` | `{ accessToken, refreshToken }` |
|
|
229
|
+
| `POST` | `/auth/logout` | Bearer | — | — | `{ loggedOut: true }` |
|
|
230
|
+
| `GET` | `/auth/me` | Bearer | — | — | objeto enriquecido via hooks |
|
|
231
|
+
| `POST` | `/auth/verify-email` | — | `emailVerification` | `{ token }` | `{ verified: true }` |
|
|
232
|
+
| `POST` | `/auth/forgot-password` | — | `passwordReset` | `{ email }` | `{ sent: true }` |
|
|
233
|
+
| `POST` | `/auth/reset-password` | — | `passwordReset` | `{ token, newPassword }` | `{ reset: true }` |
|
|
234
|
+
|
|
235
|
+
### Errores comunes
|
|
236
|
+
|
|
237
|
+
| Código | Mensaje | Causa |
|
|
238
|
+
|--------|---------|-------|
|
|
239
|
+
| `401` | `Invalid credentials` | Email/username o password incorrectos |
|
|
240
|
+
| `401` | `EMAIL_NOT_VERIFIED` | Feature `emailVerification` activa y email sin verificar |
|
|
241
|
+
| `401` | `ACCOUNT_DISABLED` | Cuenta desactivada |
|
|
242
|
+
| `401` | `Invalid refresh token` | Refresh expirado, revocado o inválido |
|
|
243
|
+
| `404` | — | Feature desactivada (endpoint no disponible) |
|
|
244
|
+
| `400` | `TOKEN_INVALID_OR_EXPIRED` | Token de verify/reset inválido o expirado |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Proteger rutas propias
|
|
249
|
+
|
|
250
|
+
Usa el guard y el decorador exportados por la lib:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { Controller, Get, UseGuards } from "@nestjs/common";
|
|
254
|
+
import {
|
|
255
|
+
JwtAuthGuard,
|
|
256
|
+
CurrentUser,
|
|
257
|
+
type AuthJwtPayload,
|
|
258
|
+
} from "@aranzatech/aranza-auth";
|
|
259
|
+
|
|
260
|
+
@Controller("projects")
|
|
261
|
+
export class ProjectsController {
|
|
262
|
+
@Get()
|
|
263
|
+
@UseGuards(JwtAuthGuard)
|
|
264
|
+
list(@CurrentUser() user: AuthJwtPayload) {
|
|
265
|
+
// user.sub → ID de la cuenta auth
|
|
266
|
+
// user.email, user.orgId, etc. → lo que agregues en buildJwtPayload
|
|
267
|
+
return { ownerId: user.sub };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
También puedes registrar `JwtAuthGuard` como guard global:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
{ provide: APP_GUARD, useClass: JwtAuthGuard }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Extender con AuthHooks
|
|
281
|
+
|
|
282
|
+
La lib maneja auth genérico. Tu dominio (org, roles, users) va en **hooks**:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// app-auth.hooks.ts
|
|
286
|
+
import { Injectable } from "@nestjs/common";
|
|
287
|
+
import type { AuthHooks, BaseAuthAccount } from "@aranzatech/aranza-auth";
|
|
288
|
+
|
|
289
|
+
interface MyAuthAccount extends BaseAuthAccount {
|
|
290
|
+
orgId: string;
|
|
291
|
+
roleId: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@Injectable()
|
|
295
|
+
export class AppAuthHooks implements AuthHooks<MyAuthAccount> {
|
|
296
|
+
constructor(private readonly orgService: OrgService) {}
|
|
297
|
+
|
|
298
|
+
/** Campos extra en el JWT */
|
|
299
|
+
async buildJwtPayload(account: MyAuthAccount) {
|
|
300
|
+
return {
|
|
301
|
+
orgId: account.orgId,
|
|
302
|
+
roleId: account.roleId,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Respuesta de GET /auth/me */
|
|
307
|
+
async enrichMe(account: MyAuthAccount) {
|
|
308
|
+
const org = await this.orgService.findById(account.orgId);
|
|
309
|
+
return {
|
|
310
|
+
id: account.id,
|
|
311
|
+
email: account.email,
|
|
312
|
+
org: { id: org.id, name: org.name },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Validaciones antes de crear cuenta */
|
|
317
|
+
async onBeforeRegister(input) {
|
|
318
|
+
// ej: verificar invitación, orgId, etc.
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Envío de emails (requerido si activas emailVerification o passwordReset) */
|
|
322
|
+
async sendEmail(type: "verify" | "reset", to: string, token: string) {
|
|
323
|
+
const base = process.env.FRONTEND_URL ?? "http://localhost:3001";
|
|
324
|
+
const path = type === "verify" ? "verify-email" : "reset-password";
|
|
325
|
+
const url = `${base}/auth/${path}?token=${token}`;
|
|
326
|
+
// await this.mailer.send({ to, subject: "...", html: url });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Registra los hooks en la config:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
AuthModule.forRootAsync({
|
|
335
|
+
useFactory: () => ({
|
|
336
|
+
secret: "...",
|
|
337
|
+
refreshSecret: "...",
|
|
338
|
+
hooks: AppAuthHooks,
|
|
339
|
+
}),
|
|
340
|
+
}),
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Métodos disponibles en AuthHooks
|
|
344
|
+
|
|
345
|
+
| Método | Cuándo se ejecuta | Requerido |
|
|
346
|
+
|--------|-------------------|-----------|
|
|
347
|
+
| `buildJwtPayload` | Login, refresh | Sí |
|
|
348
|
+
| `enrichMe` | GET /auth/me | No (usa default) |
|
|
349
|
+
| `onBeforeRegister` | Antes de crear cuenta | No |
|
|
350
|
+
| `onAfterRegister` | Después de crear cuenta | No |
|
|
351
|
+
| `onAfterLogin` | Después de login exitoso | No |
|
|
352
|
+
| `sendEmail` | Register (verify) o forgot (reset) | Sí si features de email activas |
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Extender el schema MongoDB
|
|
357
|
+
|
|
358
|
+
El schema base incluye: `email`, `username`, `passwordHash`, `refreshTokenHash`, `emailVerified`, `disabled` y campos de tokens.
|
|
359
|
+
|
|
360
|
+
### Agregar campos de dominio
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
|
364
|
+
import { Types } from "mongoose";
|
|
365
|
+
import { baseAuthAccountSchema } from "@aranzatech/aranza-auth/mongo";
|
|
366
|
+
|
|
367
|
+
@Schema()
|
|
368
|
+
export class AuthAccount {
|
|
369
|
+
@Prop({ type: Types.ObjectId, ref: "Organization", required: true })
|
|
370
|
+
orgId!: Types.ObjectId;
|
|
371
|
+
|
|
372
|
+
@Prop({ type: Types.ObjectId, ref: "Role", required: true })
|
|
373
|
+
roleId!: Types.ObjectId;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export const AuthAccountSchema = SchemaFactory.createForClass(AuthAccount);
|
|
377
|
+
AuthAccountSchema.add(baseAuthAccountSchema);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Registrar schema extendido
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { AuthAccount, AuthAccountSchema } from "./schemas/auth-account.schema";
|
|
384
|
+
|
|
385
|
+
MongoAuthModule.forFeature({
|
|
386
|
+
name: AuthAccount.name,
|
|
387
|
+
schema: AuthAccountSchema,
|
|
388
|
+
identifierField: "username", // si login es por username
|
|
389
|
+
}),
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
> `identifierField` en `MongoAuthModule.forFeature()` debe coincidir con el de `AuthModule`.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Flujos opcionales (email y password)
|
|
397
|
+
|
|
398
|
+
### Verificación de email
|
|
399
|
+
|
|
400
|
+
1. Activa `features.emailVerification: true`
|
|
401
|
+
2. Implementa `sendEmail` en hooks
|
|
402
|
+
3. Register envía email con token → usuario visita link → `POST /auth/verify-email` con `{ token }`
|
|
403
|
+
4. Login bloqueado hasta `emailVerified: true`
|
|
404
|
+
|
|
405
|
+
Si usas `identifierField: "username"`, el register **debe incluir `email`** además del username.
|
|
406
|
+
|
|
407
|
+
### Forgot / Reset password
|
|
408
|
+
|
|
409
|
+
1. Activa `features.passwordReset: true`
|
|
410
|
+
2. Implementa `sendEmail` en hooks
|
|
411
|
+
3. `POST /auth/forgot-password` con `{ email }` → siempre responde `{ sent: true }` (no revela si el email existe)
|
|
412
|
+
4. Usuario recibe link → `POST /auth/reset-password` con `{ token, newPassword }`
|
|
413
|
+
5. Reset invalida refresh tokens activos
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Exports públicos
|
|
418
|
+
|
|
419
|
+
| Import | Contenido |
|
|
420
|
+
|--------|-----------|
|
|
421
|
+
| `@aranzatech/aranza-auth` | `AuthModule`, `AuthService`, guards, DTOs, interfaces, tokens |
|
|
422
|
+
| `@aranzatech/aranza-auth/mongo` | `MongoAuthModule`, `MongoAuthRepository`, schemas |
|
|
423
|
+
|
|
424
|
+
### Tokens de inyección
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { AUTH_MODULE_OPTIONS, AUTH_HOOKS, AUTH_REPOSITORY } from "@aranzatech/aranza-auth";
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Útiles si necesitas acceder a config o reemplazar el repository manualmente.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Requisitos del proyecto consumidor
|
|
435
|
+
|
|
436
|
+
- [ ] NestJS 11+
|
|
437
|
+
- [ ] `ValidationPipe` global activo
|
|
438
|
+
- [ ] `reflect-metadata` importado al inicio de `main.ts`
|
|
439
|
+
- [ ] MongoDB conectado via `@nestjs/mongoose`
|
|
440
|
+
- [ ] `MongoAuthModule.forFeature()` importado antes o junto a `AuthModule`
|
|
441
|
+
- [ ] Secrets JWT distintos para access y refresh
|
|
442
|
+
- [ ] Si usas features de email: implementar `AuthHooks.sendEmail`
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Limitaciones conocidas (v0.1.0)
|
|
447
|
+
|
|
448
|
+
| Limitación | Workaround / versión futura |
|
|
449
|
+
|------------|----------------------------|
|
|
450
|
+
| Hooks se instancian con `new HooksClass()` sin DI de Nest | Registrar servicios manualmente o v0.2.0 con DI |
|
|
451
|
+
| Ruta fija `/auth` (routePrefix en config aún no aplicado) | Usar global prefix de Nest o v0.2.0 |
|
|
452
|
+
| Solo adapter MongoDB | Prisma en v0.3.0 |
|
|
453
|
+
| Sin OAuth (Google, GitHub, etc.) | v0.2.0 |
|
|
454
|
+
| Sin `change-password` autenticado | v0.2.0 |
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Roadmap
|
|
459
|
+
|
|
460
|
+
- [ ] **0.2.0** — OAuth (Google, GitHub), `change-password`, hooks con DI
|
|
461
|
+
- [ ] **0.3.0** — Adapter Prisma
|
|
462
|
+
- [ ] **0.4.0** — Migración de AranzaFlow como caso de referencia
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Licencia
|
|
467
|
+
|
|
468
|
+
MIT © [AranzaTech](https://github.com/aapa96)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
interface BaseAuthAccount {
|
|
2
|
+
id: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
username?: string;
|
|
5
|
+
emailVerified: boolean;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
createdAt?: Date;
|
|
8
|
+
updatedAt?: Date;
|
|
9
|
+
}
|
|
10
|
+
/** Internal shape returned by repository secret lookups. */
|
|
11
|
+
interface AuthAccountWithSecrets extends BaseAuthAccount {
|
|
12
|
+
passwordHash?: string;
|
|
13
|
+
refreshTokenHash?: string | null;
|
|
14
|
+
}
|
|
15
|
+
interface RegisterInput {
|
|
16
|
+
email?: string;
|
|
17
|
+
username?: string;
|
|
18
|
+
password: string;
|
|
19
|
+
}
|
|
20
|
+
interface AuthTokens {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken: string;
|
|
23
|
+
}
|
|
24
|
+
interface AuthHooks<TAccount extends BaseAuthAccount = BaseAuthAccount> {
|
|
25
|
+
buildJwtPayload(account: TAccount): Promise<Record<string, unknown>>;
|
|
26
|
+
onBeforeRegister?(input: RegisterInput): Promise<void>;
|
|
27
|
+
onAfterRegister?(account: TAccount): Promise<void>;
|
|
28
|
+
onAfterLogin?(account: TAccount): Promise<void>;
|
|
29
|
+
enrichMe?(account: TAccount): Promise<Record<string, unknown>>;
|
|
30
|
+
sendEmail?(type: "verify" | "reset", to: string, token: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type AuthIdentifierField = "email" | "username";
|
|
34
|
+
interface AuthFeatures {
|
|
35
|
+
/** Require verified email before login. Sends verification email on register. Default: `false`. */
|
|
36
|
+
emailVerification?: boolean;
|
|
37
|
+
/** Enable forgot/reset password endpoints. Default: `false`. */
|
|
38
|
+
passwordReset?: boolean;
|
|
39
|
+
/** Rotate refresh token hash on login/refresh. Default: `true`. */
|
|
40
|
+
refreshTokenRotation?: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface AuthJwtConfig {
|
|
43
|
+
secret: string;
|
|
44
|
+
expiresIn?: string;
|
|
45
|
+
refreshSecret: string;
|
|
46
|
+
refreshExpiresIn?: string;
|
|
47
|
+
}
|
|
48
|
+
interface AuthModuleOptions extends AuthJwtConfig {
|
|
49
|
+
/** Field used for login/register lookup. Default: `email`. */
|
|
50
|
+
identifierField?: AuthIdentifierField;
|
|
51
|
+
/** Optional feature flags — all off by default (POC-friendly). */
|
|
52
|
+
features?: AuthFeatures;
|
|
53
|
+
/** Email verification token TTL in ms. Default: 24h. */
|
|
54
|
+
emailVerificationTokenTtlMs?: number;
|
|
55
|
+
/** Password reset token TTL in ms. Default: 15min. */
|
|
56
|
+
passwordResetTokenTtlMs?: number;
|
|
57
|
+
/** Route prefix for auth controller. Default: `auth`. */
|
|
58
|
+
routePrefix?: string;
|
|
59
|
+
/** Custom hooks class. Defaults to built-in minimal payload. */
|
|
60
|
+
hooks?: new (...args: any[]) => AuthHooks;
|
|
61
|
+
}
|
|
62
|
+
interface AuthModuleAsyncOptions {
|
|
63
|
+
imports?: unknown[];
|
|
64
|
+
inject?: unknown[];
|
|
65
|
+
useFactory: (...args: unknown[]) => AuthModuleOptions | Promise<AuthModuleOptions>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface CreateAccountData extends RegisterInput {
|
|
69
|
+
passwordHash: string;
|
|
70
|
+
/** When `false`, account can login immediately. Default decided by AuthService. */
|
|
71
|
+
emailVerified?: boolean;
|
|
72
|
+
}
|
|
73
|
+
interface IAuthRepository<TAccount extends BaseAuthAccount = BaseAuthAccount> {
|
|
74
|
+
create(data: CreateAccountData): Promise<TAccount>;
|
|
75
|
+
findByIdentifier(identifier: string): Promise<TAccount | null>;
|
|
76
|
+
findByEmail(email: string): Promise<BaseAuthAccount | null>;
|
|
77
|
+
findByIdentifierWithSecrets(identifier: string): Promise<AuthAccountWithSecrets | null>;
|
|
78
|
+
findById(id: string): Promise<BaseAuthAccount | null>;
|
|
79
|
+
findByIdWithSecrets(id: string): Promise<AuthAccountWithSecrets | null>;
|
|
80
|
+
updateRefreshTokenHash(id: string, hash: string | null): Promise<void>;
|
|
81
|
+
updatePasswordHash(id: string, passwordHash: string): Promise<void>;
|
|
82
|
+
setEmailVerificationToken(id: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
83
|
+
findByEmailVerificationTokenHash(tokenHash: string): Promise<BaseAuthAccount | null>;
|
|
84
|
+
markEmailVerified(id: string): Promise<void>;
|
|
85
|
+
setResetToken(id: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
86
|
+
findByResetTokenHash(tokenHash: string): Promise<AuthAccountWithSecrets | null>;
|
|
87
|
+
clearResetToken(id: string): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type { AuthModuleOptions as A, BaseAuthAccount as B, CreateAccountData as C, IAuthRepository as I, RegisterInput as R, AuthModuleAsyncOptions as a, AuthHooks as b, AuthTokens as c, AuthAccountWithSecrets as d, AuthFeatures as e, AuthIdentifierField as f, AuthJwtConfig as g };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
interface BaseAuthAccount {
|
|
2
|
+
id: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
username?: string;
|
|
5
|
+
emailVerified: boolean;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
createdAt?: Date;
|
|
8
|
+
updatedAt?: Date;
|
|
9
|
+
}
|
|
10
|
+
/** Internal shape returned by repository secret lookups. */
|
|
11
|
+
interface AuthAccountWithSecrets extends BaseAuthAccount {
|
|
12
|
+
passwordHash?: string;
|
|
13
|
+
refreshTokenHash?: string | null;
|
|
14
|
+
}
|
|
15
|
+
interface RegisterInput {
|
|
16
|
+
email?: string;
|
|
17
|
+
username?: string;
|
|
18
|
+
password: string;
|
|
19
|
+
}
|
|
20
|
+
interface AuthTokens {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken: string;
|
|
23
|
+
}
|
|
24
|
+
interface AuthHooks<TAccount extends BaseAuthAccount = BaseAuthAccount> {
|
|
25
|
+
buildJwtPayload(account: TAccount): Promise<Record<string, unknown>>;
|
|
26
|
+
onBeforeRegister?(input: RegisterInput): Promise<void>;
|
|
27
|
+
onAfterRegister?(account: TAccount): Promise<void>;
|
|
28
|
+
onAfterLogin?(account: TAccount): Promise<void>;
|
|
29
|
+
enrichMe?(account: TAccount): Promise<Record<string, unknown>>;
|
|
30
|
+
sendEmail?(type: "verify" | "reset", to: string, token: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type AuthIdentifierField = "email" | "username";
|
|
34
|
+
interface AuthFeatures {
|
|
35
|
+
/** Require verified email before login. Sends verification email on register. Default: `false`. */
|
|
36
|
+
emailVerification?: boolean;
|
|
37
|
+
/** Enable forgot/reset password endpoints. Default: `false`. */
|
|
38
|
+
passwordReset?: boolean;
|
|
39
|
+
/** Rotate refresh token hash on login/refresh. Default: `true`. */
|
|
40
|
+
refreshTokenRotation?: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface AuthJwtConfig {
|
|
43
|
+
secret: string;
|
|
44
|
+
expiresIn?: string;
|
|
45
|
+
refreshSecret: string;
|
|
46
|
+
refreshExpiresIn?: string;
|
|
47
|
+
}
|
|
48
|
+
interface AuthModuleOptions extends AuthJwtConfig {
|
|
49
|
+
/** Field used for login/register lookup. Default: `email`. */
|
|
50
|
+
identifierField?: AuthIdentifierField;
|
|
51
|
+
/** Optional feature flags — all off by default (POC-friendly). */
|
|
52
|
+
features?: AuthFeatures;
|
|
53
|
+
/** Email verification token TTL in ms. Default: 24h. */
|
|
54
|
+
emailVerificationTokenTtlMs?: number;
|
|
55
|
+
/** Password reset token TTL in ms. Default: 15min. */
|
|
56
|
+
passwordResetTokenTtlMs?: number;
|
|
57
|
+
/** Route prefix for auth controller. Default: `auth`. */
|
|
58
|
+
routePrefix?: string;
|
|
59
|
+
/** Custom hooks class. Defaults to built-in minimal payload. */
|
|
60
|
+
hooks?: new (...args: any[]) => AuthHooks;
|
|
61
|
+
}
|
|
62
|
+
interface AuthModuleAsyncOptions {
|
|
63
|
+
imports?: unknown[];
|
|
64
|
+
inject?: unknown[];
|
|
65
|
+
useFactory: (...args: unknown[]) => AuthModuleOptions | Promise<AuthModuleOptions>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface CreateAccountData extends RegisterInput {
|
|
69
|
+
passwordHash: string;
|
|
70
|
+
/** When `false`, account can login immediately. Default decided by AuthService. */
|
|
71
|
+
emailVerified?: boolean;
|
|
72
|
+
}
|
|
73
|
+
interface IAuthRepository<TAccount extends BaseAuthAccount = BaseAuthAccount> {
|
|
74
|
+
create(data: CreateAccountData): Promise<TAccount>;
|
|
75
|
+
findByIdentifier(identifier: string): Promise<TAccount | null>;
|
|
76
|
+
findByEmail(email: string): Promise<BaseAuthAccount | null>;
|
|
77
|
+
findByIdentifierWithSecrets(identifier: string): Promise<AuthAccountWithSecrets | null>;
|
|
78
|
+
findById(id: string): Promise<BaseAuthAccount | null>;
|
|
79
|
+
findByIdWithSecrets(id: string): Promise<AuthAccountWithSecrets | null>;
|
|
80
|
+
updateRefreshTokenHash(id: string, hash: string | null): Promise<void>;
|
|
81
|
+
updatePasswordHash(id: string, passwordHash: string): Promise<void>;
|
|
82
|
+
setEmailVerificationToken(id: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
83
|
+
findByEmailVerificationTokenHash(tokenHash: string): Promise<BaseAuthAccount | null>;
|
|
84
|
+
markEmailVerified(id: string): Promise<void>;
|
|
85
|
+
setResetToken(id: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
86
|
+
findByResetTokenHash(tokenHash: string): Promise<AuthAccountWithSecrets | null>;
|
|
87
|
+
clearResetToken(id: string): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type { AuthModuleOptions as A, BaseAuthAccount as B, CreateAccountData as C, IAuthRepository as I, RegisterInput as R, AuthModuleAsyncOptions as a, AuthHooks as b, AuthTokens as c, AuthAccountWithSecrets as d, AuthFeatures as e, AuthIdentifierField as f, AuthJwtConfig as g };
|