mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-07-01 20:32:27 +08:00
fix: auto initialize default send balance (#985)
* fix: auto initialize default send balance * fix: tighten send access auto init flow * refactor: centralize send balance state * fix: separate legacy repair from admin control in send balance Add an `address_sender.source` column to distinguish legacy / auto / user / admin rows. `ensureDefaultSendBalance` now only repairs rows with `source IS NULL`, so admin-disabled and user-requested rows are never overwritten. Admin POST writes tag `source = 'admin'`; new auto-init inserts tag `'auto'`; `requestSendMailAccess` inserts tag `'user'`. Bumps DB_VERSION to v0.0.8 with the usual `PRAGMA table_info` guarded ALTER, plus a standalone SQL patch under db/. Adds E2E regressions: legacy repair path, admin-disabled rows stay disabled across settings and send, send after admin deletion auto-initializes a fresh row. * fix: drop runtime legacy repair; backfill source='legacy' on migrate Pre-v0.0.8 schema cannot distinguish legacy request-send-access remnants from admin-disabled rows — both share `balance = 0, enabled = 0`. Letting ensureDefaultSendBalance repair that shape on upgrade could silently re-enable an admin-disabled row. Remove the runtime repair path entirely: - `ensureDefaultSendBalance` now uses `ON CONFLICT(address) DO NOTHING`; existing rows are never touched. - The v0.0.8 migration (and the matching SQL patch) backfills every pre-existing row with `source = 'legacy'`, making pre-migration state explicitly off-limits to runtime auto-init. - E2E: flip the legacy test to the negative direction — a `source='legacy'` zero-balance row stays untouched by settings reads and send attempts. Harden `resetSenderToLegacy` to return 404 when `meta.changes < 1`. - Update changelog and docs: legacy/admin-disabled rows must be restored manually via the admin UI. * refactor: collapse send balance auto-init to missing-row insert Per review feedback: the runtime guarantee we actually need is "create an address_sender row when one is missing, leave existing rows alone". Once `ensureDefaultSendBalance` switched to `ON CONFLICT DO NOTHING`, the `source` column, the v0.0.8 migration, and the `resetSenderToLegacy` test endpoint became dead weight — the DO NOTHING path already protects admin-disabled and admin-edited rows without any provenance metadata. - Drop `address_sender.source` and the v0.0.8 migration; revert DB_VERSION to v0.0.7. No schema change ships with this PR. - Strip the `source` field from `ensureDefaultSendBalance`, `requestSendMailAccess`, and the admin-update path. - Remove the `/admin/test/reset_sender_to_legacy` test endpoint and its E2E helper; the negative legacy-repair test it served is no longer needed because the runtime no longer touches existing rows. - E2E coverage stays focused on the three guardrails: missing-row auto-init, admin-disabled rows stay disabled, admin deletion triggers a fresh re-insert. - Tighten changelog and docs to "auto-initialize missing rows". * docs: align common-issues with missing-row-only auto-init The FAQ entries for "DEFAULT_SEND_BALANCE set but still No balance" still described the old behaviour of repairing legacy `balance = 0 && enabled = 0` rows. Rewrite both zh and en rows to match the current runtime: only addresses with no existing `address_sender` row get auto-initialised; legacy, admin-disabled, and admin-edited rows must be restored manually through the admin console.
This commit is contained in:
@@ -9,6 +9,7 @@ import auto_reply from './auto_reply'
|
||||
import webhook_settings from './webhook_settings';
|
||||
import s3_attachment from './s3_attachment';
|
||||
import address_auth from './address_auth';
|
||||
import { getSendBalanceState } from './send_balance';
|
||||
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
@@ -94,11 +95,7 @@ api.get('/api/settings', async (c) => {
|
||||
|
||||
updateAddressUpdatedAt(c, address);
|
||||
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
||||
const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender where address = ? and enabled = 1`
|
||||
).bind(address).first("balance");
|
||||
const { balance } = await getSendBalanceState(c, address);
|
||||
return c.json({
|
||||
address: address,
|
||||
send_balance: balance || 0,
|
||||
|
||||
107
worker/src/mails_api/send_balance.ts
Normal file
107
worker/src/mails_api/send_balance.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Context } from 'hono'
|
||||
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getIntValue, getSplitStringListValue } from '../utils'
|
||||
|
||||
const ensureDefaultSendBalance = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string
|
||||
): Promise<void> => {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0);
|
||||
if (default_balance <= 0) {
|
||||
return;
|
||||
}
|
||||
// Auto-initialize a sender row only when one does not exist yet.
|
||||
// Existing rows — including admin-disabled ones — are never touched.
|
||||
await c.env.DB.prepare(
|
||||
`INSERT INTO address_sender (address, balance, enabled) VALUES (?, ?, ?)
|
||||
ON CONFLICT(address) DO NOTHING`
|
||||
).bind(address, default_balance, 1).run();
|
||||
}
|
||||
|
||||
export const getEnabledSendBalance = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string
|
||||
): Promise<number | null> => {
|
||||
const balance = await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender where address = ? and enabled = 1`
|
||||
).bind(address).first<number>("balance");
|
||||
return typeof balance === "number" ? balance : null;
|
||||
}
|
||||
|
||||
export const getSendBalanceState = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string,
|
||||
options?: {
|
||||
isAdmin?: boolean,
|
||||
initializeDefaultBalance?: boolean
|
||||
}
|
||||
): Promise<{
|
||||
isNoLimitSender: boolean,
|
||||
needCheckBalance: boolean,
|
||||
balance: number | null
|
||||
}> => {
|
||||
const user_role = c.get("userRolePayload");
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const is_no_limit_send_balance = typeof user_role === "string"
|
||||
&& no_limit_roles.includes(user_role);
|
||||
const noLimitSendAddressList = is_no_limit_send_balance ?
|
||||
[] : await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY) || [];
|
||||
const isNoLimitSendAddress = !!noLimitSendAddressList?.includes(address);
|
||||
const isNoLimitSender = is_no_limit_send_balance || isNoLimitSendAddress;
|
||||
const needCheckBalance = !options?.isAdmin && !isNoLimitSender;
|
||||
if (needCheckBalance && options?.initializeDefaultBalance !== false) {
|
||||
await ensureDefaultSendBalance(c, address);
|
||||
}
|
||||
if (isNoLimitSender) {
|
||||
return {
|
||||
isNoLimitSender: true,
|
||||
needCheckBalance: false,
|
||||
balance: 99999,
|
||||
};
|
||||
}
|
||||
return {
|
||||
isNoLimitSender: false,
|
||||
needCheckBalance: needCheckBalance,
|
||||
balance: await getEnabledSendBalance(c, address),
|
||||
};
|
||||
}
|
||||
|
||||
export const requestSendMailAccess = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string
|
||||
): Promise<{
|
||||
status: 'ok' | 'already_requested' | 'operation_failed'
|
||||
}> => {
|
||||
const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0);
|
||||
if (default_balance > 0) {
|
||||
await ensureDefaultSendBalance(c, address);
|
||||
const { balance } = await getSendBalanceState(c, address, {
|
||||
initializeDefaultBalance: false,
|
||||
});
|
||||
if (balance && balance > 0) {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
return { status: 'already_requested' };
|
||||
}
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO address_sender (address, balance, enabled) VALUES (?, ?, ?)`
|
||||
).bind(
|
||||
address, default_balance, default_balance > 0 ? 1 : 0
|
||||
).run();
|
||||
if (!success) {
|
||||
return { status: 'operation_failed' };
|
||||
}
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
return { status: 'already_requested' };
|
||||
}
|
||||
return { status: 'operation_failed' };
|
||||
}
|
||||
return { status: 'ok' };
|
||||
}
|
||||
@@ -6,11 +6,10 @@ import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer';
|
||||
|
||||
import i18n from '../i18n';
|
||||
import { CONSTANTS } from '../constants'
|
||||
import {
|
||||
getJsonSetting, getDomains, getIntValue, getBooleanValue, getJsonObjectValue, getSplitStringListValue
|
||||
} from '../utils';
|
||||
import { getJsonSetting, getDomains, getBooleanValue, getJsonObjectValue } from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
import { handleListQuery, isSendMailBindingEnabled, updateAddressUpdatedAt } from '../common'
|
||||
import { getSendBalanceState, requestSendMailAccess } from './send_balance';
|
||||
import { ensureSendMailLimit, increaseSendMailLimitCount } from './send_mail_limit_utils';
|
||||
|
||||
|
||||
@@ -22,24 +21,14 @@ api.post('/api/request_send_mail_access', async (c) => {
|
||||
if (!address) {
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
try {
|
||||
const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO address_sender (address, balance, enabled) VALUES (?, ?, ?)`
|
||||
).bind(
|
||||
address, default_balance, default_balance > 0 ? 1 : 0
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
return c.text(msgs.AlreadyRequestedMsg, 400)
|
||||
}
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
const result = await requestSendMailAccess(c, address);
|
||||
if (result.status === "ok") {
|
||||
return c.json({ status: "ok" })
|
||||
}
|
||||
return c.json({ status: "ok" })
|
||||
if (result.status === "already_requested") {
|
||||
return c.text(msgs.AlreadyRequestedMsg, 400)
|
||||
}
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
})
|
||||
|
||||
export const sendMailToVerifyAddress = async (
|
||||
@@ -159,21 +148,11 @@ export const sendMail = async (
|
||||
if (!domains.includes(mailDomain)) {
|
||||
throw new Error(msgs.InvalidDomainMsg)
|
||||
}
|
||||
const user_role = c.get("userRolePayload");
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
||||
// no need find noLimitSendAddressList if is_no_limit_send_balance
|
||||
const noLimitSendAddressList = is_no_limit_send_balance ?
|
||||
[] : await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY) || [];
|
||||
const isNoLimitSendAddress = noLimitSendAddressList?.includes(address);
|
||||
const needCheckBalance = !is_no_limit_send_balance && !options?.isAdmin && !isNoLimitSendAddress;
|
||||
if (needCheckBalance) {
|
||||
// check permission
|
||||
const balance = await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender
|
||||
where address = ? and enabled = 1`
|
||||
).bind(address).first<number>("balance");
|
||||
if (!balance || balance <= 0) {
|
||||
const sendBalanceState = await getSendBalanceState(c, address, {
|
||||
isAdmin: options?.isAdmin,
|
||||
});
|
||||
if (sendBalanceState.needCheckBalance) {
|
||||
if (!sendBalanceState.balance || sendBalanceState.balance <= 0) {
|
||||
throw new Error(msgs.NoBalanceMsg)
|
||||
}
|
||||
}
|
||||
@@ -235,7 +214,7 @@ export const sendMail = async (
|
||||
await increaseSendMailLimitCount(c);
|
||||
|
||||
// update balance
|
||||
if (!sendByVerifiedAddressList && needCheckBalance) {
|
||||
if (!sendByVerifiedAddressList && sendBalanceState.needCheckBalance) {
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE address_sender SET balance = balance - 1 where address = ?`
|
||||
|
||||
Reference in New Issue
Block a user