mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 23:47:50 +08:00
fix: respect user mail deletion toggle in user center (#979)
* fix: respect user mail deletion toggle in user center Hide user mailbox delete actions and block /user_api/mails deletion when ENABLE_USER_DELETE_EMAIL is disabled. Add an e2e regression test and changelog entries for issue #978. * test: hash user password in mail deletion e2e Use the same SHA-256 pre-hashed password format as the frontend for the user register/login flow in the mail deletion regression test.
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix: |用户侧收件箱| 修复 `ENABLE_USER_DELETE_EMAIL` 关闭时用户中心仍显示删除按钮且仍可通过 `/user_api/mails/:id` 删除邮件的问题(#978)
|
||||||
- fix: |Admin| 修复 `/admin/address` 与 `/admin/users` 在使用完整邮箱(query 长度超过 50 字节)作为搜索条件时报错 `D1_ERROR: LIKE or GLOB pattern too complex` 的问题,长查询自动改用 `instr()` 绕开 D1 的 LIKE pattern 长度限制(#956)
|
- fix: |Admin| 修复 `/admin/address` 与 `/admin/users` 在使用完整邮箱(query 长度超过 50 字节)作为搜索条件时报错 `D1_ERROR: LIKE or GLOB pattern too complex` 的问题,长查询自动改用 `instr()` 绕开 D1 的 LIKE pattern 长度限制(#956)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix: |User Mailbox| Fix an issue where the user center still showed delete actions and could still delete mail via `/user_api/mails/:id` when `ENABLE_USER_DELETE_EMAIL` was disabled (#978)
|
||||||
- fix: |Admin| Fix `D1_ERROR: LIKE or GLOB pattern too complex` on `/admin/address` and `/admin/users` when searching by full email address (query length pushes the LIKE pattern over D1's 50-byte limit). Long queries now fall back to `instr()` to bypass the LIKE pattern length cap (#956)
|
- fix: |Admin| Fix `D1_ERROR: LIKE or GLOB pattern too complex` on `/admin/address` and `/admin/users` when searching by full email address (query length pushes the LIKE pattern over D1's 50-byte limit). Long queries now fall back to `instr()` to bypass the LIKE pattern length cap (#956)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ ENABLE_CREATE_ADDRESS_SUBDOMAIN_MATCH = false
|
|||||||
JWT_SECRET = "e2e-test-secret-key-env-off"
|
JWT_SECRET = "e2e-test-secret-key-env-off"
|
||||||
BLACK_LIST = ""
|
BLACK_LIST = ""
|
||||||
ENABLE_USER_CREATE_EMAIL = true
|
ENABLE_USER_CREATE_EMAIL = true
|
||||||
ENABLE_USER_DELETE_EMAIL = true
|
ENABLE_USER_DELETE_EMAIL = false
|
||||||
ENABLE_AUTO_REPLY = true
|
ENABLE_AUTO_REPLY = true
|
||||||
DEFAULT_SEND_BALANCE = 10
|
DEFAULT_SEND_BALANCE = 10
|
||||||
ENABLE_ADDRESS_PASSWORD = true
|
ENABLE_ADDRESS_PASSWORD = true
|
||||||
|
|||||||
@@ -1,7 +1,108 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { WORKER_URL, createTestAddress, seedTestMail, deleteAddress } from '../../fixtures/test-helpers';
|
import { WORKER_URL, WORKER_URL_ENV_OFF, createTestAddress, seedTestMail, deleteAddress } from '../../fixtures/test-helpers';
|
||||||
|
|
||||||
test.describe('Mail Deletion', () => {
|
test.describe('Mail Deletion', () => {
|
||||||
|
test('user mail deletion is disabled when ENABLE_USER_DELETE_EMAIL is false', async ({ request }) => {
|
||||||
|
test.skip(!WORKER_URL_ENV_OFF, 'WORKER_URL_ENV_OFF is not configured');
|
||||||
|
|
||||||
|
const testUserEmail = `mail-delete-e2e-${Date.now()}@test.example.com`;
|
||||||
|
const testUserPassword = 'test-password-123';
|
||||||
|
const testUserPasswordHash = createHash('sha256').update(testUserPassword).digest('hex');
|
||||||
|
|
||||||
|
const enableRes = await request.post(`${WORKER_URL_ENV_OFF}/admin/user_settings`, {
|
||||||
|
data: {
|
||||||
|
enable: true,
|
||||||
|
enableMailVerify: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(enableRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const registerRes = await request.post(`${WORKER_URL_ENV_OFF}/user_api/register`, {
|
||||||
|
data: { email: testUserEmail, password: testUserPasswordHash },
|
||||||
|
});
|
||||||
|
expect(registerRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const loginRes = await request.post(`${WORKER_URL_ENV_OFF}/user_api/login`, {
|
||||||
|
data: { email: testUserEmail, password: testUserPasswordHash },
|
||||||
|
});
|
||||||
|
expect(loginRes.ok()).toBe(true);
|
||||||
|
const { jwt: userJwt } = await loginRes.json();
|
||||||
|
expect(userJwt).toBeTruthy();
|
||||||
|
|
||||||
|
const createRes = await request.post(`${WORKER_URL_ENV_OFF}/api/new_address`, {
|
||||||
|
data: {
|
||||||
|
name: `user-del-disabled${Date.now()}`,
|
||||||
|
domain: 'test.example.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(createRes.ok()).toBe(true);
|
||||||
|
const { jwt, address, address_id } = await createRes.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bindRes = await request.post(`${WORKER_URL_ENV_OFF}/user_api/bind_address`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
'x-user-token': userJwt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(bindRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const from = 'sender@test.example.com';
|
||||||
|
const subject = 'Disabled Mail Delete';
|
||||||
|
const boundary = `----E2E${Date.now()}`;
|
||||||
|
const raw = [
|
||||||
|
`From: ${from}`,
|
||||||
|
`To: ${address}`,
|
||||||
|
`Subject: ${subject}`,
|
||||||
|
`Message-ID: <e2e-${Date.now()}-${Math.random().toString(36).slice(2, 10)}@test>`,
|
||||||
|
'MIME-Version: 1.0',
|
||||||
|
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
||||||
|
'',
|
||||||
|
`--${boundary}`,
|
||||||
|
'Content-Type: text/plain; charset=utf-8',
|
||||||
|
'',
|
||||||
|
'Hello from E2E',
|
||||||
|
`--${boundary}`,
|
||||||
|
'Content-Type: text/html; charset=utf-8',
|
||||||
|
'',
|
||||||
|
'<p>Hello from E2E</p>',
|
||||||
|
`--${boundary}--`,
|
||||||
|
].join('\r\n');
|
||||||
|
|
||||||
|
const seedRes = await request.post(`${WORKER_URL_ENV_OFF}/admin/test/receive_mail`, {
|
||||||
|
data: { from, to: address, raw },
|
||||||
|
});
|
||||||
|
expect(seedRes.ok()).toBe(true);
|
||||||
|
const seedBody = await seedRes.json();
|
||||||
|
expect(seedBody.success).toBe(true);
|
||||||
|
|
||||||
|
const listRes = await request.get(`${WORKER_URL_ENV_OFF}/user_api/mails?limit=10&offset=0`, {
|
||||||
|
headers: { 'x-user-token': userJwt },
|
||||||
|
});
|
||||||
|
expect(listRes.ok()).toBe(true);
|
||||||
|
const { results } = await listRes.json();
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
|
||||||
|
const targetId = results[0].id;
|
||||||
|
const delRes = await request.delete(`${WORKER_URL_ENV_OFF}/user_api/mails/${targetId}`, {
|
||||||
|
headers: { 'x-user-token': userJwt },
|
||||||
|
});
|
||||||
|
expect(delRes.status()).toBe(403);
|
||||||
|
|
||||||
|
const afterRes = await request.get(`${WORKER_URL_ENV_OFF}/user_api/mails?limit=10&offset=0`, {
|
||||||
|
headers: { 'x-user-token': userJwt },
|
||||||
|
});
|
||||||
|
expect(afterRes.ok()).toBe(true);
|
||||||
|
const after = await afterRes.json();
|
||||||
|
expect(after.results).toHaveLength(1);
|
||||||
|
expect(after.results[0].id).toBe(targetId);
|
||||||
|
} finally {
|
||||||
|
const deleteRes = await request.delete(`${WORKER_URL_ENV_OFF}/admin/delete_address/${address_id}`);
|
||||||
|
expect(deleteRes.ok()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('delete a single mail by ID', async ({ request }) => {
|
test('delete a single mail by ID', async ({ request }) => {
|
||||||
const { jwt, address } = await createTestAddress(request, 'del-single');
|
const { jwt, address } = await createTestAddress(request, 'del-single');
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { onMounted, ref, watch } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
import MailBox from '../../components/MailBox.vue';
|
import MailBox from '../../components/MailBox.vue';
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const { openSettings } = useGlobalState()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
@@ -78,7 +80,7 @@ onMounted(() => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
<div style="margin-top: 10px;"></div>
|
<div style="margin-top: 10px;"></div>
|
||||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
|
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
|
||||||
:deleteMail="deleteMail" :showFilterInput="true" />
|
:deleteMail="deleteMail" :showFilterInput="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Context } from "hono";
|
import { Context } from "hono";
|
||||||
|
import i18n from "../i18n";
|
||||||
import { handleMailListQuery } from "../common";
|
import { handleMailListQuery } from "../common";
|
||||||
import UserBindAddressModule from "./bind_address";
|
import UserBindAddressModule from "./bind_address";
|
||||||
|
import { getBooleanValue } from "../utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getMails: async (c: Context<HonoCustomType>) => {
|
getMails: async (c: Context<HonoCustomType>) => {
|
||||||
@@ -26,6 +28,10 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
deleteMail: async (c: Context<HonoCustomType>) => {
|
deleteMail: async (c: Context<HonoCustomType>) => {
|
||||||
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
|
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||||
|
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||||
|
}
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { user_id } = c.get("userPayload");
|
const { user_id } = c.get("userPayload");
|
||||||
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id);
|
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id);
|
||||||
|
|||||||
Reference in New Issue
Block a user