Skip to content

feat(nestjs): device limit#139

Merged
kagol merged 7 commits intoopentiny:devfrom
GaoNeng-wWw:feat/device-limit
Nov 30, 2025
Merged

feat(nestjs): device limit#139
kagol merged 7 commits intoopentiny:devfrom
GaoNeng-wWw:feat/device-limit

Conversation

@GaoNeng-wWw
Copy link
Collaborator

@GaoNeng-wWw GaoNeng-wWw commented Nov 28, 2025

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #135

What is the new behavior?

允许用户通过设置后端的 DEVICE_LIMIT 环境变量来修改同时在线的设备数. 如果设置为 -1 则不做数量限制.

允许多人共用演示

1_crf.mp4

删除账号后吊销所有会话

2_crf.mp4

Does this PR introduce a breaking change?

  • Yes
  • No

同时做了双token, 前端需要修改 web/src/store/modules/user/index.ts

const useUserStore = defineStore('user', {
     async login(loginForm: LoginData) {
       try {
         const res = await userLogin(loginForm);
-        const { token } = res.data;
-        setToken(token);
+        const { accessToken } = res.data;
+        setToken(accessToken);

Other information

  • Nest.js implement
  • Nest.js unit test

Summary by CodeRabbit

  • New Features

    • Token management: issue/store tokens, liveness checks, revocation, and device-limit enforcement
    • Structured login responses now return access/refresh tokens with individual TTLs
  • Configuration

    • New env vars for auth tuning: REFRESH_TOKEN_TTL and DEVICE_LIMIT
    • App config validation added for required envs
  • Frontend

    • Login flow uses accessToken field; user role state changed to an array
  • Documentation

    • Added env var descriptions and examples for token TTLs and device limits

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions github-actions bot added enhancement New feature or request unit-test Unit test labels Nov 28, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 28, 2025

Walkthrough

Adds a self-hosted JWT library and JwtService, a Redis-backed TokenService with token lifecycle and device limits, updates auth flows/modules/tests to use TokenService, introduces config schema and env vars, adds utilities and path mappings, and adjusts frontend types/state for access/refresh token TTLs.

Changes

Cohort / File(s) Summary
JWT library
template/nestJs/libs/jwt/src/index.ts, template/nestJs/libs/jwt/src/jwt.configure.ts, template/nestJs/libs/jwt/src/jwt.module.ts, template/nestJs/libs/jwt/src/jwt.service.ts, template/nestJs/libs/jwt/tsconfig.lib.json
New configurable JwtModule and JwtService exports; ConfigurableModuleBuilder (JwtOptions); decode/verify/sign methods using jose; library tsconfig added.
Token management & types
template/nestJs/src/auth/token.service.ts, template/nestJs/src/auth/entity/token.ts, template/nestJs/src/auth/dto/login-in.dto.ts
New TokenService and TokenData type with create/issue/revoke/revokeByUid/revokeExpiredToken/getLastToken/accessTokenAlive/getUserTokenCount; token payload types and LoginResponse DTO.
Auth refactor & wiring
template/nestJs/src/auth/auth.service.ts, template/nestJs/src/auth/auth.guard.ts, template/nestJs/src/auth/auth.module.ts
AuthModule registers TokenService and self-hosted JwtModule (config-driven); AuthService and AuthGuard now inject/use TokenService and ConfigService; login/logout/kickOut flows updated to use TokenService.
Tests updated
template/nestJs/src/auth/__tests__/auth.service.spec.ts, template/nestJs/src/auth/__tests__/auth.guard.spec.ts, template/nestJs/src/permission/__tests__/permission.guard.spec.ts, template/nestJs/src/user/__tests__/user.service.spec.ts
Tests add TokenService and ConfigService mocks, add uuid.v7 mocks, change expectations to id-based revocation, and switch JwtService imports to @app/jwt where applicable.
User module & service
template/nestJs/src/user/user.module.ts, template/nestJs/src/user/user.service.ts
UserModule registers TokenService; user.service now calls authService.kickOut with user id (number) instead of email.
Configuration & manifests
template/nestJs/src/config-schema.ts, template/nestJs/.env.example, template/nestJs/nest-cli.json, template/nestJs/tsconfig.json, template/nestJs/package.json
Adds Configure type and CONFIG_SCHEMA (includes REFRESH_TOKEN_TTL, DEVICE_LIMIT), updates .env.example, registers jwt project in nest-cli.json, adds tsconfig paths @app/jwt, and updates package.json (deps, scripts, Jest mapping).
Utilities & redis helper
template/nestJs/libs/utils/pick.ts, template/nestJs/libs/utils/inex.ts, template/nestJs/libs/redis/redis.service.ts
Adds pick utility and re-export, and adds RedisService.getRedis() accessor.
Frontend updates
template/tinyvue/src/api/user.ts, template/tinyvue/src/store/modules/user/index.ts, template/tinyvue/src/store/modules/user/types.ts, template/tinyvue/package.json
Adds LoginResponse type with TTLs; login API returns accessToken and TTLs; store uses accessToken and changes role from string to array; bumped several frontend deps.
Docs
docs/tiny-pro-backend-dev-guideline.md
Documents new env vars (ACCESS_TOKEN TTL, REFRESH_TOKEN_TTL, DEVICE_LIMIT) and example values.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthService
    participant TokenService
    participant JwtService
    participant Redis

    Client->>AuthService: POST /login (email,password)
    AuthService->>AuthService: validate credentials
    AuthService->>TokenService: createToken(userId,email)
    TokenService->>JwtService: sign access token (payload + jti, ttl)
    TokenService->>JwtService: sign refresh token (payload + jti, ttl)
    JwtService-->>TokenService: accessToken, refreshToken
    TokenService-->>AuthService: TokenData (tokens, JTIs, TTLs)
    AuthService->>TokenService: issueToken(userId, TokenData)
    TokenService->>Redis: store tokens (keys, expirations) and append user token list
    TokenService->>Redis: enforce DEVICE_LIMIT (revoke oldest if needed)
    TokenService-->>AuthService: issued
    AuthService-->>Client: LoginResponse {accessToken, accessTokenTTL, refreshToken, refreshTokenTTL}

    Note over Client,AuthService: Later - request with token
    Client->>AuthService: request (Authorization: Bearer token)
    AuthService->>JwtService: decode(token)
    AuthService->>TokenService: accessTokenAlive(token)
    TokenService->>Redis: exists(accessTokenJTI)
    Redis-->>TokenService: boolean
    TokenService-->>AuthService: alive? true/false
    alt alive
        AuthService-->>Client: allow
    else
        AuthService-->>Client: deny (unauthorized)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Files needing focused review:
    • template/nestJs/src/auth/token.service.ts — Redis multi/transaction logic, TTLs, JTI bookkeeping, device-limit enforcement.
    • template/nestJs/src/auth/auth.service.ts & template/nestJs/src/auth/auth.guard.ts — TokenService integration, API vs access-token branching, payload typing and error paths.
    • template/nestJs/libs/jwt/src/jwt.service.ts — signing/verifying with jose and secret encoding.
    • template/nestJs/src/config-schema.ts and template/nestJs/.env.example — schema types vs env examples (REFRESH_TOKEN_TTL, DEVICE_LIMIT).
    • package.json / tsconfig / nest-cli.json — path mappings and Jest/moduleNameMapper for @app/jwt.
    • Tests that mock TokenService and uuid.v7 — ensure mocks reflect production behavior.

Poem

🐇 I hop into the code at night,
I sign small tokens, keep them tight,
Redis burrows hold each jti,
Old devices evicted, set them free,
A little rabbit guards your flight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature being implemented: device limit functionality for the NestJS backend.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00c1078 and 2378e93.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • template/nestJs/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • template/nestJs/package.json (6 hunks)
  • template/nestJs/src/auth/token.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • template/nestJs/src/auth/token.service.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-test
🔇 Additional comments (6)
template/nestJs/package.json (6)

52-52: uuid version aligns with PR objectives.

The uuid dependency is pinned to ^9.0.0, which addresses the ESM import error mentioned in the PR objectives (downgrade from v13). This is correct.


65-65: Type definitions for uuid are correctly added.

The @types/uuid dev dependency (^9.0.0) matches the runtime uuid version. This ensures proper TypeScript support.


11-11: Format script correctly extended for new libs directory.

The format script now includes "libs/**/*.ts", which is necessary for the new @app/jwt library (as indicated in the module mapper). This ensures consistent code formatting across the new internal library.


110-110: Jest coverageDirectory path change requires verification.

The coverageDirectory changed from ../coverage (repository root) to ./coverage (project root). Verify:

  • CI/CD pipelines and coverage reporting tools correctly point to the new location.
  • No breaking changes to coverage aggregation if multiple projects contribute to unified reporting.

113-114: I'm unable to access the repository due to clone operation failures in the sandbox environment. However, I can still provide guidance on the review comment based on the verification needs you've requested.

Since I cannot directly verify the codebase, let me provide a rewritten review comment that maintains the legitimate verification concerns while acknowledging the verification approach:


Verify module mapper alignment with new JWT library structure.

The moduleNameMapper entry ^@app/jwt(|/.*)$ pointing to <rootDir>/../libs/jwt/src/$1 requires verification. Ensure:

  • The libs/jwt/src/ directory exists with appropriate export files (e.g., index.ts or specific module files matching the regex pattern).
  • All @app/jwt imports in the codebase resolve correctly—the regex pattern (|/.*)$ allows both direct @app/jwt imports and subpath imports like @app/jwt/utils.
  • The alias pattern is consistent with the adjacent @app/models mapping, which uses a simpler direct path. Consider whether this JWT library also needs regex-based subpath support or if a direct mapping would suffice.

Verify the configuration in a local development environment by running Jest and type-checking to confirm resolution works as expected.



38-38: joi 18.0.2 is valid and stable for NestJS projects using custom schema validation adapters.

Based on verification: joi 18.0.2 is the latest stable release (as of Nov 2025), actively maintained, and has no known security vulnerabilities. While NestJS doesn't use Joi natively (it defaults to class-validator), popular adapters like nestjs-joi support both joi v17 and v18, so the ^18.0.2 version is compatible. Confirm that your project integrates Joi through an appropriate adapter (e.g., nestjs-joi or a custom pipe) before merging.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
template/tinyvue/src/store/modules/user/types.ts (1)

26-31: Align UserInfo.role (Role[]) with store usage to avoid type/shape mismatch

Changing role to Role[] matches the backend shape, but the store still treats role as a string in template/tinyvue/src/store/modules/user/index.ts (e.g., switchRoles() toggles 'user' / 'admin', and the login action builds userInfo with role: '' then assigns a role name string). This creates a type and runtime mismatch between UserInfo and the actual store state.

Consider either:

  • Updating the store to genuinely store Role[] (and derive any display role name/job from role[0]), or
  • Temporarily widening the type to cover both shapes during migration, e.g.:
-export interface UserInfo {
+export interface UserInfo {
   ...
-  role: Role[];
+  role: Role[] | RoleType;
   ...
}

and then cleaning up the string-based usages.

template/tinyvue/src/store/modules/user/index.ts (1)

36-46: Fix role state shape: array default conflicts with string-based handling

With UserInfo.role now typed as Role[], setting role: [] in state is fine, but other logic still treats role as a string:

  • switchRoles() (Line 55–60) does this.role === 'user' ? 'admin' : 'user', so role flips between strings, not Role[].
  • In login(), the constructed userInfo literal uses role: '', later assigning userInfo.role = userRes.data.role[0].name; (again, a string), while also reading userRes.data.role as an array.

This leads to role having incompatible shapes at runtime (sometimes array, sometimes string) and contradicts the updated type.

You likely want one of:

  1. Store Role[] in state

    • Initialize role from the backend array, e.g. role: userRes.data.role ?? [].
    • Keep role name/job derived from role[0] only in job or a separate primaryRoleName field.
    • Remove or rewrite switchRoles() to work on Role objects (or drop it if it was only a demo toggle).
  2. Keep role as a string and add a separate roles: Role[]

    • Revert UserInfo.role to the string union and introduce roles?: Role[] for the backend data.
    • Use roles when you need full role objects and keep role for display/permission checks.

Right now, this inconsistency will make both typing and runtime behavior brittle.

Also applies to: 55-61, 90-119

🧹 Nitpick comments (19)
template/nestJs/src/permission/__tests__/permission.guard.spec.ts (1)

8-10: Use a partial mock to avoid breaking other uuid exports in this test file

The deterministic v7 mock is good for stable tests, but replacing the entire uuid module with only v7 can break cases where SUT (or future code) imports other uuid exports (e.g., v4, default). A safer pattern is to extend the real module and override just v7:

-jest.mock('uuid', () => ({
-  v7: jest.fn(() => 'mocked-uuid-v7'),
-}));
+jest.mock('uuid', () => ({
+  ...jest.requireActual('uuid'),
+  v7: jest.fn(() => 'mocked-uuid-v7'),
+}));

This keeps the behavior of all other exports intact while still making v7 deterministic in this test.

template/nestJs/libs/redis/redis.service.ts (1)

13-15: LGTM! Exposing the Redis client is appropriate for complex operations in TokenService.

Consider adding an explicit return type for consistency with the codebase's type annotations:

-  getRedis(){
+  getRedis(): Redis {
     return this.redisClient;
   }
template/nestJs/src/user/user.service.ts (1)

375-376: Role comparison may produce false positives.

The comparison userRoles !== (roleIds || []).join('') relies on role ID ordering. If the same roles are provided in a different order, this will incorrectly trigger a kickout.

Consider using a set-based comparison:

-    const userRoles = user.role.map((role) => role.id).join('');
+    const userRoleIds = new Set(user.role.map((role) => role.id));
+    const newRoleIds = new Set(roleIds || []);
...
-    if (userRoles !== (roleIds || []).join('')) {
+    const rolesChanged = userRoleIds.size !== newRoleIds.size || 
+      [...userRoleIds].some(id => !newRoleIds.has(id));
+    if (rolesChanged) {
       await this.authService.kickOut(user.id);
     }
template/nestJs/libs/jwt/src/jwt.service.ts (1)

15-19: Redundant variable assignment.

The ret variable is unnecessary:

   async verify<T>(token: string): Promise<JWTVerifyResult<T>>{
-    const secrect = new TextEncoder().encode(this.cfg.secrect);
-    const ret = await jwtVerify<T>(token,secrect);
-    return ret
+    const secret = new TextEncoder().encode(this.cfg.secret);
+    return jwtVerify<T>(token, secret);
   }
template/nestJs/src/auth/dto/login-in.dto.ts (1)

1-6: Consider adding validation decorators and readonly modifiers.

While this is a response DTO (output), adding class-validator decorators (e.g., @IsString(), @IsNumber()) can help catch serialization issues early. Additionally, marking fields as readonly signals immutability.

Example:

+import { IsString, IsNumber } from 'class-validator';
+
 export class LoginResponse {
+  @IsString()
+  readonly accessToken: string;
+  
+  @IsNumber()
+  readonly accessTokenTTL: number;
+  
+  @IsString()
+  readonly refreshToken: string;
+  
+  @IsNumber()
+  readonly refreshTokenTTL: number;
-  accessToken: string;
-  accessTokenTTL: number;
-  refreshToken: string;
-  refreshTokenTTL: number;
 }
template/nestJs/libs/jwt/src/jwt.module.ts (1)

5-9: LGTM! Standard configurable module pattern.

The module correctly extends ConfigurableModuleClass and exports JwtService for dependency injection. The empty imports array on line 6 could be omitted as it's the default, but this is a minor stylistic choice.

template/nestJs/libs/utils/pick.ts (1)

1-16: LGTM! Type-safe object picking utility.

The implementation is correct and type-safe. The early return for falsy objects is a good defensive practice.

Optional: Consider using hasOwnProperty instead of the in operator to avoid picking inherited properties:

   for (const key of keys) {
-    if (key in obj) {
+    if (Object.prototype.hasOwnProperty.call(obj, key)) {
       result[key] = obj[key];
     }
   }

This prevents unintended behavior if obj has inherited properties matching the requested keys.

template/nestJs/src/user/__tests__/user.service.spec.ts (1)

163-164: Consider using a more specific assertion.

Other tests use toHaveBeenCalledWith(1) for the kickOut call. For consistency, consider updating this assertion to match.

-      expect(authService.kickOut).toHaveBeenCalled()
+      expect(authService.kickOut).toHaveBeenCalledWith(1)
template/nestJs/src/auth/auth.module.ts (1)

31-41: Inconsistent configuration pattern: process.env vs ConfigService.

SelfJwtModule uses ConfigService for AUTH_SECRET (line 27), but the existing JwtModule uses process.env.AUTH_SECRET directly (line 34). Consider using ConfigService consistently for both modules to benefit from validation and type safety.

     JwtModule.registerAsync({
-      imports: [RedisModule],
-      useFactory: async () => ({
-        secret: process.env.AUTH_SECRET,
+      imports: [RedisModule, ConfigModule],
+      inject: [ConfigService],
+      useFactory: async (cfg: ConfigService) => ({
+        secret: cfg.get('AUTH_SECRET'),
         global: true,
         signOptions: {
-          expiresIn: process.env.EXPIRES_IN,
+          expiresIn: cfg.get('EXPIRES_IN'),
         },
       }),
       global: true,
     }),
template/nestJs/src/auth/__tests__/auth.guard.spec.ts (1)

206-227: Consider adding a test for when accessTokenAlive returns false.

The tests cover accessTokenAlive returning true, but there's no explicit test for when accessTokenAlive returns false for a non-API token. This would improve coverage of the token validation branch in auth.guard.ts.

it('should throw HttpException when access token is not alive', async () => {
  reflector.getAllAndOverride.mockReturnValue(false);
  jwt.verify.mockResolvedValue({});
  jwt.decode.mockReturnValue({ email: 'test@example.com' });
  tokenService.accessTokenAlive.mockResolvedValue(false);
  const mockRequest = {
    headers: {
      authorization: 'Bearer validToken',
    },
  };
  const mockContext = {
    getHandler: jest.fn(),
    getClass: jest.fn(),
    switchToHttp: () => ({
      getRequest: () => mockRequest,
    }),
  } as any;

  await expect(authGuard.canActivate(mockContext)).rejects.toThrow(HttpException);
});
template/nestJs/src/auth/auth.guard.ts (2)

44-74: Error handling masks the original exception.

The outer catch block at line 76 catches all exceptions (including the HttpException thrown at lines 57-62 and 67-72) and re-throws a generic "tokenExpire" error. This masks the original exception and its context.

Consider re-throwing HttpException instances directly:

     } catch (err) {
+      if (err instanceof HttpException) {
+        throw err;
+      }
       throw new HttpException(
         i18n.t('exception.common.tokenExpire', {
           lang: I18nContext.current().lang,
         }),
         HttpStatus.UNAUTHORIZED
       );
     }

66-66: Minor formatting: add space after await.

-        if (!await this.tokenService.accessTokenAlive(token)){
+        if (!(await this.tokenService.accessTokenAlive(token))) {
template/nestJs/src/auth/__tests__/auth.service.spec.ts (2)

67-72: ConfigService mock lacks return values for get() calls.

The mock's get function returns undefined for all keys. If AuthService.login() uses ConfigService.get() for values like DEVICE_LIMIT or REDIS_SECONDS, tests may not reflect realistic behavior.

         {
           provide: ConfigService,
           useValue:{
-            get: jest.fn()
+            get: jest.fn((key: string) => {
+              const config = {
+                REDIS_SECONDS: 7200,
+                REFRESH_TOKEN_TTL: 604800000,
+                DEVICE_LIMIT: 5,
+              };
+              return config[key];
+            })
           }
         }

143-156: Test could verify the returned token data structure.

The test only asserts that tokenService.createToken was called, but doesn't verify what service.login() returns. Consider asserting the response contains expected token fields.

-      await service.login({ email: 'test@example.com', password: 'hashed-password' });
+      const result = await service.login({ email: 'test@example.com', password: 'hashed-password' });
       expect(tokenService.createToken).toHaveBeenCalledWith(1, 'test@example.com');
+      // Optionally verify response structure
+      expect(result).toBeDefined();
template/nestJs/src/config-schema.ts (1)

5-5: Consider using number type for port fields.

DATABASE_PORT and REDIS_PORT are typed as string but typically used as numbers. This may cause issues when passing to connection libraries.

-  DATABASE_PORT:string;
+  DATABASE_PORT:number;
...
-  REDIS_PORT:string;
+  REDIS_PORT:number;

And update the schema accordingly:

-  DATABASE_PORT: Joi.string(),
+  DATABASE_PORT: Joi.number(),
...
-  REDIS_PORT: Joi.string(),
+  REDIS_PORT: Joi.number(),

Also applies to: 14-14

template/nestJs/src/auth/token.service.ts (1)

36-61: revokeExpiredToken has O(n) Redis round-trips - consider pipelining.

Each token check issues a separate redis.exists() call. For users with many tokens, this creates significant latency.

Use Redis pipeline or mget to batch existence checks:

   async revokeExpiredToken(uid: number){
     const redis = this.redisService.getRedis();
-
     const allRefreshTokenJti = await redis.lrange(`user:${uid}:rt`, 0, -1);
     const allAccessTokenJti = await redis.lrange(`user:${uid}:at`, 0, -1);
-    const expiredRefreshTokenJti = [];
-    const expiredAccessTokenJti = [];
-    for (const refreshTokenJti of allRefreshTokenJti) {
-      if (await redis.exists(`rt:${uid}:${refreshTokenJti}`)) {
-        continue;
-      }
-      expiredRefreshTokenJti.push(refreshTokenJti);
-    }
-    for (const accessTokenJti of allAccessTokenJti) {
-      if (await redis.exists(`at:${uid}:${accessTokenJti}`)) {
-        continue;
-      }
-      expiredAccessTokenJti.push(accessTokenJti);
-    }
+    
+    // Pipeline existence checks
+    const pipeline = redis.pipeline();
+    allRefreshTokenJti.forEach(jti => pipeline.exists(`rt:${uid}:${jti}`));
+    allAccessTokenJti.forEach(jti => pipeline.exists(`at:${uid}:${jti}`));
+    const results = await pipeline.exec();
+    
+    const rtResults = results.slice(0, allRefreshTokenJti.length);
+    const atResults = results.slice(allRefreshTokenJti.length);
+    
+    const expiredRefreshTokenJti = allRefreshTokenJti.filter((_, i) => !rtResults[i][1]);
+    const expiredAccessTokenJti = allAccessTokenJti.filter((_, i) => !atResults[i][1]);
+    
+    // Batch removals
+    const cleanupPipeline = redis.pipeline();
     for (const accessJti of expiredAccessTokenJti) {
-      await redis.lrem(`user:${uid}:at`, 0, accessJti);
+      cleanupPipeline.lrem(`user:${uid}:at`, 0, accessJti);
     }
-    for (const refresJti of expiredRefreshTokenJti) {
-      await redis.lrem(`user:${uid}:rt`, 0, refresJti);
+    for (const refreshJti of expiredRefreshTokenJti) {
+      cleanupPipeline.lrem(`user:${uid}:rt`, 0, refreshJti);
     }
+    await cleanupPipeline.exec();
   }
template/nestJs/src/auth/auth.service.ts (2)

105-106: Use ConfigService consistently for configuration access.

Line 105 uses process.env.API_TOKEN_SECONDS directly while the rest of the file uses ConfigService. This bypasses validation and type safety. Consider adding API_TOKEN_SECONDS to the Configure schema and accessing it via this.cfg.get().

-    const ttl = parseInt(process.env.API_TOKEN_SECONDS) || 86400 * 7; // 默认7天
+    const ttl = this.cfg.get('API_TOKEN_SECONDS', { infer: true }) ?? 86400 * 7; // 默认7天

Note: This requires adding API_TOKEN_SECONDS to the Configure type in src/config-schema.ts.


100-102: Replace deprecated substr with substring.

The substr method is deprecated. Use substring or slice instead.

-      `api_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+      `api_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
template/tinyvue/src/api/user.ts (1)

25-30: LoginResponse typing is correct; consider clarifying/co-locating legacy LoginRes

The new LoginResponse interface and login() return type line up with the NestJS LoginResponse DTO (access/refresh tokens plus TTLs) and the updated store usage of res.data.accessToken.

LoginRes now only backs loginMail and logout. To reduce confusion around “two login response shapes”, consider:

  • Renaming LoginRes to something endpoint-specific (e.g., MailLoginResponse), and/or
  • Adjusting the logout generic if /auth/logout no longer returns { token, userInfo }.

Functionally the new typing for /auth/login looks good; just ensure all callers of login have been updated away from token to accessToken.

Also applies to: 48-50

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 436b465 and 8ada208.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • template/nestJs/.env.example (1 hunks)
  • template/nestJs/libs/jwt/src/index.ts (1 hunks)
  • template/nestJs/libs/jwt/src/jwt.configure.ts (1 hunks)
  • template/nestJs/libs/jwt/src/jwt.module.ts (1 hunks)
  • template/nestJs/libs/jwt/src/jwt.service.ts (1 hunks)
  • template/nestJs/libs/jwt/tsconfig.lib.json (1 hunks)
  • template/nestJs/libs/redis/redis.service.ts (1 hunks)
  • template/nestJs/libs/utils/inex.ts (1 hunks)
  • template/nestJs/libs/utils/pick.ts (1 hunks)
  • template/nestJs/nest-cli.json (2 hunks)
  • template/nestJs/package.json (5 hunks)
  • template/nestJs/src/app.module.ts (2 hunks)
  • template/nestJs/src/auth/__tests__/auth.guard.spec.ts (5 hunks)
  • template/nestJs/src/auth/__tests__/auth.service.spec.ts (6 hunks)
  • template/nestJs/src/auth/auth.guard.ts (3 hunks)
  • template/nestJs/src/auth/auth.module.ts (1 hunks)
  • template/nestJs/src/auth/auth.service.ts (4 hunks)
  • template/nestJs/src/auth/dto/login-in.dto.ts (1 hunks)
  • template/nestJs/src/auth/entity/token.ts (1 hunks)
  • template/nestJs/src/auth/token.service.ts (1 hunks)
  • template/nestJs/src/config-schema.ts (1 hunks)
  • template/nestJs/src/permission/__tests__/permission.guard.spec.ts (2 hunks)
  • template/nestJs/src/user/__tests__/user.service.spec.ts (6 hunks)
  • template/nestJs/src/user/user.module.ts (1 hunks)
  • template/nestJs/src/user/user.service.ts (5 hunks)
  • template/nestJs/tsconfig.json (1 hunks)
  • template/tinyvue/src/api/user.ts (2 hunks)
  • template/tinyvue/src/store/modules/user/index.ts (2 hunks)
  • template/tinyvue/src/store/modules/user/types.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
template/nestJs/src/auth/dto/login-in.dto.ts (1)
template/tinyvue/src/api/user.ts (1)
  • LoginResponse (25-30)
template/nestJs/libs/jwt/src/jwt.module.ts (1)
template/nestJs/src/auth/auth.module.ts (1)
  • Module (14-45)
template/nestJs/src/user/user.module.ts (1)
template/nestJs/src/auth/auth.module.ts (1)
  • Module (14-45)
template/tinyvue/src/api/user.ts (1)
template/nestJs/src/auth/dto/login-in.dto.ts (1)
  • LoginResponse (1-6)
template/nestJs/libs/jwt/src/jwt.service.ts (4)
template/nestJs/src/auth/auth.guard.ts (1)
  • Injectable (17-89)
template/nestJs/src/auth/auth.service.ts (1)
  • Injectable (16-125)
template/nestJs/src/auth/token.service.ts (1)
  • Injectable (19-185)
template/nestJs/libs/jwt/src/jwt.configure.ts (1)
  • JwtOptions (3-5)
template/nestJs/src/app.module.ts (1)
template/nestJs/src/config-schema.ts (1)
  • CONFIG_SCHEMA (24-43)
template/nestJs/src/auth/auth.guard.ts (1)
template/nestJs/src/auth/entity/token.ts (1)
  • TokenPayload (5-5)
template/nestJs/src/auth/token.service.ts (3)
template/nestJs/src/auth/entity/token.ts (2)
  • AccessTokenPayload (11-15)
  • RefreshTokenPayload (16-20)
template/nestJs/libs/jwt/src/jwt.service.ts (1)
  • Injectable (5-35)
template/nestJs/libs/redis/redis.service.ts (1)
  • Injectable (4-71)
template/nestJs/src/auth/auth.module.ts (3)
template/nestJs/libs/jwt/src/jwt.module.ts (1)
  • Module (5-10)
template/nestJs/src/app.module.ts (1)
  • Module (42-235)
template/nestJs/src/user/user.module.ts (1)
  • Module (10-16)
template/nestJs/src/auth/auth.service.ts (3)
template/nestJs/src/config-schema.ts (1)
  • Configure (3-22)
template/nestJs/src/auth/entity/token.ts (1)
  • AccessTokenPayload (11-15)
template/nestJs/libs/utils/pick.ts (1)
  • pick (1-16)
🪛 dotenv-linter (4.0.0)
template/nestJs/.env.example

[warning] 17-17: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 19-19: [UnorderedKey] The DEVICE_LIMIT key should go before the EXPIRES_IN key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e-test
🔇 Additional comments (30)
template/nestJs/src/user/user.service.ts (4)

44-44: Whitespace-only change.

No functional impact.


253-253: LGTM! Correctly updated to use user.id to align with the new TokenService.revokeByUid(id: number) signature.


297-297: LGTM! Consistent with the new id-based token revocation.


312-312: LGTM! Consistent with the new id-based token revocation.

template/nestJs/libs/jwt/src/jwt.service.ts (3)

11-13: decode() can throw on malformed tokens.

decodeJwt throws if the token is malformed. Consider whether callers handle this or if a try-catch should be added here for consistent error handling.


21-34: LGTM! The sign implementation correctly sets expiration and uses HS256 with the encoded secret.


1-9: Typo: secrect should be secret.

The configuration property secrect is misspelled. This affects the public API surface across jwt.configure.ts and consumers. Fix the typo in jwt.configure.ts:

 export interface JwtOptions {
-  secrect: string;
+  secret: string;
 }

Then update references here:

-    const secrect = new TextEncoder().encode(this.cfg.secrect);
+    const secret = new TextEncoder().encode(this.cfg.secret);

Likely an incorrect or invalid review comment.

template/nestJs/src/app.module.ts (2)

40-40: LGTM! Good practice to import and use a centralized configuration schema.


51-55: Unable to complete verification due to repository access failure.

The repository clone operation failed, preventing access to the codebase and the CONFIG_SCHEMA file referenced in the verification request. Without being able to inspect the actual schema configuration, I cannot verify whether critical environment variables (AUTH_SECRET, DATABASE_*) are properly marked as required as noted in the original review comment.

template/nestJs/src/user/user.module.ts (1)

8-14: I attempted to verify the review comment by examining the repository directly, but unfortunately encountered a repository access failure. Without access to the actual codebase, I cannot definitively confirm:

  1. Whether AuthModule currently exports TokenService, AuthService, and RedisService
  2. Whether a circular dependency actually exists between AuthModule and UserModule
  3. Whether the suggested refactoring would introduce or resolve any issues
  4. Whether this duplicate provider pattern is intentional or present elsewhere

Manual verification is needed to confirm the following before applying this refactor:

  • Check template/nestJs/src/auth/auth.module.ts to verify it exports the mentioned services
  • Check if AuthModule imports UserModule (which would create a circular dependency if UserModule then imports AuthModule)
  • Verify there are no initialization order dependencies that require the current structure

The suggestion to import AuthModule instead of duplicating providers is a sound NestJS architectural practice, but the circular dependency concern must be validated before implementation.

template/nestJs/libs/jwt/src/index.ts (1)

1-2: LGTM! Clean barrel export pattern.

The re-exports follow standard NestJS library conventions, enabling clean imports via the @app/jwt alias.

template/nestJs/tsconfig.json (1)

28-32: LGTM! Path alias correctly configured.

The @app/jwt path mapping properly points to the new JWT library and follows the existing pattern for other library aliases.

template/nestJs/libs/jwt/tsconfig.lib.json (1)

1-9: LGTM! Standard TypeScript library configuration.

The configuration properly extends the base tsconfig and sets up the JWT library with appropriate includes/excludes for a NestJS library module.

template/nestJs/libs/jwt/src/jwt.module.ts (1)

1-10: I'll help you verify the configuration interface spelling. Let me search for the "secrect" typo in the JWT-related files.
<function_calls>


#!/bin/bash

Search for "secrect" typo in JWT-related files

rg -n "secrect" --type=ts template/libs/jwt/


</function_calls>

Let me also check if the jwt directory structure matches what we're looking for:
<function_calls>


#!/bin/bash

Find JWT-related files

fd -t f "jwt" --type ts . | head -20


</function_calls>

Let me try a broader search:
<function_calls>


#!/bin/bash

Search for "secrect" across entire repository

rg -n "secrect" --type=ts


</function_calls>

Now let me examine the jwt.configure.ts file and auth.module.ts to understand the structure:
<function_calls>


#!/bin/bash

Find jwt.configure.ts

fd "jwt.configure" --type ts


</function_calls>

<function_calls>


#!/bin/bash

Find auth.module.ts

fd "auth.module" --type ts


</function_calls>

<function_calls>


#!/bin/bash

Get a list of all TypeScript files in the template directory

find template -name "*.ts" -type f 2>/dev/null | grep -E "(jwt|auth)" | head -20


</function_calls>

template/nestJs/libs/utils/inex.ts (1)

1-1: I apologize, but I'm unable to access the repository to verify the filename concern due to connectivity issues. Since I cannot execute the verification scripts or access the codebase to check:

  1. Whether the file actually exists at template/nestJs/libs/utils/inex.ts
  2. Whether there are any imports referencing this filename
  3. Project-specific naming conventions that might justify this name

I'll provide the rewritten review comment based on the original concern, but marked for manual verification:


Verify filename: "inex.ts" may be a typo for "index.ts"

The filename "inex.ts" appears to be a typo. Standard convention for barrel export files is "index.ts". If intentional, this could cause confusion and make the module harder to discover. Confirm whether this name is deliberate or should be corrected to "index.ts", and check whether any imports reference this file.

template/nestJs/src/user/__tests__/user.service.spec.ts (1)

11-13: LGTM!

The UUID mock ensures deterministic test behavior for token generation, which is good practice for unit tests.

template/nestJs/nest-cli.json (1)

34-42: LGTM!

The new jwt library project follows the same structure as existing db and models libraries, maintaining consistency in the monorepo configuration.

template/nestJs/src/auth/__tests__/auth.guard.spec.ts (1)

29-37: LGTM!

The tokenService mock provides comprehensive coverage of TokenService methods. The accessTokenAlive mock is correctly used in the valid token test case (line 211).

template/nestJs/src/auth/auth.guard.ts (1)

50-50: Good use of runtime type guard.

The 'type' in payload check ensures safe property access before comparing payload.type, preventing potential runtime errors when the payload structure differs.

template/nestJs/src/auth/entity/token.ts (2)

1-20: LGTM for the type structure.

The token payload types are well-structured with a clear base type and proper cross-referencing between access and refresh tokens via their JTIs.


5-5: Repository access unavailable for verification.

The repository clone operation failed, preventing me from verifying the usage patterns of TokenPayload and RefreshTokenPayload throughout the codebase. Without access to the actual code, I cannot definitively determine whether RefreshTokenPayload should be included in the union type or assess the impact of this suggested change.

To properly verify this concern, manual inspection of:

  • All usages of TokenPayload type
  • How RefreshTokenPayload is handled in the codebase
  • Whether refresh tokens are decoded/verified in contexts expecting TokenPayload

would be needed.

template/nestJs/package.json (2)

11-11: LGTM for the build/test configuration updates.

The format script now includes libs/**/*.ts, coverageDirectory path correction, and moduleNameMapper additions for @app/jwt are appropriate for the new library structure.

Also applies to: 109-109, 112-113


38-38: The review comment is incorrect—joi version 18.0.2 exists and is currently the latest stable release.

The original review claimed that "joi library's latest major version is 17.x" and "Version 18.0.2 does not exist on npm," but verification confirms that joi 18.0.2 is the current latest stable release as of November 2025. The code in the PR is correct and requires no changes. The suggested downgrade to ^17.13.3 would introduce an older version unnecessarily.

template/nestJs/src/auth/__tests__/auth.service.spec.ts (1)

12-14: LGTM for test module setup with TokenService integration.

The uuid mock ensures deterministic test behavior, and the tokenService mock covers all necessary lifecycle methods.

Also applies to: 33-41, 63-66

template/nestJs/src/auth/token.service.ts (2)

83-120: LGTM for the token creation logic.

The createToken method properly generates linked access and refresh tokens with cross-referenced JTIs, enabling proper token pair revocation.


63-81: LGTM for revokeByUid using Redis multi transaction.

Bulk token revocation correctly uses a transaction to atomically delete all user tokens.

template/nestJs/src/auth/auth.service.ts (3)

62-68: LGTM!

The login flow correctly builds the payload with user ID and email, creates the token via TokenService, and returns the structured token response with access/refresh tokens and their TTLs.


97-97: I'll help you verify whether the TTL unit is correct for jwtService.sign. Let me search the codebase to find the JWT service implementation and its expected parameters.
<function_calls>

#!/bin/bash

Find JWT-related service files

fd -t f -i 'jwt' --exec basename {} ;


</function_calls>

Now let me locate the auth service and JWT service files more specifically:
<function_calls>

#!/bin/bash

Find the auth service file and any jwt service files

fd -t f 'auth.service.ts'
echo "---"
fd -t f 'jwt.service.ts'
echo "---"
fd -t f -i 'jwt' -type f


</function_calls>

Let me check the auth service file to see the context around line 97:
<function_calls>

#!/bin/bash

First, let's find the auth service file

find . -name 'auth.service.ts' -type f 2>/dev/null | head -5


</function_calls>

<function_calls>

#!/bin/bash

Let's search for the line with jwtService.sign to find the auth.service.ts

rg -n "jwtService.sign" --type ts


</function_calls>

<function_calls>

#!/bin/bash

Search for JwtService and its sign method

rg -n "sign(" --type ts | grep -i jwt


</function_calls>

<function_calls>

#!/bin/bash

Let's look at the repository structure first

ls -la template/ 2>/dev/null || ls -la | head -20


</function_calls>

<function_calls>

#!/bin/bash

Check if template/nestJs/src/auth/auth.service.ts exists

cat template/nestJs/src/auth/auth.service.ts 2>/dev/null | head -120


</function_calls>


32-34: Verify all callers of kickOut are updated for the new signature.

The method signature changed from kickOut(email: string) to kickOut(id: number). Ensure all call sites throughout the codebase have been updated to pass the user ID instead of email.

template/tinyvue/src/store/modules/user/index.ts (1)

85-90: Using accessToken from LoginResponse in login flow looks correct

The change to:

const res = await userLogin(loginForm);
const { accessToken } = res.data;
setToken(accessToken);

matches the new LoginResponse typing and the PR’s breaking-change description (frontend should store the accessToken from /auth/login). No issues here, assuming setToken is meant for the short‑lived access token.

PAGINATION_LIMIT = 10
GLOBAL_PREFIX = '/'
MOCK_REGEX = '/mock'
REFRESH_TOKEN_TTL = 604800000
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix spacing inconsistency around the equals sign.

Line 17 has spaces around the equals sign (REFRESH_TOKEN_TTL = 604800000), while line 19 follows the standard .env convention without spaces (DEVICE_LIMIT=1). Maintain consistency with the rest of the file.

Apply this diff to fix the spacing:

-REFRESH_TOKEN_TTL = 604800000
+REFRESH_TOKEN_TTL=604800000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REFRESH_TOKEN_TTL = 604800000
REFRESH_TOKEN_TTL=604800000
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 17-17: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)

🤖 Prompt for AI Agents
In template/nestJs/.env.example around line 17, the assignment uses spaces
around the equals sign ("REFRESH_TOKEN_TTL = 604800000") which is inconsistent
with the rest of the file; change it to remove spaces so it reads
"REFRESH_TOKEN_TTL=604800000" to match the standard .env formatting.

Comment on lines +3 to +5
export interface JwtOptions {
secrect: string;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: secrect should be secret.

This typo will propagate to all consumers of JwtOptions, including the auth.module.ts where secrect: cfg.get('AUTH_SECRET') is used.

 export interface JwtOptions {
-  secrect: string;
+  secret: string;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface JwtOptions {
secrect: string;
}
export interface JwtOptions {
secret: string;
}
🤖 Prompt for AI Agents
In template/nestJs/libs/jwt/src/jwt.configure.ts around lines 3 to 5, the
JwtOptions interface misspells the property name as `secrect`; rename it to
`secret` and update all consumers (e.g., auth.module.ts and any config calls) to
use `secret: cfg.get('AUTH_SECRET')` (or the appropriate config key) so the
correct property name is used consistently across the codebase.

"test:e2e": "echo 'No e2e tests implemented yet.'"
},
"dependencies": {
"@gaonengwww/jose": "^6.1.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check npm for package info
curl -s https://registry.npmjs.org/@gaonengwww/jose | jq '{name: .name, "dist-tags": .["dist-tags"], time: .time.modified, maintainers: .maintainers}'

Repository: opentiny/tiny-pro

Length of output: 178


🏁 Script executed:

#!/bin/bash
# Check npm for package info
curl -s https://registry.npmjs.org/@gaonengwww/jose | jq '{name: .name, "dist-tags": .["dist-tags"], time: .time.modified, maintainers: .maintainers}'

Repository: opentiny/tiny-pro

Length of output: 178


🌐 Web query:

@gaonengwww/jose npm package

💡 Result:

I couldn't find a package named "@gaonengwww/jose" on the public npm registry. The widely used official JOSE library on npm is "jose" (by panva). Install with npm i jose. [1][2]

Sources:

  • jose package on npm (official): [1]
  • npm docs — about scopes (scoped package naming): [2]

The @gaonengwww/jose package does not exist on the npm registry.

Web search confirms this package is not available publicly. The official JOSE library for Node.js is jose (by panva). Using a non-existent package will cause npm install to fail. If JWT operations are needed, use the official jose package instead, or verify the correct scoped package name and maintainer.

🤖 Prompt for AI Agents
In template/nestJs/package.json at line 24, the dependency "@gaonengwww/jose"
does not exist on npm; replace it with the official "jose" package (e.g. "jose":
"^4") or the correct scoped name if you have a verified private package, then
run npm install and update any imports/usages in the codebase to import from
"jose" instead of the non-existent package.

Comment on lines +21 to +30
SelfJwtModule.registerAsync({
isGlobal: true,
imports: [ConfigModule],
inject: [ConfigService],
useFactory(cfg: ConfigService){
return {
secrect: cfg.get('AUTH_SECRET')
}
}
}),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo propagation: secrect should be secret.

This matches the typo in jwt.configure.ts. Once that's fixed, update this usage as well.

       useFactory(cfg: ConfigService){
         return {
-          secrect: cfg.get('AUTH_SECRET')
+          secret: cfg.get('AUTH_SECRET')
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SelfJwtModule.registerAsync({
isGlobal: true,
imports: [ConfigModule],
inject: [ConfigService],
useFactory(cfg: ConfigService){
return {
secrect: cfg.get('AUTH_SECRET')
}
}
}),
SelfJwtModule.registerAsync({
isGlobal: true,
imports: [ConfigModule],
inject: [ConfigService],
useFactory(cfg: ConfigService){
return {
secret: cfg.get('AUTH_SECRET')
}
}
}),
🤖 Prompt for AI Agents
In template/nestJs/src/auth/auth.module.ts around lines 21 to 30, the factory
return uses the misspelled key "secrect"; change it to "secret" to match
jwt.configure.ts and the expected JwtModule options, and ensure the value comes
from cfg.get('AUTH_SECRET') (adjust types if necessary so the ConfigService
value is accepted).

Comment on lines 36 to 40
async logout(token: string): Promise<void> {
//通过token解析email
const decoded = await this.jwtService.verify(token);
//退出登录后,将token从Redis删除
await this.redisService.delUserToken(`user:${decoded.email}:token`);
const decoded = await this.jwtService.verify<AccessTokenPayload>(token);
await this.tokenService.revokeByUid(decoded.payload.id)
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for token verification failure.

If jwtService.verify() fails (e.g., token is expired, malformed, or invalid), the exception will bubble up unhandled. Consider wrapping this in a try-catch to return an appropriate HTTP 401 response.

 async logout(token: string): Promise<void> {
-    const decoded = await this.jwtService.verify<AccessTokenPayload>(token);
-    await this.tokenService.revokeByUid(decoded.payload.id)
+    try {
+      const decoded = await this.jwtService.verify<AccessTokenPayload>(token);
+      await this.tokenService.revokeByUid(decoded.payload.id);
+    } catch {
+      throw new HttpException(
+        this.i18n.translate('exception.auth.invalidToken', {
+          lang: I18nContext.current().lang,
+        }),
+        HttpStatus.UNAUTHORIZED
+      );
+    }
     return;
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In template/nestJs/src/auth/auth.service.ts around lines 36–40, wrap the call to
this.jwtService.verify(...) in a try-catch so token verification failures are
handled; on failure throw a NestJS UnauthorizedException with a clear message
(e.g., 'Invalid or expired token'), and only call
this.tokenService.revokeByUid(...) when verification succeeds (use the decoded
payload id). Ensure the function returns void as before and update imports if
necessary to include UnauthorizedException.

jti: accessTokenJTI,
id,
email,
issueAt: new Date().toLocaleDateString(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

toLocaleDateString() produces locale-dependent, imprecise timestamps.

Using toLocaleDateString() for issueAt loses time precision and varies by server locale. Use ISO 8601 format for consistency.

-      issueAt: new Date().toLocaleDateString(),
+      issueAt: new Date().toISOString(),

Also applies to: 104-104

🤖 Prompt for AI Agents
In template/nestJs/src/auth/token.service.ts around lines 93 and 104, replace
the locale-dependent and imprecise new Date().toLocaleDateString() assignments
for issueAt with an ISO 8601 timestamp (e.g., new Date().toISOString()) so the
value includes full date and time in a consistent, timezone-independent format;
update both occurrences to use toISOString().

){
const redis = this.redisService.getRedis();
let userTokenCount = await this.getUserTokenCount(uid);
const limit = Number.parseInt(this.cfg.get<string>('DEVICE_LIMIT'));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add radix parameter to parseInt.

Number.parseInt without a radix may interpret strings with leading zeros as octal in some contexts.

-    const limit = Number.parseInt(this.cfg.get<string>('DEVICE_LIMIT'));
+    const limit = Number.parseInt(this.cfg.get<string>('DEVICE_LIMIT'), 10);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const limit = Number.parseInt(this.cfg.get<string>('DEVICE_LIMIT'));
const limit = Number.parseInt(this.cfg.get<string>('DEVICE_LIMIT'), 10);
🤖 Prompt for AI Agents
In template/nestJs/src/auth/token.service.ts around line 154, the call to
Number.parseInt lacks a radix which can lead to ambiguous parsing (e.g., strings
with leading zeros). Change the parsing to include an explicit radix (use 10) so
the line parses DEVICE_LIMIT as a base-10 integer; also consider defaulting or
validating the result (e.g., fallback value or throw) if parseInt returns NaN.

Comment on lines +158 to +167
while (
userTokenCount >= limit
) {
const lastToken = await this.getLastToken(uid);
if (!lastToken) {
break;
}
await this.revokeToken(lastToken.accessToken)
userTokenCount -= 1;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential infinite loop if revokeToken fails silently.

The while loop decrements userTokenCount manually after revokeToken, but if revokeToken fails (e.g., Redis error) without throwing, the count won't reflect reality, and getLastToken may return the same token repeatedly.

Consider adding error handling or re-querying the actual count:

       while (
         userTokenCount >= limit
       ) {
         const lastToken = await this.getLastToken(uid);
         if (!lastToken) {
           break;
         }
         await this.revokeToken(lastToken.accessToken)
-        userTokenCount -= 1;
+        userTokenCount = await this.getUserTokenCount(uid);
       }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In template/nestJs/src/auth/token.service.ts around lines 158 to 167, the loop
can hang if revokeToken fails silently because userTokenCount is decremented
locally while the underlying store is unchanged; wrap the revokeToken call in a
try/catch and on failure either log and rethrow (so the outer flow can fail) or
break, and after a successful revoke re-query the authoritative token count
(e.g., await this.getTokenCount(uid)) instead of only decrementing the local
counter; additionally add a small safety guard (max iterations) to ensure the
loop cannot run indefinitely if the store does not change.

Comment on lines +169 to +177
const multi = redis.multi();
await multi
.set(`rt:${uid}:${token.refreshTokenJTI}`, token.refreshToken)
.set(`at:${uid}:${token.accessTokenJTI}`, token.accessToken)
.pexpire(`rt:${uid}:${token.refreshTokenJTI}`, token.refreshTokenTTL)
.pexpire(`at:${uid}:${token.accessTokenJTI}`, token.accessTokenTTL)
.lpush(`user:${uid}:rt`, token.refreshTokenJTI)
.lpush(`user:${uid}:at`, token.accessTokenJTI)
.exec();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect usage of Redis multi transaction - await before exec() is misplaced.

The await on line 170 is applied before the chained operations, but multi() returns a chainable object that should only be awaited at exec(). This pattern is correct syntactically but stylistically confusing. However, there's a more significant issue: the user:${uid}:rt and user:${uid}:at lists have no TTL set, causing unbounded growth of the JTI lists even after tokens expire.

The token lists will grow indefinitely. Consider setting TTL on the lists or cleaning them up periodically:

     const multi = redis.multi();
-    await multi
+    multi
       .set(`rt:${uid}:${token.refreshTokenJTI}`,  token.refreshToken)
       .set(`at:${uid}:${token.accessTokenJTI}`, token.accessToken)
       .pexpire(`rt:${uid}:${token.refreshTokenJTI}`, token.refreshTokenTTL)
       .pexpire(`at:${uid}:${token.accessTokenJTI}`, token.accessTokenTTL)
       .lpush(`user:${uid}:rt`, token.refreshTokenJTI)
       .lpush(`user:${uid}:at`, token.accessTokenJTI)
-      .exec();
+    await multi.exec();

Additionally, consider adding TTL to the list keys or implementing periodic cleanup.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In template/nestJs/src/auth/token.service.ts around lines 169-177, the Redis
multi usage and list key management are problematic: remove the premature await
on the multi chain and instead build the chain then await multi.exec(), and
ensure you handle exec() errors/results; also prevent unbounded growth of the
user JTI lists by applying TTLs to the list keys (pexpire `user:${uid}:rt` and
`user:${uid}:at`) or add cleanup logic (e.g., ltrim after pushes or a periodic
job to remove expired JTIs) so the lists don’t grow indefinitely.

Comment on lines +24 to +43
export const CONFIG_SCHEMA = Joi.object<Configure>({
DATABASE_HOST: Joi.string(),
DATABASE_PORT: Joi.string(),
DATABASE_USERNAME: Joi.string(),
DATABASE_PASSWORD: Joi.string(),
DATABASE_NAME: Joi.string(),
DATABASE_SYNCHRONIZE: Joi.bool(),
DATABASE_AUTOLOADENTITIES: Joi.bool(),
AUTH_SECRET: Joi.string(),
REDIS_SECONDS: Joi.number(),
REDIS_HOST: Joi.string(),
REDIS_PORT: Joi.string(),
EXPIRES_IN: Joi.string(),
PAGINATION_PAGE: Joi.number(),
PAGINATION_LIMIT: Joi.number(),
GLOBAL_PREFIX: Joi.string(),
MOCK_REGEX: Joi.string(),
REFRESH_TOKEN_TTL: Joi.number(),
DEVICE_LIMIT: Joi.number()
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add .required() to critical configuration fields.

Security-sensitive fields like AUTH_SECRET and database connection fields should be required to prevent silent failures with undefined configuration. The app may start with missing credentials and fail at runtime.

 export const CONFIG_SCHEMA = Joi.object<Configure>({
-  DATABASE_HOST: Joi.string(),
-  DATABASE_PORT: Joi.string(),
-  DATABASE_USERNAME: Joi.string(),
-  DATABASE_PASSWORD: Joi.string(),
-  DATABASE_NAME: Joi.string(),
+  DATABASE_HOST: Joi.string().required(),
+  DATABASE_PORT: Joi.string().required(),
+  DATABASE_USERNAME: Joi.string().required(),
+  DATABASE_PASSWORD: Joi.string().required(),
+  DATABASE_NAME: Joi.string().required(),
   DATABASE_SYNCHRONIZE: Joi.bool(),
   DATABASE_AUTOLOADENTITIES: Joi.bool(),
-  AUTH_SECRET: Joi.string(),
+  AUTH_SECRET: Joi.string().required(),
   REDIS_SECONDS: Joi.number(),
-  REDIS_HOST: Joi.string(),
-  REDIS_PORT: Joi.string(),
+  REDIS_HOST: Joi.string().required(),
+  REDIS_PORT: Joi.string().required(),
   EXPIRES_IN: Joi.string(),
   PAGINATION_PAGE: Joi.number(),
   PAGINATION_LIMIT: Joi.number(),
   GLOBAL_PREFIX: Joi.string(),
   MOCK_REGEX: Joi.string(),
-  REFRESH_TOKEN_TTL: Joi.number(),
-  DEVICE_LIMIT: Joi.number()
+  REFRESH_TOKEN_TTL: Joi.number().required(),
+  DEVICE_LIMIT: Joi.number().required().min(-1)
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const CONFIG_SCHEMA = Joi.object<Configure>({
DATABASE_HOST: Joi.string(),
DATABASE_PORT: Joi.string(),
DATABASE_USERNAME: Joi.string(),
DATABASE_PASSWORD: Joi.string(),
DATABASE_NAME: Joi.string(),
DATABASE_SYNCHRONIZE: Joi.bool(),
DATABASE_AUTOLOADENTITIES: Joi.bool(),
AUTH_SECRET: Joi.string(),
REDIS_SECONDS: Joi.number(),
REDIS_HOST: Joi.string(),
REDIS_PORT: Joi.string(),
EXPIRES_IN: Joi.string(),
PAGINATION_PAGE: Joi.number(),
PAGINATION_LIMIT: Joi.number(),
GLOBAL_PREFIX: Joi.string(),
MOCK_REGEX: Joi.string(),
REFRESH_TOKEN_TTL: Joi.number(),
DEVICE_LIMIT: Joi.number()
})
export const CONFIG_SCHEMA = Joi.object<Configure>({
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.string().required(),
DATABASE_USERNAME: Joi.string().required(),
DATABASE_PASSWORD: Joi.string().required(),
DATABASE_NAME: Joi.string().required(),
DATABASE_SYNCHRONIZE: Joi.bool(),
DATABASE_AUTOLOADENTITIES: Joi.bool(),
AUTH_SECRET: Joi.string().required(),
REDIS_SECONDS: Joi.number(),
REDIS_HOST: Joi.string().required(),
REDIS_PORT: Joi.string().required(),
EXPIRES_IN: Joi.string(),
PAGINATION_PAGE: Joi.number(),
PAGINATION_LIMIT: Joi.number(),
GLOBAL_PREFIX: Joi.string(),
MOCK_REGEX: Joi.string(),
REFRESH_TOKEN_TTL: Joi.number().required(),
DEVICE_LIMIT: Joi.number().required().min(-1)
})
🤖 Prompt for AI Agents
In template/nestJs/src/config-schema.ts around lines 24 to 43, several critical
config keys (e.g., DATABASE_HOST, DATABASE_PORT, DATABASE_USERNAME,
DATABASE_PASSWORD, DATABASE_NAME, AUTH_SECRET, and any TTL/credential values
like REFRESH_TOKEN_TTL) are optional in the Joi schema; update the schema by
appending .required() to those sensitive and connection-related fields so the
app fails fast on missing configuration, leaving non-critical keys optional as
needed and ensuring types remain consistent.

@kagol
Copy link
Member

kagol commented Nov 28, 2025

@GaoNeng-wWw
pnpm dev:backend 无法启动 NestJS 后端,报错信息如下:

/tiny-pro/template/nestJs/dist/main.js:1382
module.exports = require("uuid");
                 ^
Error [ERR_REQUIRE_ESM]: require() of ES Module /tiny-pro/node_modules/.pnpm/uuid@13.0.0/node_modules/uuid/dist-node/index.js from /tiny-pro/template/nestJs/dist/main.js not supported.
Instead change the require of index.js in /tiny-pro/template/nestJs/dist/main.js to a dynamic import() which is available in all CommonJS modules.

可能跟 uuid 这个包有关

@kagol
Copy link
Member

kagol commented Nov 28, 2025

"uuid": "^9.0.0" 改成 9.0 版本好了

@kagol
Copy link
Member

kagol commented Nov 28, 2025

uuid 改成 9.0 版本之后,NestJS 后端启动成功,但是登录失败,报错信息如下:

[NestWinston] 8109 2025/11/28 23:10:27   ERROR [ExceptionsHandler] (0 , uuid_1.v7) is not a function - {
  stack: [
    'TypeError: (0 , uuid_1.v7) is not a function\n' +
      '    at TokenService.createToken (/tiny-pro/template/nestJs/dist/main.js:1292:46)\n' +
      '    at AuthService.login (/tiny-pro/template/nestJs/dist/main.js:1081:47)\n' +
      '    at processTicksAndRejections (node:internal/process/task_queues:95:5)'
  ]
}
image
http://localhost:3031/api/auth/login

{"statusCode":500,"message":"Internal server error"}

@kagol
Copy link
Member

kagol commented Nov 28, 2025

uuid 版本改成 9.0 之后,nestJs/src/auth/token.service.ts 中的代码也需要修改才行

-import { v7 } from "uuid";
+import { v4 as v7 } from "uuid";

以上修改之后可以登录了

kagol
kagol previously approved these changes Nov 30, 2025
@GaoNeng-wWw GaoNeng-wWw dismissed kagol’s stale review November 30, 2025 03:33

The merge-base changed after approval.

@kagol kagol merged commit 89f8396 into opentiny:dev Nov 30, 2025
1 of 3 checks passed
@kagol
Copy link
Member

kagol commented Nov 30, 2025

@GaoNeng-wWw 无法登录,报错信息:

Application is running on: http://[::1]:3000
[NestWinston] 8071 2025/11/30 11:57:22   ERROR [ExceptionsHandler] Invalid setExpirationTime input - {
  stack: [
    'TypeError: Invalid setExpirationTime input\n' +
      '    at validateInput (/tiny-pro/node_modules/.pnpm/@gaonengwww+jose@6.1.0_typescript@5.1.6/node_modules/@gaonengwww/jose/dist/index.js:2988:37)\n' +
      '    at JWTClaimsBuilder.set exp [as exp] (/tiny-pro/node_modules/.pnpm/@gaonengwww+jose@6.1.0_typescript@5.1.6/node_modules/@gaonengwww/jose/dist/index.js:3087:55)\n' +
      '    at SignJWT.setExpirationTime (/tiny-pro/node_modules/.pnpm/@gaonengwww+jose@6.1.0_typescript@5.1.6/node_modules/@gaonengwww/jose/dist/index.js:3623:17)\n' +
      '    at JwtService.sign (/tiny-pro/template/nestJs/dist/main.js:1473:14)\n' +
      '    at TokenService.createToken (/Users/kagol/Documents/Kagol/code/opentiny/tiny-pro/template/nestJs/dist/main.js:1311:45)\n' +
      '    at AuthService.login (/tiny-pro/template/nestJs/dist/main.js:1081:23)'
  ]
}

我尝试在 jwt.service.ts 文件的 sign 方法打印日志,发现点击登录时,会打印两次日志,第一次 expire 和传递给 setExpirationTime 方法的参数都是正常的,第二次 expire 是 undefined,导致传递给 setExpirationTime 方法的参数是 Invalid Date。

====== 2025-11-30T06:04:29.521Z 7200000
====== Invalid Date undefined

@GaoNeng-wWw
Copy link
Collaborator Author

@GaoNeng-wWw 无法登录,报错信息:

Application is running on: http://[::1]:3000
[NestWinston] 8071 2025/11/30 11:57:22   ERROR [ExceptionsHandler] Invalid setExpirationTime input - {
  stack: [
    'TypeError: Invalid setExpirationTime input\n' +
      '    at validateInput (/tiny-pro/node_modules/.pnpm/@gaonengwww+jose@6.1.0_typescript@5.1.6/node_modules/@gaonengwww/jose/dist/index.js:2988:37)\n' +
      '    at JWTClaimsBuilder.set exp [as exp] (/tiny-pro/node_modules/.pnpm/@gaonengwww+jose@6.1.0_typescript@5.1.6/node_modules/@gaonengwww/jose/dist/index.js:3087:55)\n' +
      '    at SignJWT.setExpirationTime (/tiny-pro/node_modules/.pnpm/@gaonengwww+jose@6.1.0_typescript@5.1.6/node_modules/@gaonengwww/jose/dist/index.js:3623:17)\n' +
      '    at JwtService.sign (/tiny-pro/template/nestJs/dist/main.js:1473:14)\n' +
      '    at TokenService.createToken (/Users/kagol/Documents/Kagol/code/opentiny/tiny-pro/template/nestJs/dist/main.js:1311:45)\n' +
      '    at AuthService.login (/tiny-pro/template/nestJs/dist/main.js:1081:23)'
  ]
}

我尝试在 jwt.service.ts 文件的 sign 方法打印日志,发现点击登录时,会打印两次日志,第一次 expire 和传递给 setExpirationTime 方法的参数都是正常的,第二次 expire 是 undefined,导致传递给 setExpirationTime 方法的参数是 Invalid Date。

====== 2025-11-30T06:04:29.521Z 7200000
====== Invalid Date undefined

我待会看下

@kagol
Copy link
Member

kagol commented Nov 30, 2025

好像是环境变量的问题,少了这两个环境变量导致的,我没有更新 nestJs/.env 这个文件,新的环境变量没配置。

REFRESH_TOKEN_TTL = 604800000
# 至多有多少个设备可以同时在线
DEVICE_LIMIT=1

@GaoNeng-wWw
Copy link
Collaborator Author

好像是环境变量的问题,少了这两个环境变量导致的,我没有更新 nestJs/.env 这个文件,新的环境变量没配置。

REFRESH_TOKEN_TTL = 604800000
# 至多有多少个设备可以同时在线
DEVICE_LIMIT=1

加上后还会出现这个问题吗

@kagol kagol mentioned this pull request Dec 6, 2025
13 tasks
@coderabbitai coderabbitai bot mentioned this pull request Dec 6, 2025
13 tasks
@kagol kagol mentioned this pull request Dec 14, 2025
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request unit-test Unit test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments