feat: improve address credential connections

This commit is contained in:
Dream Hunter
2026-04-30 15:33:06 +08:00
committed by GitHub
parent 347be5c762
commit 796a5e4ac5
32 changed files with 854 additions and 118 deletions

View File

@@ -2,7 +2,7 @@ import { Context } from 'hono'
import { Jwt } from 'hono/utils/jwt'
import i18n from '../i18n'
import { getBooleanValue, hashPassword } from '../utils'
import { getBooleanValue } from '../utils'
import { newAddress, handleListQuery } from '../common'
const listAddresses = async (c: Context<HonoCustomType>) => {
@@ -32,7 +32,7 @@ const listAddresses = async (c: Context<HonoCustomType>) => {
+ ` FROM address a`
+ ` where ${whereClause}`,
`SELECT count(*) as count FROM address where ${whereClause}`,
[param], limit, offset, orderBy
[param], limit, offset, orderBy, ['password']
);
}
return await handleListQuery(c,
@@ -41,7 +41,7 @@ const listAddresses = async (c: Context<HonoCustomType>) => {
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
+ ` FROM address a`,
`SELECT count(*) as count FROM address`,
[], limit, offset, orderBy
[], limit, offset, orderBy, ['password']
);
};
@@ -137,16 +137,16 @@ const resetPassword = async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { id } = c.req.param();
const { password } = await c.req.json();
// NOTE: Keep the admin API field as password, but the value is a frontend SHA-256 hash.
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return c.text(msgs.PasswordChangeDisabledMsg, 403);
}
if (!password) {
return c.text(msgs.NewPasswordRequiredMsg, 400);
}
const hashedPassword = await hashPassword(password);
const { success } = await c.env.DB.prepare(
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
).bind(hashedPassword, id).run();
).bind(password, id).run();
if (!success) {
return c.text(msgs.FailedUpdatePasswordMsg, 500);
}

View File

@@ -15,6 +15,11 @@ api.get('/open_api/settings', async (c) => {
const auth = c.req.raw.headers.get("x-custom-auth");
needAuth = !auth || !passwords.includes(auth);
}
const smtpImapProxyConfig = utils.getJsonObjectValue<SmtpImapProxyConfig>(
c.env.SMTP_IMAP_PROXY_CONFIG
) || {};
const smtpProxyConfig = smtpImapProxyConfig.smtp || {};
const imapProxyConfig = smtpImapProxyConfig.imap || {};
return c.json({
"title": c.env.TITLE,
@@ -45,6 +50,19 @@ api.get('/open_api/settings', async (c) => {
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
"enableAddressPassword": utils.getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD),
"enableAgentEmailInfo": utils.getBooleanValue(c.env.ENABLE_AGENT_EMAIL_INFO),
"smtpImapProxyConfig": {
"smtp": {
"host": utils.getStringValue(smtpProxyConfig.host),
"port": utils.getIntValue(smtpProxyConfig.port, 8025),
"starttls": utils.getBooleanValue(smtpProxyConfig.starttls),
},
"imap": {
"host": utils.getStringValue(imapProxyConfig.host),
"port": utils.getIntValue(imapProxyConfig.port, 11143),
"starttls": utils.getBooleanValue(imapProxyConfig.starttls),
},
},
"statusUrl": utils.getStringValue(c.env.STATUS_URL),
"enableGlobalTurnstileCheck": utils.isGlobalTurnstileEnabled(c)
});

View File

@@ -611,7 +611,8 @@ export const handleListQuery = async (
limit: string | number | undefined | null,
offset: string | number | undefined | null,
/** Must be pre-validated (e.g. whitelist), NOT raw user input. Interpolated directly into SQL. */
orderBy?: string
orderBy?: string,
hiddenFields: string[] = []
): Promise<Response> => {
const msgs = i18n.getMessagesbyContext(c);
if (typeof limit === "string") {
@@ -634,7 +635,23 @@ export const handleListQuery = async (
const count = offset == 0 ? await c.env.DB.prepare(
countQuery
).bind(...params).first("count") : 0;
return c.json({ results, count });
if (hiddenFields.length === 0) {
return c.json({ results, count });
}
const filteredResults = results.map((row) => hideObjectFields(row, hiddenFields));
return c.json({ results: filteredResults, count });
}
export const hideObjectFields = <T extends Record<string, unknown>>(
row: T,
fields: string[]
): T => {
const filteredRow = { ...row };
for (const field of fields) {
delete filteredRow[field];
}
return filteredRow;
}
/**

View File

@@ -23,7 +23,7 @@ export default {
return c.text(msgs.InvalidAddressTokenMsg, 400);
}
// 更新密码
// NOTE: new_password is the frontend SHA-256 hash, stored directly in address.password.
const { success } = await c.env.DB.prepare(
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
).bind(new_password, address_id).run();
@@ -67,7 +67,7 @@ export default {
return c.text(msgs.AddressNotFoundMsg, 404);
}
// 验证密码
// NOTE: password is the frontend SHA-256 hash, compared directly with address.password.
if (address.password !== password) {
return c.text(msgs.InvalidEmailOrPasswordMsg, 401);
}

15
worker/src/types.d.ts vendored
View File

@@ -4,6 +4,19 @@ type UserRole = {
prefix: string | undefined | null
}
type SmtpImapProxyConfig = {
smtp?: {
host?: string
port?: number | string
starttls?: boolean | string
}
imap?: {
host?: string
port?: number | string
starttls?: boolean | string
}
}
type Bindings = {
// bindings
DB: D1Database
@@ -45,6 +58,8 @@ type Bindings = {
DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
ENABLE_ADDRESS_PASSWORD: string | boolean | undefined
ENABLE_AGENT_EMAIL_INFO: string | boolean | undefined
SMTP_IMAP_PROXY_CONFIG: string | SmtpImapProxyConfig | undefined
ENABLE_INDEX_ABOUT: string | boolean | undefined
DEFAULT_SEND_BALANCE: number | string | undefined
NO_LIMIT_SEND_ROLE: string | undefined | null

View File

@@ -4,7 +4,7 @@ import { Jwt } from 'hono/utils/jwt'
import { isAddressCountLimitReached } from "../utils"
import { unbindTelegramByAddress } from '../telegram_api/common';
import i18n from '../i18n';
import { updateAddressUpdatedAt, commonGetUserRole } from '../common';
import { updateAddressUpdatedAt, commonGetUserRole, hideObjectFields } from '../common';
const UserBindAddressModule = {
bind: async (c: Context<HonoCustomType>) => {
@@ -140,7 +140,7 @@ const UserBindAddressModule = {
created_at: string;
updated_at: string;
}>();
return results || [];
return (results || []).map((row) => hideObjectFields(row, ['password']));
},
getBindedAddressJwt: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);

View File

@@ -81,6 +81,15 @@ ENABLE_AUTO_REPLY = false
# ENABLE_WEBHOOK = true
# Enable address password feature, if set true, will generate password for new address and support password login and change
# ENABLE_ADDRESS_PASSWORD = false
# Show AI Agent mailbox connection info in the address credential modal
# ENABLE_AGENT_EMAIL_INFO = true
# Show SMTP/IMAP client connection info in the address credential modal
# SMTP_IMAP_PROXY_CONFIG = """
# {
# "smtp": { "host": "smtp.example.com", "port": 8025, "starttls": true },
# "imap": { "host": "imap.example.com", "port": 11143, "starttls": true }
# }
# """
# Footer text
# COPYRIGHT = "Dream Hunter"
# DISABLE_SHOW_GITHUB = true