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.
- package/.prettierrc +4 -0
- package/Dockerfile.puppeteer +45 -0
- package/LICENSE +22 -0
- package/README.md +414 -0
- package/config.example.yaml +7 -0
- package/dist/app.module.d.ts +2 -0
- package/dist/app.module.js +53 -0
- package/dist/app.module.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +26 -0
- package/dist/main.js.map +1 -0
- package/dist/simple-user/aragami-init.d.ts +2 -0
- package/dist/simple-user/aragami-init.js +55 -0
- package/dist/simple-user/aragami-init.js.map +1 -0
- package/dist/simple-user/index.d.ts +6 -0
- package/dist/simple-user/index.js +23 -0
- package/dist/simple-user/index.js.map +1 -0
- package/dist/simple-user/login/login.controller.d.ts +14 -0
- package/dist/simple-user/login/login.controller.js +90 -0
- package/dist/simple-user/login/login.controller.js.map +1 -0
- package/dist/simple-user/module-builder.d.ts +2 -0
- package/dist/simple-user/module-builder.js +34 -0
- package/dist/simple-user/module-builder.js.map +1 -0
- package/dist/simple-user/options.d.ts +26 -0
- package/dist/simple-user/options.js +3 -0
- package/dist/simple-user/options.js.map +1 -0
- package/dist/simple-user/resolver.d.ts +41 -0
- package/dist/simple-user/resolver.js +48 -0
- package/dist/simple-user/resolver.js.map +1 -0
- package/dist/simple-user/send-code/code-context.d.ts +4 -0
- package/dist/simple-user/send-code/code-context.js +8 -0
- package/dist/simple-user/send-code/code-context.js.map +1 -0
- package/dist/simple-user/send-code/decorators.d.ts +2 -0
- package/dist/simple-user/send-code/decorators.js +13 -0
- package/dist/simple-user/send-code/decorators.js.map +1 -0
- package/dist/simple-user/send-code/send-code.controller.d.ts +9 -0
- package/dist/simple-user/send-code/send-code.controller.js +71 -0
- package/dist/simple-user/send-code/send-code.controller.js.map +1 -0
- package/dist/simple-user/send-code/send-code.dto.d.ts +12 -0
- package/dist/simple-user/send-code/send-code.dto.js +55 -0
- package/dist/simple-user/send-code/send-code.dto.js.map +1 -0
- package/dist/simple-user/send-code/send-code.service.d.ts +18 -0
- package/dist/simple-user/send-code/send-code.service.js +144 -0
- package/dist/simple-user/send-code/send-code.service.js.map +1 -0
- package/dist/simple-user/send-code/wait-time.dto.d.ts +3 -0
- package/dist/simple-user/send-code/wait-time.dto.js +29 -0
- package/dist/simple-user/send-code/wait-time.dto.js.map +1 -0
- package/dist/simple-user/simple-user/change-email.dto.d.ts +3 -0
- package/dist/simple-user/simple-user/change-email.dto.js +12 -0
- package/dist/simple-user/simple-user/change-email.dto.js.map +1 -0
- package/dist/simple-user/simple-user/change-password.dto.d.ts +4 -0
- package/dist/simple-user/simple-user/change-password.dto.js +41 -0
- package/dist/simple-user/simple-user/change-password.dto.js.map +1 -0
- package/dist/simple-user/simple-user/email.dto.d.ts +6 -0
- package/dist/simple-user/simple-user/email.dto.js +46 -0
- package/dist/simple-user/simple-user/email.dto.js.map +1 -0
- package/dist/simple-user/simple-user/login.dto.d.ts +11 -0
- package/dist/simple-user/simple-user/login.dto.js +80 -0
- package/dist/simple-user/simple-user/login.dto.js.map +1 -0
- package/dist/simple-user/simple-user/reset-password.dto.d.ts +4 -0
- package/dist/simple-user/simple-user/reset-password.dto.js +32 -0
- package/dist/simple-user/simple-user/reset-password.dto.js.map +1 -0
- package/dist/simple-user/simple-user/simple-user.service.d.ts +33 -0
- package/dist/simple-user/simple-user/simple-user.service.js +338 -0
- package/dist/simple-user/simple-user/simple-user.service.js.map +1 -0
- package/dist/simple-user/simple-user/user-exists.dto.d.ts +3 -0
- package/dist/simple-user/simple-user/user-exists.dto.js +28 -0
- package/dist/simple-user/simple-user/user-exists.dto.js.map +1 -0
- package/dist/simple-user/simple-user.entity.d.ts +40 -0
- package/dist/simple-user/simple-user.entity.js +119 -0
- package/dist/simple-user/simple-user.entity.js.map +1 -0
- package/dist/simple-user/simple-user.module.d.ts +8 -0
- package/dist/simple-user/simple-user.module.js +52 -0
- package/dist/simple-user/simple-user.module.js.map +1 -0
- package/dist/simple-user/tokens.d.ts +2 -0
- package/dist/simple-user/tokens.js +6 -0
- package/dist/simple-user/tokens.js.map +1 -0
- package/dist/simple-user/user-center/patch-me.d.ts +2 -0
- package/dist/simple-user/user-center/patch-me.js +16 -0
- package/dist/simple-user/user-center/patch-me.js.map +1 -0
- package/dist/simple-user/user-center/user-center.controller.d.ts +13 -0
- package/dist/simple-user/user-center/user-center.controller.js +88 -0
- package/dist/simple-user/user-center/user-center.controller.js.map +1 -0
- package/dist/simple-user/user-center/user-center.service.d.ts +2 -0
- package/dist/simple-user/user-center/user-center.service.js +17 -0
- package/dist/simple-user/user-center/user-center.service.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utility/load-config.d.ts +7 -0
- package/dist/utility/load-config.js +61 -0
- package/dist/utility/load-config.js.map +1 -0
- package/eslint.config.mjs +34 -0
- package/install-npm-typeorm.sh +3 -0
- package/nest-cli.json +7 -0
- package/package.json +98 -0
- package/test/app-e2e.spec.ts +242 -0
- package/test/jest-e2e.json +9 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +18 -0
package/.prettierrc
ADDED
|
@@ -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,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
|
package/dist/main.js.map
ADDED
|
@@ -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,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"}
|