create-pardx-scaffold 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/template/apps/api/libs/domain/auth/src/auth.service.ts +27 -1
  3. package/template/apps/api/libs/infra/clients/internal/email/dto/email.dto.ts +3 -3
  4. package/template/apps/api/libs/infra/clients/internal/volcengine-tts/dto/tts.dto.ts +4 -4
  5. package/template/apps/api/libs/infra/common/common.module.ts +10 -0
  6. package/template/apps/api/libs/infra/common/config/validation/env.validation.ts +1 -1
  7. package/template/apps/api/libs/infra/common/config/validation/keys.validation.ts +2 -2
  8. package/template/apps/api/libs/infra/common/config/validation/yaml.validation.ts +3 -3
  9. package/template/apps/api/libs/infra/common/decorators/device-info.decorator.ts +58 -0
  10. package/template/apps/api/libs/infra/common/decorators/team-info.decorator.ts +122 -0
  11. package/template/apps/api/libs/infra/common/encryption.service.ts +70 -0
  12. package/template/apps/api/libs/infra/common/index.ts +9 -0
  13. package/template/apps/api/libs/infra/shared-services/email/dto/email.dto.ts +3 -3
  14. package/template/apps/api/libs/infra/shared-services/email/email.service.ts +5 -1
  15. package/template/apps/api/libs/infra/shared-services/notification/index.ts +13 -0
  16. package/template/apps/api/libs/infra/shared-services/notification/notification.module.ts +10 -0
  17. package/template/apps/api/libs/infra/shared-services/notification/notification.service.ts +791 -0
  18. package/template/apps/web/components/client-only.tsx +28 -0
  19. package/template/apps/web/components/index.ts +23 -0
  20. package/template/apps/web/components/layout/app-navbar.tsx +109 -0
  21. package/template/apps/web/components/layout/app-shell.tsx +30 -0
  22. package/template/apps/web/components/layout/app-sidebar.tsx +206 -0
  23. package/template/apps/web/components/layout/index.ts +4 -0
  24. package/template/apps/web/components/layout/locale-switcher.tsx +57 -0
  25. package/template/apps/web/components/runtime-i18n-bridge.tsx +32 -0
  26. package/template/apps/web/components/state-components.tsx +214 -0
  27. package/template/apps/web/config.ts +22 -2
  28. package/template/apps/web/lib/api/cache-config.ts +32 -0
  29. package/template/apps/web/lib/api/contracts/client.ts +43 -1
  30. package/template/apps/web/lib/api/contracts/hooks/analytics.ts +32 -0
  31. package/template/apps/web/lib/api/contracts/hooks/index.ts +41 -2
  32. package/template/apps/web/lib/api/contracts/hooks/message.ts +60 -0
  33. package/template/apps/web/lib/api/contracts/hooks/system.ts +42 -0
  34. package/template/apps/web/lib/api/contracts/hooks/task.ts +54 -0
  35. package/template/apps/web/lib/api/contracts/hooks/user.ts +45 -0
  36. package/template/apps/web/lib/api/contracts/server-client.ts +1 -1
  37. package/template/apps/web/lib/api/prefetch.ts +128 -0
  38. package/template/apps/web/lib/api/query-client.ts +37 -0
  39. package/template/apps/web/lib/i18n/runtime-translator.ts +48 -0
  40. package/template/apps/web/lib/requests.ts +1 -1
  41. package/template/apps/web/providers/app-provider.tsx +1 -1
  42. package/template/apps/web/providers/auth-provider.tsx +228 -0
  43. package/template/apps/web/providers/index.tsx +28 -9
  44. package/template/apps/web/providers/intl-client-provider.tsx +43 -0
  45. package/template/apps/web/vitest.config.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pardx-scaffold",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Scaffold a new project from PardxAI monorepo (git-tracked files)",
5
5
  "license": "MIT",
6
6
  "bin": "./cli.js",
@@ -97,13 +97,39 @@ export class AuthService {
97
97
  await this.redis.saveData('refresh', tokens.refresh, tokens.userId);
98
98
  await this.redis.saveData('access', tokens.access, tokens.userId);
99
99
 
100
+ // Convert avatarFileId to headerImg URL
101
+ let headerImg: string | null = null;
102
+ if (user.avatarFileId) {
103
+ const avatarFile = await this.fileSource.get({
104
+ id: user.avatarFileId,
105
+ });
106
+ if (avatarFile) {
107
+ headerImg = await this.fileCdn.getImageVolcengineCdn(
108
+ avatarFile.vendor,
109
+ avatarFile.bucket,
110
+ avatarFile.key,
111
+ '360:360:360:360',
112
+ );
113
+ }
114
+ }
115
+
100
116
  return {
101
117
  refresh: tokens.refresh,
102
118
  expire: tokens.expire,
103
119
  access: tokens.access,
104
120
  accessExpire: tokens.accessExpire,
105
121
  isAnonymity: tokens.isAnonymity,
106
- user,
122
+ user: {
123
+ id: user.id!,
124
+ isAnonymity: user.isAnonymity,
125
+ isAdmin: user.isAdmin,
126
+ code: user.code,
127
+ nickname: user.nickname,
128
+ headerImg,
129
+ sex: user.sex,
130
+ mobile: user.mobile,
131
+ email: user.email,
132
+ },
107
133
  };
108
134
  }
109
135
 
@@ -21,7 +21,7 @@ export const EmailTemplateSchema = z.object({
21
21
  templateInvokeName: z.string(),
22
22
  codeExpire: z.number().optional(),
23
23
  frequency: z.number().optional(),
24
- sub: z.record(z.string()).nullable().optional(),
24
+ sub: z.record(z.string(), z.string()).nullable().optional(),
25
25
  });
26
26
 
27
27
  export const EmailConfigSchema = z.object({
@@ -46,10 +46,10 @@ export const SignalMessageSchema = z.object({
46
46
  to: z.string().email(),
47
47
  subject: z.string().optional(),
48
48
  templateInvokeName: z.string(),
49
- sub: z.record(z.array(z.string())),
49
+ sub: z.record(z.string(), z.array(z.string())),
50
50
  options: z.any().optional(),
51
51
  queueMailId: z.string().optional(),
52
- metadata: z.record(z.any()).optional(),
52
+ metadata: z.record(z.string(), z.any()).optional(),
53
53
  });
54
54
 
55
55
  // ============================================================================
@@ -41,10 +41,10 @@ export type TtsResult = z.infer<typeof TtsResultSchema>;
41
41
  export class TtsRequestDto implements TtsRequest {
42
42
  text: string;
43
43
  speaker?: string;
44
- format?: string = 'mp3';
45
- speech_rate?: number = 0;
46
- loudness_rate?: number = 0;
47
- pitch?: number = 0;
44
+ format: string = 'mp3';
45
+ speech_rate: number = 0;
46
+ loudness_rate: number = 0;
47
+ pitch: number = 0;
48
48
  }
49
49
 
50
50
  export class TtsResponseDto implements TtsResponse {
@@ -0,0 +1,10 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { ConfigModule } from '@nestjs/config';
3
+ import { EncryptionService } from './encryption.service';
4
+
5
+ @Module({
6
+ imports: [ConfigModule],
7
+ providers: [EncryptionService],
8
+ exports: [EncryptionService],
9
+ })
10
+ export class CommonModule {}
@@ -83,7 +83,7 @@ export function validateEnv(): EnvConfig {
83
83
  const result = envSchema.safeParse(expandedEnv);
84
84
 
85
85
  if (!result.success) {
86
- const errorMessages = result.error.errors
86
+ const errorMessages = result.error.issues
87
87
  .map((err) => ` - ${err.path.join('.')}: ${err.message}`)
88
88
  .join('\n');
89
89
 
@@ -96,7 +96,7 @@ export const smsProviderSchema = z.object({
96
96
  region: z.string().optional(),
97
97
  appKey: z.string().optional(),
98
98
  appCode: z.string().optional(),
99
- templates: z.array(z.record(z.any())).optional(),
99
+ templates: z.array(z.record(z.string(), z.any())).optional(),
100
100
  });
101
101
 
102
102
  export const smsConfigSchema = z.object({
@@ -460,7 +460,7 @@ export function validateKeysConfig(config: unknown): KeysConfig {
460
460
 
461
461
  if (!result.success) {
462
462
  // Mask sensitive data in error messages
463
- const errorMessages = result.error.errors
463
+ const errorMessages = result.error.issues
464
464
  .map((err) => {
465
465
  const path = err.path.join('.');
466
466
  // Don't log actual values for security
@@ -185,7 +185,7 @@ export const unleashConfigSchema = z.object({
185
185
  /** Metrics reporting interval in milliseconds (default: 60000) */
186
186
  metricsInterval: z.number().int().positive().default(60000),
187
187
  /** Custom headers for authentication */
188
- customHeaders: z.record(z.string()).optional(),
188
+ customHeaders: z.record(z.string(), z.string()).optional(),
189
189
  });
190
190
 
191
191
  /**
@@ -200,7 +200,7 @@ export const featureFlagsConfigSchema = z
200
200
  /** Unleash configuration (required if provider is 'unleash') */
201
201
  unleash: unleashConfigSchema.optional(),
202
202
  /** Default feature flags (key-value pairs) */
203
- defaultFlags: z.record(z.boolean()).optional(),
203
+ defaultFlags: z.record(z.string(), z.boolean()).optional(),
204
204
  })
205
205
  .refine(
206
206
  (data) => {
@@ -516,7 +516,7 @@ export function validateYamlConfig(config: unknown): YamlConfig {
516
516
  const result = yamlConfigSchema.safeParse(config);
517
517
 
518
518
  if (!result.success) {
519
- const errorMessages = result.error.errors
519
+ const errorMessages = result.error.issues
520
520
  .map((err) => ` - ${err.path.join('.')}: ${err.message}`)
521
521
  .join('\n');
522
522
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Device Info Decorator
3
+ * 设备信息装饰器 - 从请求头提取设备信息
4
+ *
5
+ * 使用方式:
6
+ * ```typescript
7
+ * @TsRestHandler(c.loginByEmail)
8
+ * async loginByEmail(@DeviceInfo() deviceInfo: PardxApp.HeaderData) {
9
+ * // deviceInfo 自动注入
10
+ * const result = await this.signService.loginByEmail(body, deviceInfo);
11
+ * return success(result);
12
+ * }
13
+ * ```
14
+ */
15
+
16
+ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
17
+ import { FastifyRequest } from 'fastify';
18
+ import { PardxApp } from '@/config/dto/config.dto';
19
+
20
+ /**
21
+ * @DeviceInfo() decorator - 从请求头提取设备信息
22
+ *
23
+ * 自动从以下请求头提取:
24
+ * - x-platform: 平台标识
25
+ * - x-os: 操作系统
26
+ * - x-device-id: 设备ID
27
+ * - x-mptrail: 营销追踪参数
28
+ *
29
+ * @example
30
+ * // 在控制器方法中使用
31
+ * async login(@DeviceInfo() deviceInfo: PardxApp.HeaderData) {
32
+ * console.log(deviceInfo.platform, deviceInfo.os, deviceInfo.deviceid);
33
+ * }
34
+ */
35
+ export const DeviceInfo = createParamDecorator(
36
+ (data: unknown, ctx: ExecutionContext): PardxApp.HeaderData => {
37
+ const request = ctx.switchToHttp().getRequest<FastifyRequest>();
38
+ const headers = request.headers;
39
+
40
+ return {
41
+ platform: (headers['x-platform'] as string) || '',
42
+ os: (headers['x-os'] as string) || '',
43
+ deviceid: (headers['x-device-id'] as string) || '',
44
+ mptrail: headers['x-mptrail'] as string,
45
+ };
46
+ },
47
+ );
48
+
49
+ /**
50
+ * Get device ID from request headers
51
+ * 从请求头获取设备ID的辅助函数
52
+ *
53
+ * @param request - Fastify request object
54
+ * @returns Device ID string or 'unknown'
55
+ */
56
+ export function getDeviceId(request: FastifyRequest): string {
57
+ return (request.headers['x-device-id'] as string) || 'unknown';
58
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Team Info Decorator
3
+ * 团队信息装饰器 - 从请求上下文提取团队信息
4
+ *
5
+ * 使用方式:
6
+ * ```typescript
7
+ * @TsRestHandler(c.getTeamStatistics)
8
+ * async getTeamStatistics(@TeamInfo() teamInfo: TeamContext) {
9
+ * // teamInfo 自动注入
10
+ * const result = await this.teamDomainService.getTeamStatistics(teamInfo.teamId, teamInfo.userId);
11
+ * return success(result);
12
+ * }
13
+ * ```
14
+ */
15
+
16
+ import {
17
+ createParamDecorator,
18
+ ExecutionContext,
19
+ UnauthorizedException,
20
+ } from '@nestjs/common';
21
+ import { FastifyRequest } from 'fastify';
22
+
23
+ /**
24
+ * Team context extracted from request
25
+ */
26
+ export interface TeamContext {
27
+ /** Team ID (from API Key or user session) */
28
+ teamId: string;
29
+ /** User ID (from JWT or API Key) */
30
+ userId: string;
31
+ /** User's role in the team (if available) */
32
+ role?: 'owner' | 'admin' | 'member';
33
+ /** API Key ID (if using API Key authentication) */
34
+ userApiKeyId?: string;
35
+ }
36
+
37
+ /**
38
+ * @TeamInfo() decorator - 从请求上下文提取团队信息
39
+ *
40
+ * 自动从以下来源提取:
41
+ * 1. API Gateway: API Key 验证后的 request.teamId
42
+ * 2. Web App: JWT payload 或 request.teamId
43
+ *
44
+ * @example
45
+ * // 在控制器方法中使用
46
+ * async getTeamStats(@TeamInfo() teamInfo: TeamContext) {
47
+ * console.log(teamInfo.teamId, teamInfo.userId);
48
+ * }
49
+ */
50
+ export const TeamInfo = createParamDecorator(
51
+ (data: unknown, ctx: ExecutionContext): TeamContext => {
52
+ const request = ctx.switchToHttp().getRequest<FastifyRequest>();
53
+
54
+ // 优先从 API Key 验证结果获取(Gateway 场景)
55
+ const teamId = (request as any).teamId;
56
+ const userId = (request as any).userId;
57
+ const userApiKeyId = (request as any).userApiKeyId;
58
+
59
+ // 如果没有 teamId,尝试从 userInfo 获取
60
+ const userInfo = (request as any).userInfo;
61
+ const resolvedUserId = userId || userInfo?.id;
62
+
63
+ if (!resolvedUserId) {
64
+ throw new UnauthorizedException('User not authenticated');
65
+ }
66
+
67
+ // 如果没有 teamId,使用 userId 作为 fallback(用户默认团队)
68
+ const resolvedTeamId = teamId || resolvedUserId;
69
+
70
+ return {
71
+ teamId: resolvedTeamId,
72
+ userId: resolvedUserId,
73
+ userApiKeyId,
74
+ role: (request as any).teamRole,
75
+ };
76
+ },
77
+ );
78
+
79
+ /**
80
+ * Get team ID from request context
81
+ * 从请求上下文获取团队ID的辅助函数
82
+ *
83
+ * @param request - Fastify request object
84
+ * @returns Team ID string
85
+ */
86
+ export function getTeamId(request: FastifyRequest): string {
87
+ // 优先使用 teamId(API Key 或 session 设置)
88
+ const teamId = (request as any).teamId;
89
+ if (teamId) return teamId;
90
+
91
+ // Fallback to userId(默认团队)
92
+ const userId = (request as any).userId || (request as any).userInfo?.id;
93
+ if (!userId) {
94
+ throw new Error('getTeamId: neither teamId nor userId found in request');
95
+ }
96
+
97
+ return userId;
98
+ }
99
+
100
+ /**
101
+ * Get team context from request
102
+ * 从请求获取完整团队上下文的辅助函数
103
+ *
104
+ * @param request - Fastify request object
105
+ * @returns TeamContext object
106
+ */
107
+ export function getTeamContext(request: FastifyRequest): TeamContext {
108
+ const teamId = (request as any).teamId;
109
+ const userId = (request as any).userId || (request as any).userInfo?.id;
110
+ const userApiKeyId = (request as any).userApiKeyId;
111
+
112
+ if (!userId) {
113
+ throw new Error('getTeamContext: userId not found in request');
114
+ }
115
+
116
+ return {
117
+ teamId: teamId || userId, // Fallback to userId for default team
118
+ userId,
119
+ userApiKeyId,
120
+ role: (request as any).teamRole,
121
+ };
122
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Encryption Service
3
+ *
4
+ * AES-256-CBC encryption for provider API keys.
5
+ * SHA-256 hashing for user API key storage.
6
+ *
7
+ * Supports two stored formats for backward compatibility:
8
+ * - Legacy: "ivHex:cipherTextHex" (raw hex strings joined by colon)
9
+ * - Current: OpenSSL Base64 (crypto-js native serialization)
10
+ */
11
+ import { Injectable } from '@nestjs/common';
12
+ import { ConfigService } from '@nestjs/config';
13
+ import * as crypto from 'crypto-js';
14
+
15
+ @Injectable()
16
+ export class EncryptionService {
17
+ private readonly encryptionKey: string;
18
+
19
+ constructor(private readonly configService: ConfigService) {
20
+ this.encryptionKey = this.configService.get<string>(
21
+ 'ENCRYPTION_KEY',
22
+ '12345678901234567890123456789012',
23
+ );
24
+ if (!this.encryptionKey || this.encryptionKey.length < 32) {
25
+ throw new Error('ENCRYPTION_KEY must be at least 32 characters');
26
+ }
27
+ }
28
+
29
+ encrypt(plainText: string): Uint8Array {
30
+ const iv = crypto.lib.WordArray.random(16);
31
+ const encrypted = crypto.AES.encrypt(plainText, this.encryptionKey, {
32
+ iv: iv,
33
+ mode: crypto.mode.CBC,
34
+ padding: crypto.pad.Pkcs7,
35
+ });
36
+
37
+ // Store as OpenSSL format (Base64) which is the native crypto-js serialization
38
+ const combined = encrypted.toString();
39
+ const bytes = Buffer.from(combined, 'utf-8');
40
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
41
+ }
42
+
43
+ decrypt(encryptedBuffer: Uint8Array): string {
44
+ const combined = Buffer.from(encryptedBuffer).toString('utf-8');
45
+
46
+ // Legacy format: "ivHex:cipherTextHex" (contains a colon)
47
+ // crypto-js.decrypt treats strings as Base64/OpenSSL, so hex ciphertext
48
+ // must be wrapped in a CipherParams object for correct parsing.
49
+ if (combined.includes(':')) {
50
+ const [ivHex, cipherTextHex] = combined.split(':');
51
+ const cipherParams = crypto.lib.CipherParams.create({
52
+ ciphertext: crypto.enc.Hex.parse(cipherTextHex),
53
+ });
54
+ const decrypted = crypto.AES.decrypt(cipherParams, this.encryptionKey, {
55
+ iv: crypto.enc.Hex.parse(ivHex),
56
+ mode: crypto.mode.CBC,
57
+ padding: crypto.pad.Pkcs7,
58
+ });
59
+ return decrypted.toString(crypto.enc.Utf8);
60
+ }
61
+
62
+ // Current format: OpenSSL Base64
63
+ const decrypted = crypto.AES.decrypt(combined, this.encryptionKey);
64
+ return decrypted.toString(crypto.enc.Utf8);
65
+ }
66
+
67
+ hash(input: string): string {
68
+ return crypto.SHA256(input).toString();
69
+ }
70
+ }
@@ -0,0 +1,9 @@
1
+ export { CommonModule } from './common.module';
2
+ export { EncryptionService } from './encryption.service';
3
+ export { DeviceInfo, getDeviceId } from './decorators/device-info.decorator';
4
+ export {
5
+ TeamInfo,
6
+ getTeamId,
7
+ getTeamContext,
8
+ type TeamContext,
9
+ } from './decorators/team-info.decorator';
@@ -21,7 +21,7 @@ export const EmailTemplateSchema = z.object({
21
21
  templateInvokeName: z.string(),
22
22
  codeExpire: z.number().optional(),
23
23
  frequency: z.number().optional(),
24
- sub: z.record(z.string()).nullable().optional(),
24
+ sub: z.record(z.string(), z.string()).nullable().optional(),
25
25
  });
26
26
 
27
27
  export const EmailConfigSchema = z.object({
@@ -46,10 +46,10 @@ export const SignalMessageSchema = z.object({
46
46
  to: z.string().email(),
47
47
  subject: z.string().optional(),
48
48
  templateInvokeName: z.string(),
49
- sub: z.record(z.array(z.string())),
49
+ sub: z.record(z.string(), z.array(z.string())),
50
50
  options: z.any().optional(),
51
51
  queueMailId: z.string().optional(),
52
- metadata: z.record(z.any()).optional(),
52
+ metadata: z.record(z.string(), z.any()).optional(),
53
53
  });
54
54
 
55
55
  // ============================================================================
@@ -236,7 +236,11 @@ export class EmailService implements OnModuleInit {
236
236
  const subVery = {};
237
237
  if (sub && subValues) {
238
238
  for (const key in sub) {
239
- subVery['%' + sub[key] + '%'] = [subValues[sub[key]]];
239
+ const subKey = sub[key as keyof typeof sub];
240
+ const subValueKey = subValues[subKey as keyof typeof subValues];
241
+ if (subKey && subValueKey) {
242
+ subVery['%' + subKey + '%'] = [subValueKey];
243
+ }
240
244
  }
241
245
  }
242
246
  return {
@@ -0,0 +1,13 @@
1
+ export { NotificationModule } from './notification.module';
2
+ export {
3
+ NotificationService,
4
+ type NotificationChannel,
5
+ type NotificationPriority,
6
+ type NotificationStatus,
7
+ type NotificationRequest,
8
+ type NotificationRecord,
9
+ type NotificationTemplate,
10
+ type NotificationPreferences,
11
+ type NotificationStats,
12
+ type WebhookPayload,
13
+ } from './notification.service';
@@ -0,0 +1,10 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { HttpModule } from '@nestjs/axios';
3
+ import { NotificationService } from './notification.service';
4
+
5
+ @Module({
6
+ imports: [HttpModule.register({ timeout: 10000 })],
7
+ providers: [NotificationService],
8
+ exports: [NotificationService],
9
+ })
10
+ export class NotificationModule {}