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.
- package/package.json +1 -1
- package/template/apps/api/libs/domain/auth/src/auth.service.ts +27 -1
- package/template/apps/api/libs/infra/clients/internal/email/dto/email.dto.ts +3 -3
- package/template/apps/api/libs/infra/clients/internal/volcengine-tts/dto/tts.dto.ts +4 -4
- package/template/apps/api/libs/infra/common/common.module.ts +10 -0
- package/template/apps/api/libs/infra/common/config/validation/env.validation.ts +1 -1
- package/template/apps/api/libs/infra/common/config/validation/keys.validation.ts +2 -2
- package/template/apps/api/libs/infra/common/config/validation/yaml.validation.ts +3 -3
- package/template/apps/api/libs/infra/common/decorators/device-info.decorator.ts +58 -0
- package/template/apps/api/libs/infra/common/decorators/team-info.decorator.ts +122 -0
- package/template/apps/api/libs/infra/common/encryption.service.ts +70 -0
- package/template/apps/api/libs/infra/common/index.ts +9 -0
- package/template/apps/api/libs/infra/shared-services/email/dto/email.dto.ts +3 -3
- package/template/apps/api/libs/infra/shared-services/email/email.service.ts +5 -1
- package/template/apps/api/libs/infra/shared-services/notification/index.ts +13 -0
- package/template/apps/api/libs/infra/shared-services/notification/notification.module.ts +10 -0
- package/template/apps/api/libs/infra/shared-services/notification/notification.service.ts +791 -0
- package/template/apps/web/components/client-only.tsx +28 -0
- package/template/apps/web/components/index.ts +23 -0
- package/template/apps/web/components/layout/app-navbar.tsx +109 -0
- package/template/apps/web/components/layout/app-shell.tsx +30 -0
- package/template/apps/web/components/layout/app-sidebar.tsx +206 -0
- package/template/apps/web/components/layout/index.ts +4 -0
- package/template/apps/web/components/layout/locale-switcher.tsx +57 -0
- package/template/apps/web/components/runtime-i18n-bridge.tsx +32 -0
- package/template/apps/web/components/state-components.tsx +214 -0
- package/template/apps/web/config.ts +22 -2
- package/template/apps/web/lib/api/cache-config.ts +32 -0
- package/template/apps/web/lib/api/contracts/client.ts +43 -1
- package/template/apps/web/lib/api/contracts/hooks/analytics.ts +32 -0
- package/template/apps/web/lib/api/contracts/hooks/index.ts +41 -2
- package/template/apps/web/lib/api/contracts/hooks/message.ts +60 -0
- package/template/apps/web/lib/api/contracts/hooks/system.ts +42 -0
- package/template/apps/web/lib/api/contracts/hooks/task.ts +54 -0
- package/template/apps/web/lib/api/contracts/hooks/user.ts +45 -0
- package/template/apps/web/lib/api/contracts/server-client.ts +1 -1
- package/template/apps/web/lib/api/prefetch.ts +128 -0
- package/template/apps/web/lib/api/query-client.ts +37 -0
- package/template/apps/web/lib/i18n/runtime-translator.ts +48 -0
- package/template/apps/web/lib/requests.ts +1 -1
- package/template/apps/web/providers/app-provider.tsx +1 -1
- package/template/apps/web/providers/auth-provider.tsx +228 -0
- package/template/apps/web/providers/index.tsx +28 -9
- package/template/apps/web/providers/intl-client-provider.tsx +43 -0
- package/template/apps/web/vitest.config.ts +4 -0
package/package.json
CHANGED
|
@@ -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
|
|
45
|
-
speech_rate
|
|
46
|
-
loudness_rate
|
|
47
|
-
pitch
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 {}
|