nicot-simple-user 1.0.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.
Files changed (98) hide show
  1. package/.prettierrc +4 -0
  2. package/Dockerfile.puppeteer +45 -0
  3. package/LICENSE +22 -0
  4. package/README.md +414 -0
  5. package/config.example.yaml +7 -0
  6. package/dist/app.module.d.ts +2 -0
  7. package/dist/app.module.js +53 -0
  8. package/dist/app.module.js.map +1 -0
  9. package/dist/main.d.ts +1 -0
  10. package/dist/main.js +26 -0
  11. package/dist/main.js.map +1 -0
  12. package/dist/simple-user/aragami-init.d.ts +2 -0
  13. package/dist/simple-user/aragami-init.js +55 -0
  14. package/dist/simple-user/aragami-init.js.map +1 -0
  15. package/dist/simple-user/index.d.ts +6 -0
  16. package/dist/simple-user/index.js +23 -0
  17. package/dist/simple-user/index.js.map +1 -0
  18. package/dist/simple-user/login/login.controller.d.ts +14 -0
  19. package/dist/simple-user/login/login.controller.js +90 -0
  20. package/dist/simple-user/login/login.controller.js.map +1 -0
  21. package/dist/simple-user/module-builder.d.ts +2 -0
  22. package/dist/simple-user/module-builder.js +34 -0
  23. package/dist/simple-user/module-builder.js.map +1 -0
  24. package/dist/simple-user/options.d.ts +26 -0
  25. package/dist/simple-user/options.js +3 -0
  26. package/dist/simple-user/options.js.map +1 -0
  27. package/dist/simple-user/resolver.d.ts +41 -0
  28. package/dist/simple-user/resolver.js +48 -0
  29. package/dist/simple-user/resolver.js.map +1 -0
  30. package/dist/simple-user/send-code/code-context.d.ts +4 -0
  31. package/dist/simple-user/send-code/code-context.js +8 -0
  32. package/dist/simple-user/send-code/code-context.js.map +1 -0
  33. package/dist/simple-user/send-code/decorators.d.ts +2 -0
  34. package/dist/simple-user/send-code/decorators.js +13 -0
  35. package/dist/simple-user/send-code/decorators.js.map +1 -0
  36. package/dist/simple-user/send-code/send-code.controller.d.ts +9 -0
  37. package/dist/simple-user/send-code/send-code.controller.js +71 -0
  38. package/dist/simple-user/send-code/send-code.controller.js.map +1 -0
  39. package/dist/simple-user/send-code/send-code.dto.d.ts +12 -0
  40. package/dist/simple-user/send-code/send-code.dto.js +55 -0
  41. package/dist/simple-user/send-code/send-code.dto.js.map +1 -0
  42. package/dist/simple-user/send-code/send-code.service.d.ts +18 -0
  43. package/dist/simple-user/send-code/send-code.service.js +144 -0
  44. package/dist/simple-user/send-code/send-code.service.js.map +1 -0
  45. package/dist/simple-user/send-code/wait-time.dto.d.ts +3 -0
  46. package/dist/simple-user/send-code/wait-time.dto.js +29 -0
  47. package/dist/simple-user/send-code/wait-time.dto.js.map +1 -0
  48. package/dist/simple-user/simple-user/change-email.dto.d.ts +3 -0
  49. package/dist/simple-user/simple-user/change-email.dto.js +12 -0
  50. package/dist/simple-user/simple-user/change-email.dto.js.map +1 -0
  51. package/dist/simple-user/simple-user/change-password.dto.d.ts +4 -0
  52. package/dist/simple-user/simple-user/change-password.dto.js +41 -0
  53. package/dist/simple-user/simple-user/change-password.dto.js.map +1 -0
  54. package/dist/simple-user/simple-user/email.dto.d.ts +6 -0
  55. package/dist/simple-user/simple-user/email.dto.js +46 -0
  56. package/dist/simple-user/simple-user/email.dto.js.map +1 -0
  57. package/dist/simple-user/simple-user/login.dto.d.ts +11 -0
  58. package/dist/simple-user/simple-user/login.dto.js +80 -0
  59. package/dist/simple-user/simple-user/login.dto.js.map +1 -0
  60. package/dist/simple-user/simple-user/reset-password.dto.d.ts +4 -0
  61. package/dist/simple-user/simple-user/reset-password.dto.js +32 -0
  62. package/dist/simple-user/simple-user/reset-password.dto.js.map +1 -0
  63. package/dist/simple-user/simple-user/simple-user.service.d.ts +33 -0
  64. package/dist/simple-user/simple-user/simple-user.service.js +338 -0
  65. package/dist/simple-user/simple-user/simple-user.service.js.map +1 -0
  66. package/dist/simple-user/simple-user/user-exists.dto.d.ts +3 -0
  67. package/dist/simple-user/simple-user/user-exists.dto.js +28 -0
  68. package/dist/simple-user/simple-user/user-exists.dto.js.map +1 -0
  69. package/dist/simple-user/simple-user.entity.d.ts +40 -0
  70. package/dist/simple-user/simple-user.entity.js +119 -0
  71. package/dist/simple-user/simple-user.entity.js.map +1 -0
  72. package/dist/simple-user/simple-user.module.d.ts +8 -0
  73. package/dist/simple-user/simple-user.module.js +52 -0
  74. package/dist/simple-user/simple-user.module.js.map +1 -0
  75. package/dist/simple-user/tokens.d.ts +2 -0
  76. package/dist/simple-user/tokens.js +6 -0
  77. package/dist/simple-user/tokens.js.map +1 -0
  78. package/dist/simple-user/user-center/patch-me.d.ts +2 -0
  79. package/dist/simple-user/user-center/patch-me.js +16 -0
  80. package/dist/simple-user/user-center/patch-me.js.map +1 -0
  81. package/dist/simple-user/user-center/user-center.controller.d.ts +13 -0
  82. package/dist/simple-user/user-center/user-center.controller.js +88 -0
  83. package/dist/simple-user/user-center/user-center.controller.js.map +1 -0
  84. package/dist/simple-user/user-center/user-center.service.d.ts +2 -0
  85. package/dist/simple-user/user-center/user-center.service.js +17 -0
  86. package/dist/simple-user/user-center/user-center.service.js.map +1 -0
  87. package/dist/tsconfig.build.tsbuildinfo +1 -0
  88. package/dist/utility/load-config.d.ts +7 -0
  89. package/dist/utility/load-config.js +61 -0
  90. package/dist/utility/load-config.js.map +1 -0
  91. package/eslint.config.mjs +34 -0
  92. package/install-npm-typeorm.sh +3 -0
  93. package/nest-cli.json +7 -0
  94. package/package.json +98 -0
  95. package/test/app-e2e.spec.ts +242 -0
  96. package/test/jest-e2e.json +9 -0
  97. package/tsconfig.build.json +4 -0
  98. package/tsconfig.json +18 -0
package/.prettierrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all"
4
+ }
@@ -0,0 +1,45 @@
1
+ FROM node:lts-trixie-slim as base
2
+ LABEL Author="Nanahira <nanahira@momobako.com>"
3
+
4
+ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
5
+ ENV DEBIAN_FRONTEND=noninteractive
6
+
7
+ RUN set -eux; \
8
+ apt-get update; \
9
+ apt-get install -y --no-install-recommends curl ca-certificates gnupg; \
10
+ install -d -m 0755 /etc/apt/keyrings; \
11
+ curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
12
+ | gpg --dearmor -o /etc/apt/keyrings/google-linux.gpg; \
13
+ chmod a+r /etc/apt/keyrings/google-linux.gpg; \
14
+ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-linux.gpg] https://dl.google.com/linux/chrome/deb stable main" \
15
+ > /etc/apt/sources.list.d/google-chrome.list; \
16
+ apt-get update; \
17
+ apt-get install -y --no-install-recommends \
18
+ python3 build-essential git \
19
+ google-chrome-stable \
20
+ libnss3 libfreetype6-dev libharfbuzz-bin libharfbuzz-dev \
21
+ fonts-freefont-otf fonts-freefont-ttf \
22
+ fonts-noto-cjk fonts-noto-cjk-extra \
23
+ fonts-wqy-microhei fonts-wqy-zenhei \
24
+ xvfb libpq-dev; \
25
+ apt-get purge -y --auto-remove gnupg; \
26
+ apt-get clean; \
27
+ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/*
28
+
29
+ WORKDIR /usr/src/app
30
+ COPY ./package*.json ./
31
+
32
+ FROM base as builder
33
+ RUN npm ci && npm cache clean --force
34
+ COPY . ./
35
+ RUN npm run build
36
+
37
+ FROM base
38
+ ENV NODE_ENV production
39
+ RUN npm ci && npm i pg-native && npm cache clean --force
40
+ COPY --from=builder /usr/src/app/dist ./dist
41
+ COPY ./config.example.yaml ./config.yaml
42
+
43
+ ENV NODE_PG_FORCE_NATIVE=true
44
+ EXPOSE 3000
45
+ CMD [ "npm", "run", "start:prod" ]
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Nanahira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,414 @@
1
+ # nicot-simple-user
2
+
3
+ `nicot-simple-user` is a configurable NestJS feature module that provides a complete “simple user system”:
4
+
5
+ - Anonymous users (keyed by `x-client-ssaid`)
6
+ - Email verification codes (send + verify)
7
+ - Login with code or password (auto-create on first code login)
8
+ - Server-side sessions (Redis/Aragami), revocable instantly (no JWT)
9
+ - Built-in risk control (cooldown, attempt limits, temporary blocks)
10
+ - Swagger/OpenAPI schemas patched dynamically based on your configured `userClass`
11
+
12
+ This is a **library module** meant to be imported into an existing NestJS application.
13
+
14
+ It is built on top of **nicot** and follows nicot’s entity/DTO conventions.
15
+ nicot (npm): https://www.npmjs.com/package/nicot
16
+
17
+ ---
18
+
19
+ ## Peer Dependencies
20
+
21
+ `nicot-simple-user` expects the following peer dependencies:
22
+
23
+ ```json
24
+ "peerDependencies": {
25
+ "@nestjs/common": "^11.0.1",
26
+ "@nestjs/core": "^11.0.1",
27
+ "@nestjs/swagger": "^11.2.3",
28
+ "@nestjs/typeorm": "^11.0.0",
29
+ "class-transformer": "^0.5.1",
30
+ "class-validator": "^0.14.3",
31
+ "nicot": "^1.3.1",
32
+ "typeorm": "^0.3.28"
33
+ }
34
+ ```
35
+
36
+ > Most NestJS apps already have `@nestjs/common` and `@nestjs/core` installed.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ Install `nicot-simple-user` plus its peer dependencies (no version pin needed here):
43
+
44
+ ```shell
45
+ # pnpm
46
+ pnpm add nicot-simple-user \
47
+ @nestjs/swagger @nestjs/typeorm \
48
+ class-transformer class-validator \
49
+ nicot typeorm
50
+
51
+ # npm
52
+ npm i nicot-simple-user \
53
+ @nestjs/swagger @nestjs/typeorm \
54
+ class-transformer class-validator \
55
+ nicot typeorm
56
+
57
+ # yarn
58
+ yarn add nicot-simple-user \
59
+ @nestjs/swagger @nestjs/typeorm \
60
+ class-transformer class-validator \
61
+ nicot typeorm
62
+ ```
63
+
64
+ You also need:
65
+ - a working TypeORM setup in your Nest app
66
+ - a Redis-compatible backend for Aragami sessions/risk-control (usually Redis)
67
+
68
+ ---
69
+
70
+ ## Quick Start (Recommended: `registerAsync`)
71
+
72
+ ### Why `registerAsync`?
73
+
74
+ `nicot-simple-user` supports dynamic module configuration. The recommended approach is to use `registerAsync` so you can:
75
+
76
+ - read config from `@nestjs/config`
77
+ - import your own delivery module (SMTP/SMS/etc.)
78
+ - inject dependencies into `sendCodeGenerator`
79
+
80
+ > API note: `register()` and `registerAsync()` each take **one parameter only**.
81
+ >
82
+ > - `register()` takes a single object where **options + extras are merged**.
83
+ > - `registerAsync()` takes a single object that contains **extras + async options factory**.
84
+
85
+ This README focuses on `registerAsync()`.
86
+
87
+ ---
88
+
89
+ ## Example: register with `@nestjs/config` + an SMTP module
90
+
91
+ Below is an example that:
92
+ - imports `ConfigModule` / `ConfigService`
93
+ - imports a hypothetical `SmtpModule` (your own module)
94
+ - generates and sends email codes via an injected `SmtpService`
95
+
96
+ ```ts
97
+ import { Module } from '@nestjs/common'
98
+ import { ConfigModule, ConfigService } from '@nestjs/config'
99
+ import { TypeOrmModule } from '@nestjs/typeorm'
100
+ import { SimpleUserModule } from 'nicot-simple-user'
101
+ import { SendCodeDto } from 'nicot-simple-user/send-code/send-code.dto'
102
+
103
+ // Your own modules (examples)
104
+ import { SmtpModule } from './smtp/smtp.module'
105
+ import { SmtpService } from './smtp/smtp.service'
106
+
107
+ // Optional: your custom user entity (see "Custom userClass" section)
108
+ import { AppUser } from './entities/app-user.entity'
109
+
110
+ @Module({
111
+ imports: [
112
+ ConfigModule.forRoot({ isGlobal: true }),
113
+
114
+ TypeOrmModule.forRoot({
115
+ // ... your DB config
116
+ // entities: [AppUser, ...]
117
+ }),
118
+
119
+ // The module responsible for actually delivering the code
120
+ SmtpModule.registerAsync({
121
+ imports: [ConfigModule],
122
+ inject: [ConfigService],
123
+ useFactory: async (config: ConfigService) => ({
124
+ host: config.getOrThrow<string>('SMTP_HOST'),
125
+ user: config.getOrThrow<string>('SMTP_USER'),
126
+ pass: config.getOrThrow<string>('SMTP_PASS'),
127
+ }),
128
+ }),
129
+
130
+ // nicot-simple-user
131
+ SimpleUserModule.registerAsync({
132
+ // ---- extras (structural) ----
133
+ userClass: AppUser, // optional, defaults to SimpleUser
134
+ userConnectionName: 'default', // optional
135
+ userServiceCrudExtras: { relations: [] }, // optional: affects /me OpenAPI schema
136
+ isGlobal: false, // optional
137
+
138
+ // ---- async options ----
139
+ imports: [ConfigModule, SmtpModule],
140
+ inject: [ConfigService, SmtpService],
141
+ useFactory: async (config: ConfigService, smtp: SmtpService) => ({
142
+ redisUrl: config.getOrThrow<string>('REDIS_URL'),
143
+
144
+ // REQUIRED: generate + deliver the code, then return it for storage & verification
145
+ sendCodeGenerator: async (ctx: SendCodeDto) => {
146
+ const code = String(Math.floor(100000 + Math.random() * 900000))
147
+
148
+ await smtp.sendMail({
149
+ to: ctx.email,
150
+ subject: `Your verification code (${ctx.codePurpose})`,
151
+ text: `Your verification code is: ${code}`,
152
+ })
153
+
154
+ return code
155
+ },
156
+
157
+ // optional behavior tuning
158
+ allowAnonymousUsers: true,
159
+ loginExpiryTimeMs: 30 * 24 * 60 * 60 * 1000,
160
+ sendCodeValidTimeMs: 10 * 60 * 1000,
161
+ sendCodeCooldownTimeMs: 60 * 1000,
162
+ verifyCodeMaxAttempts: 5,
163
+ verifyCodeBlockTimeMs: 15 * 60 * 1000,
164
+ passwordMaxAttempts: 5,
165
+ passwordBlockTimeMs: 15 * 60 * 1000,
166
+ }),
167
+ }),
168
+ ],
169
+ })
170
+ export class AppModule {}
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Request Headers
176
+
177
+ Clients should send the following headers:
178
+
179
+ - `x-client-ssaid` (**required** in most endpoints): a stable client session identifier
180
+ - `x-client-token` (optional): auth token for logged-in users
181
+
182
+ ### About `x-client-ssaid`
183
+
184
+ `x-client-ssaid` is how the module identifies a client session/device. It is required even for anonymous users.
185
+
186
+ - Generate it once on the client and persist it (localStorage/cookie/device storage).
187
+ - Use a stable random string (UUID/ULID/NanoID are all acceptable).
188
+ - Treat it like a session identifier, not a secret.
189
+
190
+ ---
191
+
192
+ ## Custom `userClass` (nicot entity)
193
+
194
+ By default, the module uses the built-in `SimpleUser` entity.
195
+ If your app needs extra fields, you can extend `SimpleUser` and pass it as `userClass`.
196
+
197
+ **Important:** your extended user class should be a **nicot entity** (not a plain TypeORM-only entity), so nicot decorators can control API output.
198
+
199
+ ### Example: extend `SimpleUser` with nicot decorators
200
+
201
+ ```ts
202
+ import { Entity, Index } from 'typeorm'
203
+ import { SimpleUser } from 'nicot-simple-user/simple-user.entity'
204
+ import { StringColumn, NotInResult } from 'nicot'
205
+
206
+ @Entity()
207
+ export class AppUser extends SimpleUser {
208
+ @Index()
209
+ @StringColumn(64, { nullable: true, description: 'User nickname' })
210
+ nickname?: string
211
+
212
+ // This field will be excluded from nicot result DTOs
213
+ @NotInResult()
214
+ @StringColumn(255, { nullable: true, description: 'Internal-only field' })
215
+ internalNote?: string
216
+ }
217
+ ```
218
+
219
+ ### Why nicot decorators matter for `/me`
220
+
221
+ The `/api/user-center/me` endpoint uses a nicot-generated DTO (via `RestfulFactory`) based on your configured `userClass`.
222
+ That means fields marked with nicot’s `@NotInResult()` are **trimmed** from:
223
+
224
+ - the `/me` OpenAPI schema
225
+ - the `/me` response output
226
+
227
+ So you can safely keep internal-only columns without exposing them through `/me`.
228
+
229
+ ---
230
+
231
+ ## API Overview (HTTP)
232
+
233
+ All endpoints return a standard envelope:
234
+
235
+ - `statusCode`
236
+ - `message`
237
+ - `success`
238
+ - `timestamp`
239
+ - optional `data`
240
+
241
+ ### Endpoints
242
+
243
+ #### Send verification code
244
+
245
+ **POST** `/api/send-code/send`
246
+
247
+ - Headers: `x-client-ssaid`
248
+ - Body: `{ email, codePurpose }`
249
+ - 200 success
250
+ - 429 cooldown hit (returns `data.waitTimeMs`)
251
+
252
+ `codePurpose` values:
253
+ - `login`
254
+ - `ResetPassword`
255
+ - `ChangeEmail`
256
+
257
+ ---
258
+
259
+ #### Verify a code
260
+
261
+ **GET** `/api/send-code/verify?email=...&codePurpose=...&code=...`
262
+
263
+ - 200 success
264
+ - 403 invalid code
265
+ - 429 too many invalid attempts (returns `data.waitTimeMs`)
266
+
267
+ By default, successful verification consumes the code.
268
+
269
+ ---
270
+
271
+ #### Check if a user exists by email
272
+
273
+ **GET** `/api/login/user-exists?email=...`
274
+
275
+ Returns:
276
+ - `data.exists: boolean`
277
+
278
+ ---
279
+
280
+ #### Login (code or password)
281
+
282
+ **POST** `/api/login`
283
+
284
+ - Headers: `x-client-ssaid`
285
+ - Body:
286
+ - `{ email, code }` for code login
287
+ - `{ email, password }` for password login
288
+ - `{ email, code, setPassword }` to set password on first creation (optional)
289
+
290
+ Returns:
291
+ - `data.token` (64-char opaque string)
292
+ - `data.tokenExpiresAt`
293
+ - `data.userId`
294
+
295
+ Notes:
296
+ - Existing user:
297
+ - code login verifies code
298
+ - password login verifies password (with risk control)
299
+ - New user:
300
+ - requires `code`
301
+ - upgrades the anonymous user associated with `x-client-ssaid`
302
+ - optional `setPassword` sets a password during creation
303
+
304
+ ---
305
+
306
+ #### Get current user
307
+
308
+ **GET** `/api/user-center/me`
309
+
310
+ - Headers: `x-client-ssaid`
311
+ - Headers: `x-client-token` (optional; used to resolve logged-in user)
312
+
313
+ Behavior:
314
+ - With `allowAnonymousUsers=true` (default), missing token may still resolve to an anonymous user record.
315
+ - With `allowAnonymousUsers=false`, missing/invalid token results in 401.
316
+
317
+ ---
318
+
319
+ #### Change password
320
+
321
+ **POST** `/api/user-center/change-password`
322
+
323
+ - Headers: `x-client-ssaid`
324
+ - Headers: `x-client-token`
325
+ - Body: `{ newPassword, currentPassword? }`
326
+
327
+ Rules:
328
+ - If a password already exists, `currentPassword` must be correct.
329
+ - On success, all sessions for this user email are revoked.
330
+
331
+ ---
332
+
333
+ #### Change email
334
+
335
+ **POST** `/api/user-center/change-email`
336
+
337
+ - Headers: `x-client-ssaid`
338
+ - Headers: `x-client-token`
339
+ - Body: `{ email, code }` (code must be for `ChangeEmail`)
340
+
341
+ ---
342
+
343
+ #### Reset password
344
+
345
+ **POST** `/api/login/reset-password`
346
+
347
+ - Body: `{ email, code, newPassword }` (code must be for `ResetPassword`)
348
+
349
+ On success:
350
+ - password hash is updated
351
+ - all sessions for that email are revoked
352
+
353
+ ---
354
+
355
+ ## Risk Control Behavior
356
+
357
+ ### Send-code cooldown (429)
358
+
359
+ Cooldown is enforced across multiple dimensions:
360
+
361
+ - `email + purpose`
362
+ - `ip + purpose`
363
+ - `ssaid + purpose`
364
+
365
+ If any dimension is in cooldown, the API returns:
366
+
367
+ - 429
368
+ - `data.waitTimeMs`: milliseconds until retry is allowed
369
+
370
+ ---
371
+
372
+ ### Verify-code invalid attempt blocking (429)
373
+
374
+ Invalid verification attempts are blocked after:
375
+
376
+ - `verifyCodeMaxAttempts` (default 5) within
377
+ - `verifyCodeBlockTimeMs` (default 15 minutes)
378
+
379
+ Successful verification clears the failure records.
380
+
381
+ ---
382
+
383
+ ### Password attempt blocking (429)
384
+
385
+ Password failures are tracked across:
386
+
387
+ - `userId`
388
+ - `ssaid`
389
+ - `ip`
390
+
391
+ and blocked after `passwordMaxAttempts` within `passwordBlockTimeMs`.
392
+
393
+ ---
394
+
395
+ ## Testing Notes
396
+
397
+ For deterministic tests, configure `sendCodeGenerator` to always return `123456`:
398
+
399
+ ```ts
400
+ SimpleUserModule.registerAsync({
401
+ imports: [],
402
+ inject: [],
403
+ useFactory: async () => ({
404
+ redisUrl: process.env.REDIS_URL!,
405
+ sendCodeGenerator: async () => '123456',
406
+ }),
407
+ })
408
+ ```
409
+
410
+ ---
411
+
412
+ ## License
413
+
414
+ MIT
@@ -0,0 +1,7 @@
1
+ host: '::'
2
+ port: 3000
3
+ DB_HOST: '127.0.0.1'
4
+ DB_PORT: 5432
5
+ DB_USER: 'postgres'
6
+ DB_PASS: 'postgres'
7
+ DB_NAME: 'postgres'
@@ -0,0 +1,2 @@
1
+ export declare class AppModule {
2
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.AppModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const config_1 = require("@nestjs/config");
12
+ const load_config_1 = require("./utility/load-config");
13
+ const typeorm_1 = require("@nestjs/typeorm");
14
+ const simple_user_module_1 = require("./simple-user/simple-user.module");
15
+ let AppModule = class AppModule {
16
+ };
17
+ exports.AppModule = AppModule;
18
+ exports.AppModule = AppModule = __decorate([
19
+ (0, common_1.Module)({
20
+ imports: [
21
+ config_1.ConfigModule.forRoot({
22
+ load: [load_config_1.loadConfig],
23
+ isGlobal: true,
24
+ ignoreEnvVars: true,
25
+ ignoreEnvFile: true,
26
+ }),
27
+ typeorm_1.TypeOrmModule.forRootAsync({
28
+ inject: [config_1.ConfigService],
29
+ useFactory: async (config) => ({
30
+ type: 'postgres',
31
+ entities: [],
32
+ autoLoadEntities: true,
33
+ dropSchema: !!config.get('DB_DROP_SCHEMA'),
34
+ synchronize: !config.get('DB_NO_INIT'),
35
+ host: config.get('DB_HOST'),
36
+ port: parseInt(config.get('DB_PORT')) || 5432,
37
+ username: config.get('DB_USER'),
38
+ password: config.get('DB_PASS'),
39
+ database: config.get('DB_NAME'),
40
+ supportBigNumbers: true,
41
+ bigNumberStrings: false,
42
+ }),
43
+ }),
44
+ simple_user_module_1.SimpleUserModule.register({
45
+ sendCodeGenerator: (ctx) => {
46
+ console.log(`Generating code for ${ctx.email} on ${ctx.codePurpose}`);
47
+ return '123456';
48
+ },
49
+ }),
50
+ ],
51
+ })
52
+ ], AppModule);
53
+ //# sourceMappingURL=app.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2CAA6D;AAC7D,uDAAmD;AACnD,6CAAgD;AAChD,yEAAoE;AAmC7D,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAjCrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,IAAI,EAAE,CAAC,wBAAU,CAAC;gBAClB,QAAQ,EAAE,IAAI;gBACd,aAAa,EAAE,IAAI;gBACnB,aAAa,EAAE,IAAI;aACpB,CAAC;YACF,uBAAa,CAAC,YAAY,CAAC;gBACzB,MAAM,EAAE,CAAC,sBAAa,CAAC;gBACvB,UAAU,EAAE,KAAK,EAAE,MAAqB,EAAE,EAAE,CAAC,CAAC;oBAC5C,IAAI,EAAE,UAAU;oBAChB,QAAQ,EAAE,EAAE;oBACZ,gBAAgB,EAAE,IAAI;oBACtB,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC;oBAC1C,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC;oBACtC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;oBAC3B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,IAAI;oBAC7C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;oBAC/B,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;oBAC/B,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;oBAC/B,iBAAiB,EAAE,IAAI;oBACvB,gBAAgB,EAAE,KAAK;iBACxB,CAAC;aACH,CAAC;YACF,qCAAgB,CAAC,QAAQ,CAAC;gBACxB,iBAAiB,EAAE,CAAC,GAAG,EAAE,EAAE;oBACzB,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,CAAC,KAAK,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;oBACtE,OAAO,QAAQ,CAAC;gBAClB,CAAC;aACF,CAAC;SACH;KACF,CAAC;GACW,SAAS,CAAG"}
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@nestjs/core");
4
+ const swagger_1 = require("@nestjs/swagger");
5
+ const app_module_1 = require("./app.module");
6
+ const config_1 = require("@nestjs/config");
7
+ async function bootstrap() {
8
+ const app = await core_1.NestFactory.create(app_module_1.AppModule);
9
+ app.setGlobalPrefix('api');
10
+ app.enableCors();
11
+ app.set('trust proxy', ['172.16.0.0/12', 'loopback']);
12
+ app.enableShutdownHooks();
13
+ const config = app.get(config_1.ConfigService);
14
+ if (!config.get('NO_OPENAPI')) {
15
+ const documentConfig = new swagger_1.DocumentBuilder()
16
+ .setTitle('app')
17
+ .setDescription('The app')
18
+ .setVersion('1.0')
19
+ .build();
20
+ const document = swagger_1.SwaggerModule.createDocument(app, documentConfig);
21
+ swagger_1.SwaggerModule.setup('docs', app, document);
22
+ }
23
+ await app.listen(config.get('port') || 3000, config.get('host') || '::');
24
+ }
25
+ bootstrap();
26
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAiE;AAEjE,6CAAyC;AACzC,2CAA+C;AAE/C,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAyB,sBAAS,CAAC,CAAC;IACxE,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC3B,GAAG,CAAC,UAAU,EAAE,CAAC;IACjB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC,CAAC;IACtD,GAAG,CAAC,mBAAmB,EAAE,CAAC;IAE1B,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,sBAAa,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9B,MAAM,cAAc,GAAG,IAAI,yBAAe,EAAE;aACzC,QAAQ,CAAC,KAAK,CAAC;aACf,cAAc,CAAC,SAAS,CAAC;aACzB,UAAU,CAAC,KAAK,CAAC;aACjB,KAAK,EAAE,CAAC;QAEX,MAAM,QAAQ,GAAG,uBAAa,CAAC,cAAc,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QACnE,uBAAa,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,GAAG,CAAC,MAAM,CACd,MAAM,CAAC,GAAG,CAAS,MAAM,CAAC,IAAI,IAAI,EAClC,MAAM,CAAC,GAAG,CAAS,MAAM,CAAC,IAAI,IAAI,CACnC,CAAC;AACJ,CAAC;AACD,SAAS,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ export declare function attachAragamiWithBridge(base: DynamicModule): DynamicModule;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.attachAragamiWithBridge = attachAragamiWithBridge;
10
+ const common_1 = require("@nestjs/common");
11
+ const nicot_1 = require("nicot");
12
+ const module_builder_1 = require("./module-builder");
13
+ const nestjs_aragami_1 = require("nestjs-aragami");
14
+ let SimpleUserAragamiBridgeModule = class SimpleUserAragamiBridgeModule {
15
+ };
16
+ SimpleUserAragamiBridgeModule = __decorate([
17
+ (0, common_1.Module)({})
18
+ ], SimpleUserAragamiBridgeModule);
19
+ function deriveAragamiOptions(o) {
20
+ return {
21
+ ...(o.redisUrl ? { redis: { uri: o.redisUrl } } : {}),
22
+ ...(o.aragamiExtras || {}),
23
+ };
24
+ }
25
+ const ARAGAMI_OPTIONS = Symbol('ARAGAMI_OPTIONS');
26
+ function attachAragamiWithBridge(base) {
27
+ const baseImports = base.imports ?? [];
28
+ const baseProviders = base.providers ?? [];
29
+ const bridge = {
30
+ module: SimpleUserAragamiBridgeModule,
31
+ imports: baseImports,
32
+ providers: [
33
+ ...baseProviders,
34
+ (0, nicot_1.createProvider)({
35
+ provide: ARAGAMI_OPTIONS,
36
+ inject: [module_builder_1.MODULE_OPTIONS_TOKEN],
37
+ }, (o) => deriveAragamiOptions(o)),
38
+ ],
39
+ exports: [ARAGAMI_OPTIONS],
40
+ };
41
+ return {
42
+ ...base,
43
+ imports: [
44
+ ...baseImports,
45
+ bridge,
46
+ nestjs_aragami_1.AragamiModule.registerAsync({
47
+ imports: [bridge],
48
+ inject: [ARAGAMI_OPTIONS],
49
+ useFactory: (aragamiOptions) => aragamiOptions,
50
+ }),
51
+ ],
52
+ exports: [...(base.exports || []), nestjs_aragami_1.AragamiModule],
53
+ };
54
+ }
55
+ //# sourceMappingURL=aragami-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aragami-init.js","sourceRoot":"","sources":["../../src/simple-user/aragami-init.ts"],"names":[],"mappings":";;;;;;;;AAmBA,0DAiCC;AApDD,2CAAuD;AAGvD,iCAAuC;AACvC,qDAAwD;AACxD,mDAA+C;AAG/C,IAAM,6BAA6B,GAAnC,MAAM,6BAA6B;CAAG,CAAA;AAAhC,6BAA6B;IADlC,IAAA,eAAM,EAAC,EAAE,CAAC;GACL,6BAA6B,CAAG;AAEtC,SAAS,oBAAoB,CAAC,CAAoB;IAChD,OAAO;QACL,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC;KAC3B,CAAC;AACJ,CAAC;AAED,MAAM,eAAe,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAElD,SAAgB,uBAAuB,CAAC,IAAmB;IACzD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;IACvC,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;IAE3C,MAAM,MAAM,GAAkB;QAC5B,MAAM,EAAE,6BAA6B;QACrC,OAAO,EAAE,WAAW;QACpB,SAAS,EAAE;YACT,GAAG,aAAa;YAChB,IAAA,sBAAc,EACZ;gBACE,OAAO,EAAE,eAAe;gBACxB,MAAM,EAAE,CAAC,qCAAoB,CAAC;aAC/B,EACD,CAAC,CAAoB,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAClD;SACF;QACD,OAAO,EAAE,CAAC,eAAe,CAAC;KAC3B,CAAC;IAEF,OAAO;QACL,GAAG,IAAI;QACP,OAAO,EAAE;YACP,GAAG,WAAW;YACd,MAAM;YACN,8BAAa,CAAC,aAAa,CAAC;gBAC1B,OAAO,EAAE,CAAC,MAAM,CAAC;gBACjB,MAAM,EAAE,CAAC,eAAe,CAAC;gBACzB,UAAU,EAAE,CAAC,cAA8B,EAAE,EAAE,CAAC,cAAc;aAC/D,CAAC;SACH;QACD,OAAO,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,8BAAa,CAAC;KAClD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ export * from './options';
2
+ export * from './simple-user.module';
3
+ export * from './simple-user/simple-user.service';
4
+ export * from './resolver';
5
+ export * from './simple-user.entity';
6
+ export * from 'aragami';