修复开发模式匿名恢复路由
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-16 13:16:55 +08:00
parent 581e70b2ca
commit 6146ddba5d
4 changed files with 55 additions and 15 deletions

View File

@ -90,10 +90,15 @@ export function readDevelopmentRuntime(root = runtimeRoot, { previous = false }
if (typeof webDomain !== 'string' || !/^[a-z0-9.-]+(?::\d+)?$/i.test(webDomain)) {
throw new Error('请先在 yunx-test/config/config.json 配置有效的 Web 域名')
}
const mailDomain = config.domains?.primary || webDomain
if (typeof mailDomain !== 'string' || !/^[a-z0-9.-]+(?::\d+)?$/i.test(mailDomain)) {
throw new Error('请先在 yunx-test/config/config.json 配置有效的主邮件域名')
}
const certificatePath = resolveDevelopmentCertificate(root, previous ? ssl.tls?.previousCertificateRef : ssl.tls?.certificateRef)
const privateKeyPath = resolveDevelopmentCertificate(root, previous ? ssl.tls?.previousPrivateKeyRef : ssl.tls?.privateKeyRef, true)
return {
origin: `https://${webDomain}`,
mailOrigin: `https://${mailDomain}`,
certificatePath,
privateKeyPath,
generation: config.generation

View File

@ -30,6 +30,21 @@ const developmentHTTPRedirect = (frontendOrigin, host = '127.0.0.1', port = 80)
}
})
const developmentBackendProxy = (backendUrl, backendOrigin) => ({
target: backendUrl,
changeOrigin: true,
secure: false,
configure: (proxy) => {
proxy.on('proxyReq', (proxyRequest) => {
const originURL = new URL(backendOrigin)
proxyRequest.setHeader('origin', backendOrigin)
proxyRequest.setHeader('host', originURL.host)
proxyRequest.setHeader('x-forwarded-proto', originURL.protocol.replace(':', ''))
proxyRequest.removeHeader('referer')
})
}
})
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd(), '')
@ -37,6 +52,7 @@ export default defineConfig(({ mode, command }) => {
const developmentTLS = command === 'serve' ? readDevelopmentTLS() : undefined
const runtime = developmentTLS?.runtime
const backendOrigin = runtime?.origin
const publicRecoveryOrigin = runtime?.mailOrigin
const frontendOrigin = runtime?.origin
return {
@ -64,20 +80,10 @@ export default defineConfig(({ mode, command }) => {
https: developmentTLS?.options,
cors: true,
proxy: {
"/api": {
target: backendUrl,
changeOrigin: true,
secure: false,
configure: (proxy) => {
proxy.on('proxyReq', (proxyRequest) => {
const originURL = new URL(backendOrigin)
proxyRequest.setHeader('origin', backendOrigin)
proxyRequest.setHeader('host', originURL.host)
proxyRequest.setHeader('x-forwarded-proto', originURL.protocol.replace(':', ''))
proxyRequest.removeHeader('referer')
})
}
},
"/api": developmentBackendProxy(backendUrl, backendOrigin),
// 匿名恢复入口由后端按 Web 域名计算跳转,恢复 API 则必须保持主邮件域名的同源校验。
"/public-password-recovery": developmentBackendProxy(backendUrl, backendOrigin),
"/rest/api/recovery": developmentBackendProxy(backendUrl, publicRecoveryOrigin),
"/attachments": {
target: backendUrl,
changeOrigin: true,

View File

@ -30,16 +30,24 @@ test('前端构建必须自动同步 Go 嵌入资源', async () => {
test('前端入口按需加载 Element Plus,并延迟设置页和写信编辑器', async () => {
const main = await read('frontend/src/main.js')
const mainApp = await read('frontend/src/main-app.js')
const vite = await read('frontend/vite.config.js')
const header = await read('frontend/src/views/components/HomeHeader/HomeHeader.js')
assert.doesNotMatch(main, /app\.use\(ElementPlus\)/)
assert.doesNotMatch(main, /element-plus\/dist\/index\.css/)
assert.match(main, /element-services\.css/)
assert.match(main, /public-recovery\.js/)
assert.match(mainApp, /element-services\.css/)
assert.match(vite, /manualChunks\(id\)/)
assert.match(header, /defineAsyncComponent/)
assert.match(header, /import\('\.\.\/ConfigSettings\/ConfigSettings\.vue'\)/)
})
test('开发服务器将匿名恢复入口和接口安全转发到后端', async () => {
const vite = await read('frontend/vite.config.js')
assert.match(vite, /"\/public-password-recovery": developmentBackendProxy\(backendUrl, backendOrigin\)/)
assert.match(vite, /"\/rest\/api\/recovery": developmentBackendProxy\(backendUrl, publicRecoveryOrigin\)/)
})
test('开发启动必须固定运行配置根目录并集中保存日志', async () => {
const content = await read('deploy/start-dev.ps1')
assert.match(content, /\$env:SMAIL_ROOT = \$runtime\r?\n/)

View File

@ -21,6 +21,7 @@ test('开发配置只从模拟部署目录读取且不返回凭据字段', async
await writeFile(path.join(root, 'ssl/private.key'), 'key', { mode: 0o600 })
const runtime = readDevelopmentRuntime(root)
assert.equal(runtime.origin, 'https://mail.example.test')
assert.equal(runtime.mailOrigin, 'https://mail.example.test')
assert.equal(runtime.generation, 9)
assert.equal(runtime.privateKeyPath, path.join(root, 'ssl/private.key'))
assert.equal(JSON.stringify(runtime).includes('not-exported'), false)
@ -31,6 +32,26 @@ test('开发配置只从模拟部署目录读取且不返回凭据字段', async
assert.throws(() => readDevelopmentRuntime(root), /事务尚未完成/)
})
test('开发运行配置单独保留主邮件域名供匿名恢复接口校验', async () => {
const root = await mkdtemp(path.join(os.tmpdir(), 'smail-runtime-recovery-domain-'))
await mkdir(path.join(root, 'config'))
await mkdir(path.join(root, 'ssl'))
await writeFile(path.join(root, 'config/config.json'), JSON.stringify({
generation: 5,
domains: { web: 'web.example.test', primary: 'mail.example.test' }
}))
await writeFile(path.join(root, 'config/ssl.json'), JSON.stringify({
meta: { generation: 5 },
tls: { certificateRef: 'ssl/public.crt', privateKeyRef: 'ssl/private.key' }
}))
await writeFile(path.join(root, 'ssl/public.crt'), 'certificate')
await writeFile(path.join(root, 'ssl/private.key'), 'key', { mode: 0o600 })
const runtime = readDevelopmentRuntime(root)
assert.equal(runtime.origin, 'https://web.example.test')
assert.equal(runtime.mailOrigin, 'https://mail.example.test')
})
test('源码指纹检测编辑且忽略编译嵌入目录', async () => {
const root = await mkdtemp(path.join(os.tmpdir(), 'smail-build-state-'))
for (const name of ['backend', 'frontend', 'plugins/pmail_telegram_push', 'deploy']) {