nicot-simple-user 1.0.1 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nicot-simple-user",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Simple user module based on NICOT",
5
5
  "author": "Nanahira <nanahira@momobako.com>",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "@nestjs/typeorm": "^11.0.0",
42
42
  "class-transformer": "^0.5.1",
43
43
  "class-validator": "^0.14.3",
44
- "nicot": "^1.3.1",
44
+ "nicot": "^1.3.4",
45
45
  "typeorm": "^0.3.28"
46
46
  },
47
47
  "devDependencies": {
@@ -63,7 +63,7 @@
63
63
  "eslint-plugin-prettier": "^5.2.2",
64
64
  "globals": "^16.0.0",
65
65
  "jest": "^29.7.0",
66
- "nesties": "^1.1.27",
66
+ "nesties": "^1.1.29",
67
67
  "pg": "^8.16.3",
68
68
  "prettier": "^3.4.2",
69
69
  "reflect-metadata": "^0.2.2",
@@ -2,27 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing';
2
2
  import { INestApplication } from '@nestjs/common';
3
3
  import request from 'supertest';
4
4
  import { AppModule } from './../src/app.module';
5
-
6
- type BlankReturnMessageDto = {
7
- statusCode: number;
8
- message: string;
9
- success: boolean;
10
- timestamp: string;
11
- };
12
-
13
- type ReturnMessage<T> = BlankReturnMessageDto & { data?: T };
14
-
15
- type LoginResponseDto = {
16
- token: string;
17
- tokenExpiresAt: string;
18
- userId: number;
19
- };
20
-
21
- type SimpleUserResultDto = {
22
- id: number;
23
- email?: string;
24
- passwordSet: boolean;
25
- };
5
+ import { AppUser } from '../src/app-user.entity';
6
+ import { GenericReturnMessageDto } from 'nicot';
7
+ import { LoginResponseDto } from '../src/simple-user/simple-user/login.dto';
26
8
 
27
9
  describe('SimpleUserModule (e2e)', () => {
28
10
  let app: INestApplication;
@@ -45,6 +27,31 @@ describe('SimpleUserModule (e2e)', () => {
45
27
 
46
28
  let token: string | undefined;
47
29
 
30
+ type ArticleCreateResult = {
31
+ id: number;
32
+ title: string;
33
+ content: string;
34
+ userId: number;
35
+ };
36
+
37
+ type ArticleResult = ArticleCreateResult & {
38
+ user?: {
39
+ id: number;
40
+ email?: string;
41
+ passwordSet: boolean;
42
+ age: number;
43
+ };
44
+ };
45
+
46
+ let userId: number | undefined;
47
+ let currentEmail = email1;
48
+
49
+ let articleId: number | undefined;
50
+ const articleTitle1 = `Hello_${rand}`;
51
+ const articleTitle2 = `Hello2_${rand}`;
52
+ const articleContent1 = `Content_${rand}`;
53
+ const articleContent2 = `Content2_${rand}`;
54
+
48
55
  beforeAll(async () => {
49
56
  const moduleFixture: TestingModule = await Test.createTestingModule({
50
57
  imports: [AppModule],
@@ -114,13 +121,14 @@ describe('SimpleUserModule (e2e)', () => {
114
121
  .expect(200);
115
122
 
116
123
  expectOkEnvelope(res.body);
117
- const data = (res.body as ReturnMessage<LoginResponseDto>).data;
124
+ const data = (res.body as GenericReturnMessageDto<LoginResponseDto>).data;
118
125
  expect(data).toBeDefined();
119
126
  expect(typeof data.userId).toBe('number');
120
127
  expect(typeof data.token).toBe('string');
121
128
  expect(data.token).toHaveLength(64);
122
129
 
123
130
  token = data.token;
131
+ userId = data.userId;
124
132
  });
125
133
 
126
134
  it('GET /user-center/me -> should return current user info', async () => {
@@ -131,12 +139,13 @@ describe('SimpleUserModule (e2e)', () => {
131
139
  .expect(200);
132
140
 
133
141
  expectOkEnvelope(res.body);
134
- const data = (res.body as ReturnMessage<SimpleUserResultDto>).data;
142
+ const data = (res.body as GenericReturnMessageDto<AppUser>).data;
135
143
  expect(data).toBeDefined();
136
144
  expect(typeof data.id).toBe('number');
137
145
  // 刚用 email 登录,通常应该有 email
138
146
  expect(data.email).toBe(email1);
139
147
  expect(typeof data.passwordSet).toBe('boolean');
148
+ expect(data.age).toBe(18); // from AppUser default
140
149
  });
141
150
 
142
151
  it('POST /user-center/change-password -> set password (first time) and then login by password', async () => {
@@ -158,7 +167,8 @@ describe('SimpleUserModule (e2e)', () => {
158
167
  .expect(200);
159
168
 
160
169
  expectOkEnvelope(loginOk.body);
161
- const data = (loginOk.body as ReturnMessage<LoginResponseDto>).data;
170
+ const data = (loginOk.body as GenericReturnMessageDto<LoginResponseDto>)
171
+ .data;
162
172
  expect(data.token).toHaveLength(64);
163
173
 
164
174
  // 用错密码应 403
@@ -181,6 +191,8 @@ describe('SimpleUserModule (e2e)', () => {
181
191
 
182
192
  expectOkEnvelope(send.body);
183
193
 
194
+ currentEmail = email2;
195
+
184
196
  // 提交换绑
185
197
  const change = await request(httpServer)
186
198
  .post('/user-center/change-email')
@@ -199,10 +211,131 @@ describe('SimpleUserModule (e2e)', () => {
199
211
  .expect(200);
200
212
 
201
213
  expectOkEnvelope(relogin.body);
202
- const data = (relogin.body as ReturnMessage<LoginResponseDto>).data;
214
+ const data = (relogin.body as GenericReturnMessageDto<LoginResponseDto>)
215
+ .data;
203
216
  expect(data.token).toHaveLength(64);
204
217
  });
205
218
 
219
+ describe('Article (e2e)', () => {
220
+ it('POST /article -> should create article and auto-bind userId (ignore body.userId)', async () => {
221
+ expect(token).toBeDefined();
222
+ expect(userId).toBeDefined();
223
+
224
+ const res = await request(httpServer)
225
+ .post('/article')
226
+ .set('x-client-ssaid', ssaid)
227
+ .set('x-client-token', token)
228
+ .send({
229
+ title: articleTitle1,
230
+ content: articleContent1,
231
+ // 故意注入:如果 binding 正常,这个不该生效
232
+ userId: 999999999,
233
+ })
234
+ .expect(200);
235
+
236
+ expectOkEnvelope(res.body);
237
+
238
+ const data = (res.body as GenericReturnMessageDto<ArticleCreateResult>)
239
+ .data;
240
+ expect(data).toBeDefined();
241
+ expect(typeof data.id).toBe('number');
242
+ expect(data.title).toBe(articleTitle1);
243
+ expect(data.content).toBe(articleContent1);
244
+
245
+ // 核心:binding 自动绑定为当前登录用户
246
+ expect(data.userId).toBe(userId);
247
+
248
+ articleId = data.id;
249
+ });
250
+
251
+ it('GET /article/{id} -> should return article with user info and correct userId/email', async () => {
252
+ expect(articleId).toBeDefined();
253
+
254
+ const res = await request(httpServer)
255
+ .get(`/article/${articleId}`)
256
+ .set('x-client-ssaid', ssaid)
257
+ .set('x-client-token', token)
258
+ .expect(200);
259
+
260
+ expectOkEnvelope(res.body);
261
+ const data = (res.body as GenericReturnMessageDto<ArticleResult>).data;
262
+ expect(data).toBeDefined();
263
+ expect(data.id).toBe(articleId);
264
+ expect(data.userId).toBe(userId);
265
+
266
+ // 你的 schema 里 findOne 会带 user(ManyToOne + NotColumn),这里顺手断言一下
267
+ expect(data.user).toBeDefined();
268
+ expect(data.user.id).toBe(userId);
269
+ // email 可能是可选字段,但你这里应该有(我们已经换绑过)
270
+ expect(data.user.email).toBe(currentEmail);
271
+ });
272
+
273
+ it('GET /article -> should list articles and include the created one', async () => {
274
+ expect(articleId).toBeDefined();
275
+
276
+ const res = await request(httpServer)
277
+ .get('/article')
278
+ .set('x-client-ssaid', ssaid)
279
+ .set('x-client-token', token)
280
+ .query({ pageCount: 1, recordsPerPage: 10 })
281
+ .expect(200);
282
+
283
+ expectOkEnvelope(res.body);
284
+ expect(typeof res.body.total).toBe('number');
285
+ expect(Array.isArray(res.body.data)).toBe(true);
286
+
287
+ const found = (res.body.data as ArticleCreateResult[]).find(
288
+ (x) => x.id === articleId,
289
+ );
290
+ expect(found).toBeDefined();
291
+ expect(found.userId).toBe(userId);
292
+ });
293
+
294
+ it('PATCH /article/{id} -> should update title/content', async () => {
295
+ expect(articleId).toBeDefined();
296
+
297
+ const patch = await request(httpServer)
298
+ .patch(`/article/${articleId}`)
299
+ .set('x-client-ssaid', ssaid)
300
+ .set('x-client-token', token)
301
+ .send({ title: articleTitle2, content: articleContent2 })
302
+ .expect(200);
303
+
304
+ expectOkEnvelope(patch.body);
305
+
306
+ const after = await request(httpServer)
307
+ .get(`/article/${articleId}`)
308
+ .set('x-client-ssaid', ssaid)
309
+ .set('x-client-token', token)
310
+ .expect(200);
311
+
312
+ expectOkEnvelope(after.body);
313
+ const data = (after.body as GenericReturnMessageDto<ArticleResult>).data;
314
+ expect(data.title).toBe(articleTitle2);
315
+ expect(data.content).toBe(articleContent2);
316
+ });
317
+
318
+ it('DELETE /article/{id} -> should delete; then GET should 404', async () => {
319
+ expect(articleId).toBeDefined();
320
+
321
+ const del = await request(httpServer)
322
+ .delete(`/article/${articleId}`)
323
+ .set('x-client-ssaid', ssaid)
324
+ .set('x-client-token', token)
325
+ .expect(200);
326
+
327
+ expectOkEnvelope(del.body);
328
+
329
+ const gone = await request(httpServer)
330
+ .get(`/article/${articleId}`)
331
+ .set('x-client-ssaid', ssaid)
332
+ .set('x-client-token', token);
333
+
334
+ expect(gone.status).toBe(404);
335
+ expectOkEnvelope(gone.body);
336
+ });
337
+ });
338
+
206
339
  it('POST /send-code/send (ResetPassword) -> POST /login/reset-password -> login with new password', async () => {
207
340
  // 发 reset code(purpose 注意大小写:ResetPassword)
208
341
  const send = await request(httpServer)
@@ -1,45 +0,0 @@
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" ]
@@ -1,2 +0,0 @@
1
- export declare class AppModule {
2
- }
@@ -1,53 +0,0 @@
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
@@ -1 +0,0 @@
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 DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/main.js DELETED
@@ -1,26 +0,0 @@
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
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
-
3
- npm install --save typeorm @nestjs/typeorm pg nicot