Skip to content

Commit a1a0b30

Browse files
committed
feat(migration): add v9.6.3 migration and enhance SMTP options handling
- Introduced a new migration script for version 9.6.3 to flatten SMTP options in the database. - Updated the configuration schema to directly include SMTP host, port, and secure settings, improving clarity and usability. - Enhanced the email service to utilize the new SMTP configuration structure, ensuring proper handling of email notifications. These changes improve the email configuration process and streamline the migration to the latest version. Signed-off-by: Innei <tukon479@gmail.com>
1 parent 6101bc9 commit a1a0b30

File tree

6 files changed

+97
-23
lines changed

6 files changed

+97
-23
lines changed

apps/core/src/migration/history.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import v9_3_2 from './version/v9.3.2'
1919
import v9_4_1 from './version/v9.4.1'
2020
import v9_5_0 from './version/v9.5.0'
2121
import v9_6_0 from './version/v9.6.0'
22+
import v9_6_3 from './version/v9.6.3'
2223

2324
export default [
2425
v200Alpha1,
@@ -42,4 +43,5 @@ export default [
4243
v9_4_1,
4344
v9_5_0,
4445
v9_6_0,
46+
v9_6_3,
4547
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Db } from 'mongodb'
2+
import { defineMigration } from '../helper'
3+
4+
export default defineMigration(
5+
'v9.6.3-flatten-smtp-options',
6+
async (db: Db) => {
7+
const optionsCollection = db.collection('options')
8+
9+
const mailOptionsDoc = await optionsCollection.findOne({
10+
name: 'mailOptions',
11+
})
12+
13+
if (mailOptionsDoc?.value?.smtp?.options) {
14+
const { smtp } = mailOptionsDoc.value as any
15+
const { options, ...restSmtp } = smtp
16+
17+
await optionsCollection.updateOne(
18+
{ name: 'mailOptions' },
19+
{
20+
$set: {
21+
'value.smtp': {
22+
...restSmtp,
23+
host: options?.host ?? '',
24+
port: options?.port ?? 465,
25+
secure: options?.secure ?? true,
26+
},
27+
},
28+
},
29+
)
30+
}
31+
},
32+
)

apps/core/src/modules/configs/configs.encrypt.util.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,48 @@ export const sanitizeConfigForResponse = <T extends object>(
316316

317317
return result
318318
}
319+
320+
/**
321+
* Remove empty string values from encrypted fields
322+
* This prevents empty values from overwriting existing encrypted data during merge
323+
*/
324+
export const removeEmptyEncryptedFields = <T extends object>(
325+
target: T,
326+
prefix = '',
327+
): T => {
328+
const result = { ...target }
329+
const keys = Object.keys(result)
330+
331+
for (const key of keys) {
332+
const currentPath = prefix ? `${prefix}.${key}` : key
333+
const value = (result as any)[key]
334+
335+
if (Array.isArray(value)) {
336+
;(result as any)[key] = value
337+
.map((item, i) => {
338+
const itemPath = `${currentPath}.${i}`
339+
if (isObject(item) && !isArrayLike(item)) {
340+
return removeEmptyEncryptedFields(item, itemPath)
341+
} else if (
342+
typeof item === 'string' &&
343+
item === '' &&
344+
isEncryptedPath(itemPath)
345+
) {
346+
return undefined
347+
}
348+
return item
349+
})
350+
.filter((item) => item !== undefined)
351+
} else if (isObject(value) && !isArrayLike(value)) {
352+
;(result as any)[key] = removeEmptyEncryptedFields(value, currentPath)
353+
} else if (
354+
typeof value === 'string' &&
355+
value === '' &&
356+
isEncryptedPath(currentPath)
357+
) {
358+
delete (result as any)[key]
359+
}
360+
}
361+
362+
return result
363+
}

apps/core/src/modules/configs/configs.schema.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,21 @@ export class UrlDto extends createZodDto(UrlSchema) {}
2424
export type UrlConfig = z.infer<typeof UrlSchema>
2525

2626
// ==================== Mail Options ====================
27-
const SmtpOptionsSchema = z.object({
28-
port: field.number(
29-
z.preprocess(
30-
(val) => (val ? Number(val) : val),
31-
z.number().int().optional(),
32-
),
33-
'SMTP 端口',
34-
{ 'ui:options': { halfGrid: true } },
35-
),
36-
host: field.halfGrid(
37-
z.url({ message: 'host must be a valid URL' }).optional().or(z.literal('')),
38-
'SMTP 主机',
39-
),
40-
secure: field.toggle(z.boolean().optional(), '使用 SSL/TLS'),
41-
})
42-
4327
const SmtpConfigSchema = withMeta(
4428
z
4529
.object({
4630
user: field.halfGrid(z.string().optional(), 'SMTP 用户名'),
4731
pass: field.passwordHalfGrid(z.string().min(1).optional(), 'SMTP 密码'),
48-
options: withMeta(SmtpOptionsSchema.optional(), {
49-
'ui:options': { connect: true },
50-
}),
32+
host: field.halfGrid(z.string().optional(), 'SMTP 主机'),
33+
port: field.number(
34+
z.preprocess(
35+
(val) => (val ? Number(val) : val),
36+
z.number().int().optional(),
37+
),
38+
'SMTP 端口',
39+
{ 'ui:options': { halfGrid: true } },
40+
),
41+
secure: field.toggle(z.boolean().optional(), '使用 SSL/TLS'),
5142
})
5243
.optional(),
5344
{

apps/core/src/modules/configs/configs.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { generateDefaultConfig } from './configs.default'
1919
import {
2020
decryptObject,
2121
encryptObject,
22+
removeEmptyEncryptedFields,
2223
sanitizeConfigForResponse,
2324
} from './configs.encrypt.util'
2425
import { configDtoMapping, IConfig } from './configs.interface'
@@ -203,6 +204,9 @@ export class ConfigsService {
203204
value: Partial<IConfig[T]>,
204205
) {
205206
value = camelcaseKeys(value) as any
207+
value = removeEmptyEncryptedFields(value as object, key) as Partial<
208+
IConfig[T]
209+
>
206210

207211
if (key === 'ai') {
208212
value = await this.hydrateAiProviderApiKeys(value as any)

apps/core/src/processors/helper/helper.email.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,16 @@ export class EmailService implements OnModuleInit, OnModuleDestroy {
152152
}
153153
} else {
154154
const { smtp } = mailOptions
155-
const { user, pass, options } = smtp || {}
155+
const { user, pass, host, port, secure } = smtp || {}
156156
if (!user && !pass) {
157157
const message = '未启动邮件通知'
158158
this.logger.warn(message)
159159
return
160160
}
161161
this.instance = createTransport({
162-
host: options?.host || '',
163-
port: Number.parseInt((options?.port as any) || '465'),
164-
secure: options?.secure,
162+
host: host || '',
163+
port: Number.parseInt((port as any) || '465'),
164+
secure,
165165
auth: { user, pass },
166166
tls: {
167167
rejectUnauthorized: false,

0 commit comments

Comments
 (0)