mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-12 02:20:12 +08:00
feat: add i18n support for backend API and Telegram bot (#797)
* feat: add i18n support for backend API and Telegram bot - Add comprehensive i18n support for all backend API error messages (zh/en) - Add /lang command for Telegram bot to set language preference - Add bilingual command descriptions for Telegram bot - Support per-user language preference stored in KV - Global push uses DEFAULT_LANG, user push uses saved preference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: improve Telegram bot language preference feature - Add internationalized message for disabled language feature - Fix hardcoded English message in /lang command - Optimize getTgMessages calls (reduce from 3 to 1 call) - Remove verbose comments for better code clarity - Add TgLangFeatureDisabledMsg to i18n (zh/en) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
|
||||
- feat: |数据库| 为 `message_id` 字段添加索引,优化邮件更新操作性能,需执行 `db/2025-12-15-message-id-index.sql` 更新数据库
|
||||
- feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句
|
||||
- feat: |国际化| 后端 API 错误消息全面支持中英文国际化
|
||||
- feat: |Telegram| 机器人支持中英文切换,新增 `/lang` 命令设置语言偏好
|
||||
|
||||
## v1.1.0
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
- feat: |Email Filtering| Remove backend keyword parameter, switch to frontend filtering of current page emails, optimize query performance
|
||||
- feat: |Database| Add index for `message_id` field to optimize email update operations, need to execute `db/2025-12-15-message-id-index.sql` to update database
|
||||
- feat: |Admin| Add custom SQL cleanup feature to maintenance page, support scheduled task execution of custom cleanup statements
|
||||
- feat: |i18n| Backend API error messages now fully support Chinese and English internationalization
|
||||
- feat: |Telegram| Bot supports Chinese/English switching, add `/lang` command to set language preference
|
||||
|
||||
## v1.1.0
|
||||
|
||||
|
||||
@@ -14,23 +14,24 @@ export default {
|
||||
return c.json(settings)
|
||||
},
|
||||
saveSetting: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const value = await c.req.json();
|
||||
const settings = new UserSettings(value);
|
||||
if (settings.enableMailVerify && !c.env.KV) {
|
||||
return c.text("Please enable KV first if you want to enable mail verify", 403)
|
||||
return c.text(msgs.EnableKVForMailVerifyMsg, 403)
|
||||
}
|
||||
if (settings.enableMailVerify && !settings.verifyMailSender) {
|
||||
return c.text("Please provide verifyMailSender", 400)
|
||||
return c.text(msgs.VerifyMailSenderNotSetMsg, 400)
|
||||
}
|
||||
if (settings.enableMailVerify && settings.verifyMailSender) {
|
||||
const mailDomain = settings.verifyMailSender.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
return c.text(`VerifyMailSender(${settings.verifyMailSender}) domain must in ${JSON.stringify(domains, null, 2)}`, 400)
|
||||
return c.text(`${msgs.VerifyMailDomainInvalidMsg} ${JSON.stringify(domains, null, 2)}`, 400)
|
||||
}
|
||||
}
|
||||
if (settings.maxAddressCount < 0) {
|
||||
return c.text("Invalid maxAddressCount", 400)
|
||||
return c.text(msgs.InvalidMaxAddressCountMsg, 400)
|
||||
}
|
||||
await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings));
|
||||
return c.json({ success: true })
|
||||
@@ -60,9 +61,10 @@ export default {
|
||||
);
|
||||
},
|
||||
createUser: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) {
|
||||
return c.text("Invalid email or password", 400)
|
||||
return c.text(msgs.InvalidEmailOrPasswordMsg, 400)
|
||||
}
|
||||
// geo data
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
@@ -77,14 +79,14 @@ export default {
|
||||
email, password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
return c.text(msgs.FailedToRegisterMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = (e as Error).message;
|
||||
if (errorMsg && errorMsg.includes("UNIQUE")) {
|
||||
return c.text("User already exists", 400)
|
||||
return c.text(msgs.UserAlreadyExistsMsg, 400)
|
||||
}
|
||||
return c.text(`Failed to register: ${errorMsg}`, 500)
|
||||
return c.text(`${msgs.FailedToRegisterMsg}: ${errorMsg}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -99,7 +101,7 @@ export default {
|
||||
`DELETE FROM users_address WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success || !addressSuccess) {
|
||||
return c.text("Failed to delete user", 500)
|
||||
return c.text(msgs.FailedDeleteUserMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -114,28 +116,29 @@ export default {
|
||||
`UPDATE users SET password = ? WHERE id = ?`
|
||||
).bind(password, user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to reset password", 500)
|
||||
return c.text(msgs.FailedUpdatePasswordMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text(`Failed to reset password: ${(e as Error).message}`, 500)
|
||||
return c.text(`${msgs.FailedUpdatePasswordMsg}: ${(e as Error).message}`, 500)
|
||||
}
|
||||
return c.json({ success: true });
|
||||
},
|
||||
updateUserRoles: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { user_id, role_text } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
if (!user_id) return c.text(msgs.InvalidUserIdMsg, 400);
|
||||
if (!role_text) {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM user_roles WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
const user_roles = getUserRoles(c);
|
||||
if (!user_roles.find((r) => r.role === role_text)) {
|
||||
return c.text("Invalid role_text", 400)
|
||||
return c.text(msgs.InvalidRoleTextMsg, 400)
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO user_roles (user_id, role_text)`
|
||||
@@ -143,7 +146,7 @@ export default {
|
||||
+ ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')`
|
||||
).bind(user_id, role_text, role_text).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
|
||||
@@ -4,6 +4,11 @@ import { cleanup } from '../common';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
import { CleanupSettings, CustomSqlCleanup } from '../models';
|
||||
import i18n from '../i18n';
|
||||
import { LocaleMessages } from '../i18n/type';
|
||||
|
||||
// SQL validation error types
|
||||
type SqlValidationError = 'empty' | 'too_long' | 'not_delete' | 'multiple_statements' | 'has_comments';
|
||||
|
||||
// Normalize SQL: trim and remove trailing semicolon
|
||||
const normalizeSql = (sql: string): string => {
|
||||
@@ -14,34 +19,45 @@ const normalizeSql = (sql: string): string => {
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// Get error message from error type
|
||||
const getValidationErrorMsg = (errorType: SqlValidationError, msgs: LocaleMessages): string => {
|
||||
switch (errorType) {
|
||||
case 'empty': return msgs.SqlEmptyMsg;
|
||||
case 'too_long': return msgs.SqlTooLongMsg;
|
||||
case 'not_delete': return msgs.SqlOnlyDeleteMsg;
|
||||
case 'multiple_statements': return msgs.SqlSingleStatementMsg;
|
||||
case 'has_comments': return msgs.SqlNoCommentsMsg;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate custom SQL cleanup statement
|
||||
export const validateCustomSql = (sql: string): { valid: boolean; error?: string } => {
|
||||
export const validateCustomSql = (sql: string): { valid: boolean; errorType?: SqlValidationError } => {
|
||||
if (!sql || !sql.trim()) {
|
||||
return { valid: false, error: "SQL statement is empty" };
|
||||
return { valid: false, errorType: 'empty' };
|
||||
}
|
||||
|
||||
const trimmedSql = normalizeSql(sql);
|
||||
|
||||
// Check SQL length (max 1000 characters)
|
||||
if (trimmedSql.length > 1000) {
|
||||
return { valid: false, error: "SQL statement is too long (max 1000 characters)" };
|
||||
return { valid: false, errorType: 'too_long' };
|
||||
}
|
||||
|
||||
const sqlUpper = trimmedSql.toUpperCase();
|
||||
|
||||
// Only allow DELETE statements
|
||||
if (!sqlUpper.startsWith('DELETE ')) {
|
||||
return { valid: false, error: "Only DELETE statements are allowed" };
|
||||
return { valid: false, errorType: 'not_delete' };
|
||||
}
|
||||
|
||||
// Only allow single statement (no semicolons after trimming)
|
||||
if (trimmedSql.includes(';')) {
|
||||
return { valid: false, error: "Only single SQL statement is allowed" };
|
||||
return { valid: false, errorType: 'multiple_statements' };
|
||||
}
|
||||
|
||||
// Forbid SQL comments
|
||||
if (/--/.test(trimmedSql) || /\/\*/.test(trimmedSql)) {
|
||||
return { valid: false, error: "SQL comments are not allowed" };
|
||||
return { valid: false, errorType: 'has_comments' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
@@ -52,13 +68,14 @@ export const executeCustomSqlCleanup = async (
|
||||
c: Context<HonoCustomType>,
|
||||
customSql: CustomSqlCleanup
|
||||
): Promise<{ success: boolean; rowsAffected?: number; error?: string }> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!customSql || !customSql.sql) {
|
||||
return { success: false, error: "Invalid custom SQL cleanup config" };
|
||||
return { success: false, error: msgs.InvalidCleanupConfigMsg };
|
||||
}
|
||||
|
||||
const validation = validateCustomSql(customSql.sql);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
return { success: false, error: getValidationErrorMsg(validation.errorType!, msgs) };
|
||||
}
|
||||
|
||||
const sql = normalizeSql(customSql.sql);
|
||||
@@ -78,12 +95,13 @@ export const executeCustomSqlCleanup = async (
|
||||
|
||||
export default {
|
||||
cleanup: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { cleanType, cleanDays } = await c.req.json();
|
||||
try {
|
||||
await cleanup(c, cleanType, cleanDays);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return c.text(`Failed to cleanup ${(error as Error).message}`, 500)
|
||||
return c.text(`${msgs.OperationFailedMsg}: ${(error as Error).message}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -92,6 +110,7 @@ export default {
|
||||
return c.json(cleanupSetting)
|
||||
},
|
||||
saveCleanup: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const cleanupSetting = await c.req.json<CleanupSettings>();
|
||||
|
||||
// Validate custom SQL cleanup list
|
||||
@@ -100,7 +119,8 @@ export default {
|
||||
if (customSql.sql) {
|
||||
const validation = validateCustomSql(customSql.sql);
|
||||
if (!validation.valid) {
|
||||
return c.text(`Invalid SQL [${customSql.name || 'unnamed'}]: ${validation.error}`, 400);
|
||||
const errorMsg = getValidationErrorMsg(validation.errorType!, msgs);
|
||||
return c.text(`[${customSql.name || 'unnamed'}]: ${errorMsg}`, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,9 @@ api.get('/admin/address', async (c) => {
|
||||
|
||||
api.post('/admin/new_address', async (c) => {
|
||||
const { name, domain, enablePrefix } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!name) {
|
||||
return c.text("Please provide a name", 400)
|
||||
return c.text(msgs.RequiredFieldMsg, 400)
|
||||
}
|
||||
try {
|
||||
const res = await newAddress(c, {
|
||||
@@ -67,19 +66,20 @@ api.post('/admin/new_address', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/admin/delete_address/:id', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE id = ? `
|
||||
).bind(id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to delete address", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to delete mails", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address IN`
|
||||
@@ -94,13 +94,14 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/admin/clear_inbox/:id', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to clear inbox", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: mailSuccess
|
||||
@@ -108,13 +109,14 @@ api.delete('/admin/clear_inbox/:id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/admin/clear_sent_items/:id', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { success: sendboxSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!sendboxSuccess) {
|
||||
return c.text("Failed to clear sent items", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: sendboxSuccess
|
||||
@@ -136,15 +138,16 @@ api.get('/admin/show_password/:id', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/address/:id/reset_password', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { password } = await c.req.json();
|
||||
// 检查功能是否启用
|
||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
||||
return c.text("Password management is disabled", 403);
|
||||
return c.text(msgs.PasswordChangeDisabledMsg, 403);
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return c.text("Password is required", 400);
|
||||
return c.text(msgs.NewPasswordRequiredMsg, 400);
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
@@ -153,7 +156,7 @@ api.post('/admin/address/:id/reset_password', async (c) => {
|
||||
).bind(hashedPassword, id).run();
|
||||
|
||||
if (!success) {
|
||||
return c.text("Failed to reset password", 500);
|
||||
return c.text(msgs.FailedUpdatePasswordMsg, 500);
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
@@ -181,18 +184,19 @@ api.get('/admin/address_sender', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/address_sender', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
/* eslint-disable prefer-const */
|
||||
let { address, address_id, balance, enabled } = await c.req.json();
|
||||
/* eslint-enable prefer-const */
|
||||
if (!address_id) {
|
||||
return c.text("Invalid address_id", 400)
|
||||
return c.text(msgs.InvalidAddressIdMsg, 400)
|
||||
}
|
||||
enabled = enabled ? 1 : 0;
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE address_sender SET enabled = ?, balance = ? WHERE id = ? `
|
||||
).bind(enabled, balance, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update address sender", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
await sendAdminInternalMail(
|
||||
c, address, "Account Send Access Updated",
|
||||
@@ -291,16 +295,17 @@ api.get('/admin/account_settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/account_settings', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
||||
const {
|
||||
blockList, sendBlockList, noLimitSendAddressList,
|
||||
verifiedAddressList, fromBlockList, emailRuleSettings
|
||||
} = await c.req.json();
|
||||
if (!blockList || !sendBlockList || !verifiedAddressList) {
|
||||
return c.text("Invalid blockList or sendBlockList", 400)
|
||||
return c.text(msgs.InvalidInputMsg, 400)
|
||||
}
|
||||
if (!c.env.SEND_MAIL && verifiedAddressList.length > 0) {
|
||||
return c.text("Please enable SEND_MAIL to use verifiedAddressList", 400)
|
||||
return c.text(msgs.EnableSendMailMsg, 400)
|
||||
}
|
||||
await saveSetting(
|
||||
c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY,
|
||||
@@ -315,7 +320,7 @@ api.post('/admin/account_settings', async (c) => {
|
||||
JSON.stringify(verifiedAddressList)
|
||||
)
|
||||
if (fromBlockList?.length > 0 && !c.env.KV) {
|
||||
return c.text("Please enable KV to use fromBlockList", 400)
|
||||
return c.text(msgs.EnableKVMsg, 400)
|
||||
}
|
||||
if (fromBlockList) {
|
||||
await c.env.KV.put(CONSTANTS.EMAIL_KV_BLACK_LIST, JSON.stringify(fromBlockList || []))
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getJsonSetting, saveSetting } from "../utils";
|
||||
import { IpBlacklistSettings } from "../ip_blacklist";
|
||||
import i18n from "../i18n";
|
||||
|
||||
/**
|
||||
* Get IP blacklist settings from database
|
||||
@@ -26,55 +27,47 @@ async function getIpBlacklistSettings(c: Context<HonoCustomType>): Promise<Respo
|
||||
* Save IP blacklist settings to database
|
||||
*/
|
||||
async function saveIpBlacklistSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const settings = await c.req.json<IpBlacklistSettings>();
|
||||
|
||||
// Validate settings
|
||||
if (typeof settings.enabled !== 'boolean') {
|
||||
return c.text("Invalid enabled value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: enabled`, 400);
|
||||
}
|
||||
|
||||
if (!Array.isArray(settings.blacklist)) {
|
||||
return c.text("Invalid blacklist value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: blacklist`, 400);
|
||||
}
|
||||
|
||||
if (!Array.isArray(settings.asnBlacklist)) {
|
||||
return c.text("Invalid asnBlacklist value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: asnBlacklist`, 400);
|
||||
}
|
||||
|
||||
if (!Array.isArray(settings.fingerprintBlacklist)) {
|
||||
return c.text("Invalid fingerprintBlacklist value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: fingerprintBlacklist`, 400);
|
||||
}
|
||||
|
||||
if (typeof settings.enableDailyLimit !== 'boolean') {
|
||||
return c.text("Invalid enableDailyLimit value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: enableDailyLimit`, 400);
|
||||
}
|
||||
|
||||
const limit = Number(settings.dailyRequestLimit);
|
||||
if (isNaN(limit) || limit < 1 || limit > 1000000) {
|
||||
return c.text("Invalid dailyRequestLimit value (must be between 1 and 1000000)", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: dailyRequestLimit (1-1000000)`, 400);
|
||||
}
|
||||
|
||||
// Add size limit
|
||||
const MAX_BLACKLIST_SIZE = 1000;
|
||||
if (settings.blacklist.length > MAX_BLACKLIST_SIZE) {
|
||||
return c.text(
|
||||
`Blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
|
||||
400
|
||||
);
|
||||
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: blacklist (${MAX_BLACKLIST_SIZE})`, 400);
|
||||
}
|
||||
|
||||
if (settings.asnBlacklist.length > MAX_BLACKLIST_SIZE) {
|
||||
return c.text(
|
||||
`ASN blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
|
||||
400
|
||||
);
|
||||
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: asnBlacklist (${MAX_BLACKLIST_SIZE})`, 400);
|
||||
}
|
||||
|
||||
if (settings.fingerprintBlacklist.length > MAX_BLACKLIST_SIZE) {
|
||||
return c.text(
|
||||
`Fingerprint blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
|
||||
400
|
||||
);
|
||||
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: fingerprintBlacklist (${MAX_BLACKLIST_SIZE})`, 400);
|
||||
}
|
||||
|
||||
// Sanitize patterns (trim and remove empty strings)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles,
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
||||
import i18n from './i18n';
|
||||
|
||||
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
||||
|
||||
@@ -134,6 +135,7 @@ export const newAddress = async (
|
||||
sourceMeta?: string | undefined | null,
|
||||
}
|
||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// trim whitespace and remove special characters
|
||||
name = name.trim().replace(getNameRegex(c), '')
|
||||
// check name
|
||||
@@ -153,10 +155,10 @@ export const newAddress = async (
|
||||
);
|
||||
// check name length
|
||||
if (name.length < minAddressLength) {
|
||||
throw new Error(`Name too short (min ${minAddressLength})`);
|
||||
throw new Error(`${msgs.NameTooShortMsg} (min ${minAddressLength})`);
|
||||
}
|
||||
if (name.length > maxAddressLength) {
|
||||
throw new Error(`Name too long (max ${maxAddressLength})`);
|
||||
throw new Error(`${msgs.NameTooLongMsg} (max ${maxAddressLength})`);
|
||||
}
|
||||
// create address with prefix
|
||||
if (typeof addressPrefix === "string") {
|
||||
@@ -177,7 +179,7 @@ export const newAddress = async (
|
||||
}
|
||||
// check domain is valid
|
||||
if (!domain || !allowDomains.includes(domain)) {
|
||||
throw new Error("Invalid domain")
|
||||
throw new Error(msgs.InvalidDomainMsg)
|
||||
}
|
||||
// create address
|
||||
name = name + "@" + domain;
|
||||
@@ -187,7 +189,7 @@ export const newAddress = async (
|
||||
`INSERT INTO address(name, source_meta) VALUES(?, ?)`
|
||||
).bind(name, sourceMeta).run();
|
||||
if (!result.success) {
|
||||
throw new Error("Failed to create address")
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
await updateAddressUpdatedAt(c, name);
|
||||
} catch (e) {
|
||||
@@ -198,13 +200,13 @@ export const newAddress = async (
|
||||
`INSERT INTO address(name) VALUES(?)`
|
||||
).bind(name).run();
|
||||
if (!result.success) {
|
||||
throw new Error("Failed to create address")
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
await updateAddressUpdatedAt(c, name);
|
||||
} else if (message && message.includes("UNIQUE")) {
|
||||
throw new Error("Address already exists")
|
||||
throw new Error(msgs.AddressAlreadyExistsMsg)
|
||||
} else {
|
||||
throw new Error("Failed to create address")
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
}
|
||||
const address_id = await c.env.DB.prepare(
|
||||
@@ -247,8 +249,9 @@ export const cleanup = async (
|
||||
cleanType: string | undefined | null,
|
||||
cleanDays: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) {
|
||||
throw new Error("Invalid cleanType or cleanDays")
|
||||
throw new Error(msgs.InvalidCleanupConfigMsg)
|
||||
}
|
||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||
switch (cleanType) {
|
||||
@@ -294,7 +297,7 @@ export const cleanup = async (
|
||||
)
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid cleanType")
|
||||
throw new Error(msgs.InvalidCleanTypeMsg)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -336,11 +339,12 @@ export const deleteAddressWithData = async (
|
||||
address: string | undefined | null,
|
||||
address_id: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
throw new Error("Delete email is disabled")
|
||||
throw new Error(msgs.UserDeleteEmailDisabledMsg)
|
||||
}
|
||||
if (!address && !address_id) {
|
||||
throw new Error("Address or address_id required")
|
||||
throw new Error(msgs.RequiredFieldMsg)
|
||||
}
|
||||
// get address_id or address
|
||||
if (!address_id) {
|
||||
@@ -354,7 +358,7 @@ export const deleteAddressWithData = async (
|
||||
}
|
||||
// check address again
|
||||
if (!address || !address_id) {
|
||||
throw new Error("Can't find address");
|
||||
throw new Error(msgs.AddressNotFoundMsg);
|
||||
}
|
||||
// unbind telegram
|
||||
await unbindTelegramByAddress(c, address);
|
||||
@@ -378,7 +382,7 @@ export const deleteAddressWithData = async (
|
||||
`DELETE FROM address WHERE name = ? `
|
||||
).bind(address).run();
|
||||
if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) {
|
||||
throw new Error("Failed to delete address")
|
||||
throw new Error(msgs.OperationFailedMsg)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -389,6 +393,7 @@ export const handleListQuery = async (
|
||||
limit: string | number | undefined | null,
|
||||
offset: string | number | undefined | null
|
||||
): Promise<Response> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (typeof limit === "string") {
|
||||
limit = parseInt(limit);
|
||||
}
|
||||
@@ -396,10 +401,10 @@ export const handleListQuery = async (
|
||||
offset = parseInt(offset);
|
||||
}
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
return c.text(msgs.InvalidLimitMsg, 400)
|
||||
}
|
||||
if (offset == null || offset == undefined || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
return c.text(msgs.InvalidOffsetMsg, 400)
|
||||
}
|
||||
const resultsQuery = `${query} order by id desc limit ? offset ?`;
|
||||
const { results } = await c.env.DB.prepare(resultsQuery).bind(
|
||||
|
||||
@@ -46,6 +46,129 @@ const messages: LocaleMessages = {
|
||||
PasswordLoginDisabledMsg: "Password login is disabled",
|
||||
EmailPasswordRequiredMsg: "Email and password are required",
|
||||
AddressNotFoundMsg: "Address not found",
|
||||
|
||||
// Common messages (merged similar ones)
|
||||
OperationFailedMsg: "Operation failed",
|
||||
RequiredFieldMsg: "Required field is missing",
|
||||
InvalidInputMsg: "Invalid input",
|
||||
|
||||
// Address related
|
||||
NameTooShortMsg: "Name is too short",
|
||||
NameTooLongMsg: "Name is too long",
|
||||
InvalidDomainMsg: "Invalid domain",
|
||||
AddressAlreadyExistsMsg: "Address already exists",
|
||||
MaxAddressCountReachedMsg: "Max address count reached",
|
||||
AddressNotBindedMsg: "Address is not binded",
|
||||
AddressAlreadyBindedMsg: "Address is already binded, please unbind first",
|
||||
TargetUserNotFoundMsg: "Target user not found",
|
||||
|
||||
// Send mail related
|
||||
NoBalanceMsg: "No balance",
|
||||
AddressBlockedMsg: "Address is blocked",
|
||||
SubjectEmptyMsg: "Subject is empty",
|
||||
ContentEmptyMsg: "Content is empty",
|
||||
AlreadyRequestedMsg: "Already requested",
|
||||
EnableResendOrSmtpMsg: "Please enable resend or smtp for this domain",
|
||||
EnableResendOrSmtpWithVerifiedMsg: "Please enable resend or smtp for this domain, or add recipient to verified address list",
|
||||
InvalidToMailMsg: "Invalid recipient address",
|
||||
|
||||
// Admin related
|
||||
InvalidAddressIdMsg: "Invalid address_id",
|
||||
EnableKVMsg: "Please enable KV first",
|
||||
EnableSendMailMsg: "Please enable SEND_MAIL first",
|
||||
InvalidCleanupConfigMsg: "Invalid cleanType or cleanDays",
|
||||
InvalidCleanTypeMsg: "Invalid cleanType",
|
||||
EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify",
|
||||
VerifyMailDomainInvalidMsg: "VerifyMailSender domain must be in",
|
||||
InvalidMaxAddressCountMsg: "Invalid maxAddressCount",
|
||||
FailedDeleteUserMsg: "Failed to delete user",
|
||||
InvalidUserIdMsg: "Invalid user_id",
|
||||
InvalidRoleTextMsg: "Invalid role_text",
|
||||
|
||||
// SQL validation
|
||||
SqlEmptyMsg: "SQL statement is empty",
|
||||
SqlTooLongMsg: "SQL statement is too long (max 1000 characters)",
|
||||
SqlOnlyDeleteMsg: "Only DELETE statements are allowed",
|
||||
SqlSingleStatementMsg: "Only single SQL statement is allowed",
|
||||
SqlNoCommentsMsg: "SQL comments are not allowed",
|
||||
|
||||
// Passkey related
|
||||
InvalidPasskeyNameMsg: "Invalid passkey name",
|
||||
PasskeyNotFoundMsg: "Passkey not found",
|
||||
AuthenticationFailedMsg: "Authentication failed",
|
||||
RegistrationFailedMsg: "Registration failed",
|
||||
|
||||
// Auto reply related
|
||||
AutoReplyDisabledMsg: "Auto reply is disabled",
|
||||
InvalidAutoReplyMsg: "Invalid subject or message",
|
||||
SubjectOrMessageTooLongMsg: "Subject or message is too long",
|
||||
|
||||
// Bind address related
|
||||
NoAddressOrUserTokenMsg: "No address or user token",
|
||||
InvalidAddressOrUserTokenMsg: "Invalid address or user token",
|
||||
|
||||
// Pagination related
|
||||
InvalidLimitMsg: "Invalid limit",
|
||||
InvalidOffsetMsg: "Invalid offset",
|
||||
|
||||
// Clear inbox/sent items related
|
||||
FailedClearInboxMsg: "Failed to clear inbox",
|
||||
FailedClearSentItemsMsg: "Failed to clear sent items",
|
||||
|
||||
// Webhook related
|
||||
WebhookNotAllowedForUserMsg: "Webhook settings is not allowed for this user",
|
||||
|
||||
// IP blacklist related
|
||||
InvalidIpBlacklistSettingMsg: "Invalid IP blacklist setting",
|
||||
BlacklistExceedsMaxSizeMsg: "Blacklist exceeds maximum size",
|
||||
|
||||
// Telegram bot messages
|
||||
TgUnableGetUserInfoMsg: "Unable to get user info",
|
||||
TgNoPermissionMsg: "You don't have permission to use this bot",
|
||||
TgWelcomeMsg: "Welcome! You can open the mini app",
|
||||
TgCurrentPrefixMsg: "Current prefix enabled:",
|
||||
TgCurrentDomainsMsg: "Available domains:",
|
||||
TgAvailableCommandsMsg: "Available commands:",
|
||||
TgCreateSuccessMsg: "Address created successfully:",
|
||||
TgCreateFailedMsg: "Failed to create address:",
|
||||
TgBindSuccessMsg: "Binding successful:",
|
||||
TgBindFailedMsg: "Binding failed:",
|
||||
TgUnbindSuccessMsg: "Unbinding successful:",
|
||||
TgUnbindFailedMsg: "Unbinding failed:",
|
||||
TgDeleteSuccessMsg: "Deleted successfully:",
|
||||
TgDeleteFailedMsg: "Delete failed:",
|
||||
TgAddressListMsg: "Address list:",
|
||||
TgGetAddressFailedMsg: "Failed to get address list:",
|
||||
TgCleanSuccessMsg: "Invalid addresses cleaned:",
|
||||
TgCurrentAddressListMsg: "Current address list:",
|
||||
TgCleanFailedMsg: "Failed to clean invalid addresses:",
|
||||
TgNotBoundAddressMsg: "This address is not bound:",
|
||||
TgInvalidAddressMsg: "Invalid address",
|
||||
TgNoMoreMailsMsg: "No more mails",
|
||||
TgNoMailMsg: "No mail",
|
||||
TgGetMailFailedMsg: "Failed to get mail:",
|
||||
TgParseMailFailedMsg: "Failed to parse mail:",
|
||||
TgViewMailBtnMsg: "View Mail",
|
||||
TgPrevBtnMsg: "Prev",
|
||||
TgNextBtnMsg: "Next",
|
||||
TgPleaseInputCredentialMsg: "Please enter credential",
|
||||
TgPleaseInputAddressMsg: "Please enter address",
|
||||
TgAddressMsg: "Address:",
|
||||
TgPasswordMsg: "Password:",
|
||||
TgCredentialMsg: "Credential:",
|
||||
TgNoSenderMsg: "No sender",
|
||||
TgMsgTooLongMsg: "Message too long, please view in mini app",
|
||||
TgParseFailedViewInAppMsg: "Parse failed, please view in mini app",
|
||||
TgMaxAddressReachedMsg: "Maximum address limit reached",
|
||||
TgMaxAddressReachedCleanMsg: "Maximum address limit reached, please /cleaninvalidaddress first",
|
||||
TgInvalidCredentialMsg: "Invalid credential",
|
||||
TgAddressNotYoursMsg: "This address does not belong to you",
|
||||
TgLangSetSuccessMsg: "Language set successfully:",
|
||||
TgCurrentLangMsg: "Current language:",
|
||||
TgSelectLangMsg: "Please select language:",
|
||||
TgNoPermissionViewMailMsg: "No permission to view this mail",
|
||||
TgBotTokenRequiredMsg: "TELEGRAM_BOT_TOKEN is required",
|
||||
TgLangFeatureDisabledMsg: "Language setting feature is disabled. System default language is used.",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -44,4 +44,127 @@ export type LocaleMessages = {
|
||||
PasswordLoginDisabledMsg: string
|
||||
EmailPasswordRequiredMsg: string
|
||||
AddressNotFoundMsg: string
|
||||
|
||||
// Common messages (merged similar ones)
|
||||
OperationFailedMsg: string
|
||||
RequiredFieldMsg: string
|
||||
InvalidInputMsg: string
|
||||
|
||||
// Address related
|
||||
NameTooShortMsg: string
|
||||
NameTooLongMsg: string
|
||||
InvalidDomainMsg: string
|
||||
AddressAlreadyExistsMsg: string
|
||||
MaxAddressCountReachedMsg: string
|
||||
AddressNotBindedMsg: string
|
||||
AddressAlreadyBindedMsg: string
|
||||
TargetUserNotFoundMsg: string
|
||||
|
||||
// Send mail related
|
||||
NoBalanceMsg: string
|
||||
AddressBlockedMsg: string
|
||||
SubjectEmptyMsg: string
|
||||
ContentEmptyMsg: string
|
||||
AlreadyRequestedMsg: string
|
||||
EnableResendOrSmtpMsg: string
|
||||
EnableResendOrSmtpWithVerifiedMsg: string
|
||||
InvalidToMailMsg: string
|
||||
|
||||
// Admin related
|
||||
InvalidAddressIdMsg: string
|
||||
EnableKVMsg: string
|
||||
EnableSendMailMsg: string
|
||||
InvalidCleanupConfigMsg: string
|
||||
InvalidCleanTypeMsg: string
|
||||
EnableKVForMailVerifyMsg: string
|
||||
VerifyMailDomainInvalidMsg: string
|
||||
InvalidMaxAddressCountMsg: string
|
||||
FailedDeleteUserMsg: string
|
||||
InvalidUserIdMsg: string
|
||||
InvalidRoleTextMsg: string
|
||||
|
||||
// SQL validation
|
||||
SqlEmptyMsg: string
|
||||
SqlTooLongMsg: string
|
||||
SqlOnlyDeleteMsg: string
|
||||
SqlSingleStatementMsg: string
|
||||
SqlNoCommentsMsg: string
|
||||
|
||||
// Passkey related
|
||||
InvalidPasskeyNameMsg: string
|
||||
PasskeyNotFoundMsg: string
|
||||
AuthenticationFailedMsg: string
|
||||
RegistrationFailedMsg: string
|
||||
|
||||
// Auto reply related
|
||||
AutoReplyDisabledMsg: string
|
||||
InvalidAutoReplyMsg: string
|
||||
SubjectOrMessageTooLongMsg: string
|
||||
|
||||
// Bind address related
|
||||
NoAddressOrUserTokenMsg: string
|
||||
InvalidAddressOrUserTokenMsg: string
|
||||
|
||||
// Pagination related
|
||||
InvalidLimitMsg: string
|
||||
InvalidOffsetMsg: string
|
||||
|
||||
// Clear inbox/sent items related
|
||||
FailedClearInboxMsg: string
|
||||
FailedClearSentItemsMsg: string
|
||||
|
||||
// Webhook related
|
||||
WebhookNotAllowedForUserMsg: string
|
||||
|
||||
// IP blacklist related
|
||||
InvalidIpBlacklistSettingMsg: string
|
||||
BlacklistExceedsMaxSizeMsg: string
|
||||
|
||||
// Telegram bot messages
|
||||
TgUnableGetUserInfoMsg: string
|
||||
TgNoPermissionMsg: string
|
||||
TgWelcomeMsg: string
|
||||
TgCurrentPrefixMsg: string
|
||||
TgCurrentDomainsMsg: string
|
||||
TgAvailableCommandsMsg: string
|
||||
TgCreateSuccessMsg: string
|
||||
TgCreateFailedMsg: string
|
||||
TgBindSuccessMsg: string
|
||||
TgBindFailedMsg: string
|
||||
TgUnbindSuccessMsg: string
|
||||
TgUnbindFailedMsg: string
|
||||
TgDeleteSuccessMsg: string
|
||||
TgDeleteFailedMsg: string
|
||||
TgAddressListMsg: string
|
||||
TgGetAddressFailedMsg: string
|
||||
TgCleanSuccessMsg: string
|
||||
TgCurrentAddressListMsg: string
|
||||
TgCleanFailedMsg: string
|
||||
TgNotBoundAddressMsg: string
|
||||
TgInvalidAddressMsg: string
|
||||
TgNoMoreMailsMsg: string
|
||||
TgNoMailMsg: string
|
||||
TgGetMailFailedMsg: string
|
||||
TgParseMailFailedMsg: string
|
||||
TgViewMailBtnMsg: string
|
||||
TgPrevBtnMsg: string
|
||||
TgNextBtnMsg: string
|
||||
TgPleaseInputCredentialMsg: string
|
||||
TgPleaseInputAddressMsg: string
|
||||
TgAddressMsg: string
|
||||
TgPasswordMsg: string
|
||||
TgCredentialMsg: string
|
||||
TgNoSenderMsg: string
|
||||
TgMsgTooLongMsg: string
|
||||
TgParseFailedViewInAppMsg: string
|
||||
TgMaxAddressReachedMsg: string
|
||||
TgMaxAddressReachedCleanMsg: string
|
||||
TgInvalidCredentialMsg: string
|
||||
TgAddressNotYoursMsg: string
|
||||
TgLangSetSuccessMsg: string
|
||||
TgCurrentLangMsg: string
|
||||
TgSelectLangMsg: string
|
||||
TgNoPermissionViewMailMsg: string
|
||||
TgBotTokenRequiredMsg: string
|
||||
TgLangFeatureDisabledMsg: string
|
||||
}
|
||||
|
||||
@@ -46,6 +46,129 @@ const messages: LocaleMessages = {
|
||||
PasswordLoginDisabledMsg: "密码登录已禁用",
|
||||
EmailPasswordRequiredMsg: "邮箱和密码不能为空",
|
||||
AddressNotFoundMsg: "邮箱地址不存在",
|
||||
|
||||
// Common messages (merged similar ones)
|
||||
OperationFailedMsg: "操作失败",
|
||||
RequiredFieldMsg: "缺少必填字段",
|
||||
InvalidInputMsg: "输入无效",
|
||||
|
||||
// Address related
|
||||
NameTooShortMsg: "名称太短",
|
||||
NameTooLongMsg: "名称太长",
|
||||
InvalidDomainMsg: "无效的域名",
|
||||
AddressAlreadyExistsMsg: "邮箱地址已存在",
|
||||
MaxAddressCountReachedMsg: "已达到最大地址数量限制",
|
||||
AddressNotBindedMsg: "邮箱地址未绑定",
|
||||
AddressAlreadyBindedMsg: "邮箱地址已绑定, 请先解绑",
|
||||
TargetUserNotFoundMsg: "目标用户不存在",
|
||||
|
||||
// Send mail related
|
||||
NoBalanceMsg: "余额不足",
|
||||
AddressBlockedMsg: "地址已被屏蔽",
|
||||
SubjectEmptyMsg: "主题不能为空",
|
||||
ContentEmptyMsg: "内容不能为空",
|
||||
AlreadyRequestedMsg: "已经申请过了",
|
||||
EnableResendOrSmtpMsg: "请先为此域名启用 resend 或 smtp",
|
||||
EnableResendOrSmtpWithVerifiedMsg: "请先为此域名启用 resend 或 smtp,或将收件人添加到已验证地址列表",
|
||||
InvalidToMailMsg: "收件人地址无效",
|
||||
|
||||
// Admin related
|
||||
InvalidAddressIdMsg: "无效的 address_id",
|
||||
EnableKVMsg: "请先启用 KV",
|
||||
EnableSendMailMsg: "请先启用 SEND_MAIL",
|
||||
InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays",
|
||||
InvalidCleanTypeMsg: "无效的 cleanType",
|
||||
EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV",
|
||||
VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在",
|
||||
InvalidMaxAddressCountMsg: "无效的 maxAddressCount",
|
||||
FailedDeleteUserMsg: "删除用户失败",
|
||||
InvalidUserIdMsg: "无效的 user_id",
|
||||
InvalidRoleTextMsg: "无效的 role_text",
|
||||
|
||||
// SQL validation
|
||||
SqlEmptyMsg: "SQL 语句为空",
|
||||
SqlTooLongMsg: "SQL 语句过长 (最大 1000 字符)",
|
||||
SqlOnlyDeleteMsg: "只允许 DELETE 语句",
|
||||
SqlSingleStatementMsg: "只允许单条 SQL 语句",
|
||||
SqlNoCommentsMsg: "不允许 SQL 注释",
|
||||
|
||||
// Passkey related
|
||||
InvalidPasskeyNameMsg: "无效的 passkey 名称",
|
||||
PasskeyNotFoundMsg: "Passkey 不存在",
|
||||
AuthenticationFailedMsg: "认证失败",
|
||||
RegistrationFailedMsg: "注册失败",
|
||||
|
||||
// Auto reply related
|
||||
AutoReplyDisabledMsg: "自动回复已禁用",
|
||||
InvalidAutoReplyMsg: "无效的主题或消息",
|
||||
SubjectOrMessageTooLongMsg: "主题或消息太长",
|
||||
|
||||
// Bind address related
|
||||
NoAddressOrUserTokenMsg: "缺少地址或用户令牌",
|
||||
InvalidAddressOrUserTokenMsg: "无效的地址或用户令牌",
|
||||
|
||||
// Pagination related
|
||||
InvalidLimitMsg: "无效的 limit 参数",
|
||||
InvalidOffsetMsg: "无效的 offset 参数",
|
||||
|
||||
// Clear inbox/sent items related
|
||||
FailedClearInboxMsg: "清空收件箱失败",
|
||||
FailedClearSentItemsMsg: "清空已发送邮件失败",
|
||||
|
||||
// Webhook related
|
||||
WebhookNotAllowedForUserMsg: "此用户不允许使用 Webhook 设置",
|
||||
|
||||
// IP blacklist related
|
||||
InvalidIpBlacklistSettingMsg: "无效的 IP 黑名单设置",
|
||||
BlacklistExceedsMaxSizeMsg: "黑名单超出最大条目限制",
|
||||
|
||||
// Telegram bot messages
|
||||
TgUnableGetUserInfoMsg: "无法获取用户信息",
|
||||
TgNoPermissionMsg: "您没有权限使用此机器人",
|
||||
TgWelcomeMsg: "欢迎使用本机器人, 您可以打开 mini app",
|
||||
TgCurrentPrefixMsg: "当前已启用前缀:",
|
||||
TgCurrentDomainsMsg: "当前可用域名:",
|
||||
TgAvailableCommandsMsg: "请使用以下命令:",
|
||||
TgCreateSuccessMsg: "创建地址成功:",
|
||||
TgCreateFailedMsg: "创建地址失败:",
|
||||
TgBindSuccessMsg: "绑定成功:",
|
||||
TgBindFailedMsg: "绑定失败:",
|
||||
TgUnbindSuccessMsg: "解绑成功:",
|
||||
TgUnbindFailedMsg: "解绑失败:",
|
||||
TgDeleteSuccessMsg: "删除成功:",
|
||||
TgDeleteFailedMsg: "删除失败:",
|
||||
TgAddressListMsg: "地址列表:",
|
||||
TgGetAddressFailedMsg: "获取地址列表失败:",
|
||||
TgCleanSuccessMsg: "清理无效地址成功:",
|
||||
TgCurrentAddressListMsg: "当前地址列表:",
|
||||
TgCleanFailedMsg: "清理无效地址失败:",
|
||||
TgNotBoundAddressMsg: "未绑定此地址:",
|
||||
TgInvalidAddressMsg: "无效地址",
|
||||
TgNoMoreMailsMsg: "已经没有邮件了",
|
||||
TgNoMailMsg: "无邮件",
|
||||
TgGetMailFailedMsg: "获取邮件失败:",
|
||||
TgParseMailFailedMsg: "解析邮件失败:",
|
||||
TgViewMailBtnMsg: "查看邮件",
|
||||
TgPrevBtnMsg: "上一条",
|
||||
TgNextBtnMsg: "下一条",
|
||||
TgPleaseInputCredentialMsg: "请输入凭证",
|
||||
TgPleaseInputAddressMsg: "请输入地址",
|
||||
TgAddressMsg: "地址:",
|
||||
TgPasswordMsg: "密码:",
|
||||
TgCredentialMsg: "凭证:",
|
||||
TgNoSenderMsg: "无发件人",
|
||||
TgMsgTooLongMsg: "消息过长请到 mini app 查看",
|
||||
TgParseFailedViewInAppMsg: "解析失败,请打开 mini app 查看",
|
||||
TgMaxAddressReachedMsg: "绑定地址数量已达上限",
|
||||
TgMaxAddressReachedCleanMsg: "绑定地址数量已达上限, 请先 /cleaninvalidaddress",
|
||||
TgInvalidCredentialMsg: "无效凭证",
|
||||
TgAddressNotYoursMsg: "此地址不属于您",
|
||||
TgLangSetSuccessMsg: "语言设置成功:",
|
||||
TgCurrentLangMsg: "当前语言:",
|
||||
TgSelectLangMsg: "请选择语言:",
|
||||
TgNoPermissionViewMailMsg: "无权查看此邮件",
|
||||
TgBotTokenRequiredMsg: "需要设置 TELEGRAM_BOT_TOKEN",
|
||||
TgLangFeatureDisabledMsg: "语言设置功能已禁用,使用系统默认语言",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7,8 +7,7 @@ export default {
|
||||
// 修改地址密码
|
||||
changePassword: async (c: Context<HonoCustomType>) => {
|
||||
const { new_password } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address, address_id } = c.get("jwtPayload");
|
||||
|
||||
// 检查功能是否启用
|
||||
@@ -39,8 +38,7 @@ export default {
|
||||
// 地址密码登录
|
||||
login: async (c: Context<HonoCustomType>) => {
|
||||
const { email, password, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
|
||||
// 检查功能是否启用
|
||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Context } from "hono";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import i18n from "../i18n";
|
||||
|
||||
|
||||
export default {
|
||||
getAutoReply: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
return c.text(msgs.AutoReplyDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const results = await c.env.DB.prepare(
|
||||
@@ -23,17 +25,18 @@ export default {
|
||||
})
|
||||
},
|
||||
saveAutoReply: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
return c.text(msgs.AutoReplyDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload");
|
||||
const { auto_reply } = await c.req.json();
|
||||
const { name, subject, source_prefix, message, enabled } = auto_reply;
|
||||
if ((!subject || !message) && enabled) {
|
||||
return c.text("Invalid subject or message", 400)
|
||||
return c.text(msgs.InvalidAutoReplyMsg, 400)
|
||||
}
|
||||
else if (subject.length > 255 || message.length > 255) {
|
||||
return c.text("Subject or message too long", 400)
|
||||
return c.text(msgs.SubjectOrMessageTooLongMsg, 400)
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT OR REPLACE INTO auto_reply_mails`
|
||||
@@ -44,7 +47,7 @@ export default {
|
||||
subject || '', message || '', enabled ? 1 : 0
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to auto_reply settings", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
|
||||
@@ -45,8 +45,7 @@ api.get('/api/mail/:mail_id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/mails/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
@@ -64,8 +63,7 @@ api.delete('/api/mails/:id', async (c) => {
|
||||
api.get('/api/settings', async (c) => {
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
const user_role = c.get("userRolePayload")
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (address_id && address_id > 0) {
|
||||
try {
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
@@ -106,8 +104,7 @@ api.get('/api/settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/api/new_address', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL)
|
||||
&& !c.get("userPayload")
|
||||
) {
|
||||
@@ -171,8 +168,7 @@ api.delete('/api/delete_address', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/clear_inbox', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
@@ -181,7 +177,7 @@ api.delete('/api/clear_inbox', async (c) => {
|
||||
`DELETE FROM raw_mails WHERE address = ?`
|
||||
).bind(address).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to clear inbox", 500)
|
||||
return c.text(msgs.FailedClearInboxMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
@@ -189,8 +185,7 @@ api.delete('/api/clear_inbox', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/clear_sent_items', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
@@ -199,7 +194,7 @@ api.delete('/api/clear_sent_items', async (c) => {
|
||||
`DELETE FROM sendbox WHERE address = ?`
|
||||
).bind(address).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to clear sent items", 500)
|
||||
return c.text(msgs.FailedClearSentItemsMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
|
||||
@@ -14,9 +14,10 @@ import { handleListQuery } from '../common'
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
api.post('/api/requset_send_mail_access', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address } = c.get("jwtPayload")
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
try {
|
||||
const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0);
|
||||
@@ -26,14 +27,14 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
address, default_balance, default_balance > 0 ? 1 : 0
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to request send mail access", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
return c.text("Already requested", 400)
|
||||
return c.text(msgs.AlreadyRequestedMsg, 400)
|
||||
}
|
||||
return c.text("Failed to request send mail access", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
@@ -126,14 +127,15 @@ export const sendMail = async (
|
||||
isAdmin?: boolean
|
||||
}
|
||||
): Promise<void> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!address) {
|
||||
throw new Error("No address")
|
||||
throw new Error(msgs.AddressNotFoundMsg)
|
||||
}
|
||||
// check domain
|
||||
const mailDomain = address.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
throw new Error("Invalid domain")
|
||||
throw new Error(msgs.InvalidDomainMsg)
|
||||
}
|
||||
const user_role = c.get("userRolePayload");
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
@@ -150,7 +152,7 @@ export const sendMail = async (
|
||||
where address = ? and enabled = 1`
|
||||
).bind(address).first<number>("balance");
|
||||
if (!balance || balance <= 0) {
|
||||
throw new Error("No balance")
|
||||
throw new Error(msgs.NoBalanceMsg)
|
||||
}
|
||||
}
|
||||
const {
|
||||
@@ -158,18 +160,18 @@ export const sendMail = async (
|
||||
subject, content, is_html
|
||||
} = reqJson;
|
||||
if (!to_mail) {
|
||||
throw new Error("Invalid to mail")
|
||||
throw new Error(msgs.InvalidToMailMsg)
|
||||
}
|
||||
// check SEND_BLOCK_LIST_KEY
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY) as string[];
|
||||
if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) {
|
||||
throw new Error("to_mail address is blocked")
|
||||
throw new Error(msgs.AddressBlockedMsg)
|
||||
}
|
||||
if (!subject) {
|
||||
throw new Error("Subject is empty")
|
||||
throw new Error(msgs.SubjectEmptyMsg)
|
||||
}
|
||||
if (!content) {
|
||||
throw new Error("Content is empty")
|
||||
throw new Error(msgs.ContentEmptyMsg)
|
||||
}
|
||||
|
||||
// send to verified address list, do not update balance
|
||||
@@ -202,9 +204,9 @@ export const sendMail = async (
|
||||
}
|
||||
else {
|
||||
if (c.env.SEND_MAIL) {
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}. Or add ${to_mail} to verified address list`);
|
||||
throw new Error(`${msgs.EnableResendOrSmtpWithVerifiedMsg} (${mailDomain})`);
|
||||
}
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}`);
|
||||
throw new Error(`${msgs.EnableResendOrSmtpMsg} (${mailDomain})`);
|
||||
}
|
||||
|
||||
// update balance
|
||||
@@ -253,11 +255,12 @@ api.post('/api/send_mail', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/external/api/send_mail', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { token } = await c.req.json();
|
||||
try {
|
||||
const { address } = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
const reqJson = await c.req.json();
|
||||
await sendMail(c, address as string, reqJson);
|
||||
@@ -289,8 +292,7 @@ api.get('/api/sendbox', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/sendbox/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings, WebhookSettings } from "../models";
|
||||
import { commonParseMail, sendWebhook } from "../common";
|
||||
import i18n from "../i18n";
|
||||
|
||||
|
||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address } = c.get("jwtPayload")
|
||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
|
||||
return c.text("Webhook settings is not allowed for this user", 403);
|
||||
return c.text(msgs.WebhookNotAllowedForUserMsg, 403);
|
||||
}
|
||||
const settings = await c.env.KV.get<WebhookSettings>(
|
||||
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
|
||||
@@ -18,10 +20,11 @@ async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response>
|
||||
|
||||
|
||||
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address } = c.get("jwtPayload")
|
||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
|
||||
return c.text("Webhook settings is not allowed for this user", 403);
|
||||
return c.text(msgs.WebhookNotAllowedForUserMsg, 403);
|
||||
}
|
||||
const settings = await c.req.json<WebhookSettings>();
|
||||
await c.env.KV.put(
|
||||
|
||||
@@ -3,9 +3,11 @@ import { Jwt } from "hono/utils/jwt";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getBooleanValue, getIntValue, getJsonSetting } from "../utils";
|
||||
import { deleteAddressWithData, newAddress, generateRandomName } from "../common";
|
||||
import { LocaleMessages } from "../i18n/type";
|
||||
|
||||
export const tgUserNewAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
c: Context<HonoCustomType>, userId: string, address: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||
if (c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
@@ -23,7 +25,7 @@ export const tgUserNewAddress = async (
|
||||
const [name, domain] = trimmedAddress.includes("@") ? trimmedAddress.split("@") : [trimmedAddress, null];
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限");
|
||||
throw Error(msgs.TgMaxAddressReachedMsg);
|
||||
}
|
||||
// Generate name if disabled or not provided
|
||||
const finalName = (!name || disableCustomAddressName) ? generateRandomName(c) : name;
|
||||
@@ -48,7 +50,8 @@ export const tgUserNewAddress = async (
|
||||
}
|
||||
|
||||
export const jwtListToAddressData = async (
|
||||
c: Context<HonoCustomType>, jwtList: string[]
|
||||
c: Context<HonoCustomType>, jwtList: string[],
|
||||
msgs: LocaleMessages
|
||||
): Promise<{
|
||||
addressList: string[], addressIdMap: Record<string, number>,
|
||||
invalidJwtList: string[]
|
||||
@@ -63,35 +66,36 @@ export const jwtListToAddressData = async (
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(address_id).first("name");
|
||||
if (!name) {
|
||||
addressList.push("无效地址");
|
||||
addressList.push(msgs.TgInvalidAddressMsg);
|
||||
invalidJwtList.push(jwt);
|
||||
continue;
|
||||
}
|
||||
addressList.push(address as string);
|
||||
addressIdMap[address as string] = address_id as number;
|
||||
} catch (e) {
|
||||
addressList.push("无效凭证");
|
||||
addressList.push(msgs.TgInvalidCredentialMsg);
|
||||
invalidJwtList.push(jwt);
|
||||
console.log(`获取地址列表失败: ${(e as Error).message}`);
|
||||
console.log(`Failed to get address list: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
return { addressList, addressIdMap, invalidJwtList };
|
||||
}
|
||||
|
||||
export const bindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<string> => {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
throw Error("无效凭证");
|
||||
throw Error(msgs.TgInvalidCredentialMsg);
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (address as string in addressIdMap) {
|
||||
return address as string;
|
||||
}
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限, 请先 /cleaninvalidaddress");
|
||||
throw Error(msgs.TgMaxAddressReachedCleanMsg);
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||
// for mail push to telegram
|
||||
@@ -133,12 +137,13 @@ export const unbindTelegramByAddress = async (
|
||||
|
||||
|
||||
export const deleteTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
c: Context<HonoCustomType>, userId: string, address: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<boolean> => {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (!(address in addressIdMap)) {
|
||||
throw Error("此地址不属于您");
|
||||
throw Error(msgs.TgAddressNotYoursMsg);
|
||||
}
|
||||
await deleteAddressWithData(c, null, addressIdMap[address])
|
||||
return true;
|
||||
|
||||
@@ -5,26 +5,29 @@ import { Writable } from 'node:stream'
|
||||
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
|
||||
import settings from './settings'
|
||||
import miniapp from './miniapp'
|
||||
import i18n from '../i18n'
|
||||
|
||||
export const api = new Hono<HonoCustomType>();
|
||||
export { sendMailToTelegram }
|
||||
|
||||
api.use("/telegram/*", async (c, next) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!c.env.TELEGRAM_BOT_TOKEN) {
|
||||
return c.text("TELEGRAM_BOT_TOKEN is required", 400);
|
||||
return c.text(msgs.TgBotTokenRequiredMsg, 400);
|
||||
}
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is required", 400);
|
||||
return c.text(msgs.KVNotAvailableMsg, 400);
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
|
||||
api.use("/admin/telegram/*", async (c, next) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!c.env.TELEGRAM_BOT_TOKEN) {
|
||||
return c.text("TELEGRAM_BOT_TOKEN is required", 400);
|
||||
return c.text(msgs.TgBotTokenRequiredMsg, 400);
|
||||
}
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is required", 400);
|
||||
return c.text(msgs.KVNotAvailableMsg, 400);
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
@@ -51,7 +54,7 @@ api.post("/admin/telegram/init", async (c) => {
|
||||
console.log(`setting webhook to ${webhookUrl}`);
|
||||
const bot = newTelegramBot(c, token);
|
||||
await bot.telegram.setWebhook(webhookUrl)
|
||||
await initTelegramBotCommands(bot);
|
||||
await initTelegramBotCommands(c, bot);
|
||||
return c.json({
|
||||
message: "webhook set successfully",
|
||||
});
|
||||
|
||||
@@ -84,8 +84,7 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
||||
|
||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
@@ -95,7 +94,7 @@ async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response>
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
// get the address list from the KV
|
||||
const res = await tgUserNewAddress(c, userId, address)
|
||||
const res = await tgUserNewAddress(c, userId, address, msgs)
|
||||
return c.json(res);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -105,9 +104,10 @@ async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response>
|
||||
|
||||
async function bindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, jwt } = await c.req.json();
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
await bindTelegramAddress(c, userId, jwt);
|
||||
await bindTelegramAddress(c, userId, jwt, msgs);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
catch (e) {
|
||||
@@ -129,10 +129,11 @@ async function unbindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
|
||||
async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, mailId } = await c.req.json();
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
const result = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where id = ?`
|
||||
).bind(mailId).first();
|
||||
@@ -140,14 +141,14 @@ async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const superUser = settings?.enableGlobalMailPush && settings?.globalMailPushList.includes(userId);
|
||||
if (!superUser) {
|
||||
if (result?.address && !(result.address as string in addressIdMap)) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
return c.text(msgs.TgNoPermissionViewMailMsg, 403);
|
||||
}
|
||||
const address_id = addressIdMap[result?.address as string];
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
return c.text(msgs.TgNoPermissionViewMailMsg, 403);
|
||||
}
|
||||
}
|
||||
return c.json(result);
|
||||
|
||||
@@ -4,48 +4,79 @@ import { Telegraf, Context as TgContext, Markup } from "telegraf";
|
||||
import { callbackQuery } from "telegraf/filters";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
import { getBooleanValue, getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
|
||||
import { commonParseMail } from "../common";
|
||||
import { UserFromGetMe } from "telegraf/types";
|
||||
import i18n from "../i18n";
|
||||
import { LocaleMessages } from "../i18n/type";
|
||||
|
||||
// Helper to get messages by userId
|
||||
const getTgMessages = async (
|
||||
c: Context<HonoCustomType>,
|
||||
ctx?: TgContext,
|
||||
userId?: string | null
|
||||
): Promise<LocaleMessages> => {
|
||||
// Check if user language config is enabled (default false)
|
||||
if (!getBooleanValue(c.env.TG_ALLOW_USER_LANG)) {
|
||||
return i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
}
|
||||
|
||||
const uid = userId || ctx?.message?.from?.id?.toString() || ctx?.callbackQuery?.from?.id?.toString();
|
||||
if (uid) {
|
||||
const savedLang = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:lang:${uid}`);
|
||||
if (savedLang) { return i18n.getMessages(savedLang); }
|
||||
}
|
||||
return i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
};
|
||||
|
||||
// Bilingual command descriptions with full usage instructions
|
||||
const COMMANDS = [
|
||||
{
|
||||
command: "start",
|
||||
description: "开始使用"
|
||||
description: "开始使用 | Get started"
|
||||
},
|
||||
{
|
||||
command: "new",
|
||||
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new, 通过 /new <name>@<domain> 可以指定, name [a-z0-9] 有效, name 为空则随机生成, @<domain> 可选"
|
||||
description: "新建邮箱, /new <name>@<domain>, name[a-z0-9]有效, 为空随机生成, @domain可选 | Create address, /new <name>@<domain>, name[a-z0-9] valid, empty=random, @domain optional"
|
||||
},
|
||||
{
|
||||
command: "address",
|
||||
description: "查看邮箱地址列表"
|
||||
description: "查看邮箱地址列表 | View address list"
|
||||
},
|
||||
{
|
||||
command: "bind",
|
||||
description: "绑定邮箱地址, 请输入 /bind <邮箱地址凭证>"
|
||||
description: "绑定邮箱, /bind <邮箱地址凭证> | Bind address, /bind <credential>"
|
||||
},
|
||||
{
|
||||
command: "unbind",
|
||||
description: "解绑邮箱地址, 请输入 /unbind <邮箱地址>"
|
||||
description: "解绑邮箱, /unbind <邮箱地址> | Unbind address, /unbind <address>"
|
||||
},
|
||||
{
|
||||
command: "delete",
|
||||
description: "删除邮箱地址, 请输入 /delete <邮箱地址>"
|
||||
description: "删除邮箱, /delete <邮箱地址> | Delete address, /delete <address>"
|
||||
},
|
||||
{
|
||||
command: "mails",
|
||||
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
|
||||
description: "查看邮件, /mails <邮箱地址>, 不输入地址默认第一个 | View mails, /mails <address>, default first if empty"
|
||||
},
|
||||
{
|
||||
command: "cleaninvalidaddress",
|
||||
description: "清理无效地址, 请输入 /cleaninvalidaddress"
|
||||
description: "清理无效地址 | Clean invalid addresses"
|
||||
},
|
||||
{
|
||||
command: "lang",
|
||||
description: "设置语言 /lang <zh|en> | Set language /lang <zh|en>"
|
||||
},
|
||||
]
|
||||
|
||||
export const getTelegramCommands = (c: Context<HonoCustomType>) => {
|
||||
return getBooleanValue(c.env.TG_ALLOW_USER_LANG)
|
||||
? COMMANDS
|
||||
: COMMANDS.filter(cmd => cmd.command !== "lang");
|
||||
}
|
||||
|
||||
export function newTelegramBot(c: Context<HonoCustomType>, token: string): Telegraf {
|
||||
const bot = new Telegraf(token);
|
||||
const botInfo = getJsonObjectValue<UserFromGetMe>(c.env.TG_BOT_INFO);
|
||||
@@ -61,14 +92,16 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
|
||||
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
if (settings?.enableAllowList && settings?.enableAllowList
|
||||
if (settings?.enableAllowList
|
||||
&& !settings.allowList.includes(userId.toString())
|
||||
) {
|
||||
return await ctx.reply("您没有权限使用此机器人");
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgNoPermissionMsg);
|
||||
}
|
||||
try {
|
||||
await next();
|
||||
@@ -79,153 +112,192 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
})
|
||||
|
||||
bot.command("start", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const prefix = getStringValue(c.env.PREFIX)
|
||||
const domains = getDomains(c);
|
||||
const commands = getTelegramCommands(c);
|
||||
return await ctx.reply(
|
||||
"欢迎使用本机器人, 您可以打开 mini app \n\n"
|
||||
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
||||
+ `当前可用域名: ${JSON.stringify(domains)}\n`
|
||||
+ "请使用以下命令:\n"
|
||||
+ COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n")
|
||||
`${msgs.TgWelcomeMsg}\n\n`
|
||||
+ (prefix ? `${msgs.TgCurrentPrefixMsg} ${prefix}\n` : '')
|
||||
+ `${msgs.TgCurrentDomainsMsg} ${JSON.stringify(domains)}\n`
|
||||
+ `${msgs.TgAvailableCommandsMsg}\n`
|
||||
+ commands.map(cmd => `/${cmd.command}: ${cmd.description}`).join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
bot.command("new", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/new".length).trim();
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`创建地址成功:\n`
|
||||
+ `地址: ${res.address}\n`
|
||||
+ (res.password ? `密码: \`${res.password}\`\n` : '')
|
||||
+ `凭证: \`${res.jwt}\`\n`,
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address, msgs);
|
||||
return await ctx.reply(`${msgs.TgCreateSuccessMsg}\n`
|
||||
+ `${msgs.TgAddressMsg} ${res.address}\n`
|
||||
+ (res.password ? `${msgs.TgPasswordMsg} \`${res.password}\`\n` : '')
|
||||
+ `${msgs.TgCredentialMsg} \`${res.jwt}\`\n`,
|
||||
{
|
||||
parse_mode: "Markdown"
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`创建地址失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgCreateFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("bind", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const jwt = ctx?.message?.text.slice("/bind".length).trim();
|
||||
if (!jwt) {
|
||||
return await ctx.reply("请输入凭证");
|
||||
return await ctx.reply(msgs.TgPleaseInputCredentialMsg);
|
||||
}
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt);
|
||||
return await ctx.reply(`绑定成功:\n`
|
||||
+ `地址: ${address}`
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt, msgs);
|
||||
return await ctx.reply(`${msgs.TgBindSuccessMsg}\n`
|
||||
+ `${msgs.TgAddressMsg} ${address}`
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return await ctx.reply(`绑定失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgBindFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("unbind", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/unbind".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
return await ctx.reply(msgs.TgPleaseInputAddressMsg);
|
||||
}
|
||||
await unbindTelegramAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`解绑成功:\n地址: ${address}`
|
||||
return await ctx.reply(`${msgs.TgUnbindSuccessMsg}\n${msgs.TgAddressMsg} ${address}`
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return await ctx.reply(`解绑失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgUnbindFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
})
|
||||
|
||||
bot.command("delete", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/delete".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
return await ctx.reply(msgs.TgPleaseInputAddressMsg);
|
||||
}
|
||||
await deleteTelegramAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`删除成功: ${address}`);
|
||||
await deleteTelegramAddress(c, userId.toString(), address, msgs);
|
||||
return await ctx.reply(`${msgs.TgDeleteSuccessMsg} ${address}`);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`删除失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgDeleteFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("address", async (ctx) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList } = await jwtListToAddressData(c, jwtList);
|
||||
return await ctx.reply(`地址列表:\n\n`
|
||||
+ addressList.map(a => `地址: ${a}`).join("\n")
|
||||
const { addressList } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
return await ctx.reply(`${msgs.TgAddressListMsg}\n\n`
|
||||
+ addressList.map(a => `${msgs.TgAddressMsg} ${a}`).join("\n")
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`获取地址列表失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgGetAddressFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("cleaninvalidaddress", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { invalidJwtList } = await jwtListToAddressData(c, jwtList);
|
||||
const { invalidJwtList } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
const newJwtList = jwtList.filter(jwt => !invalidJwtList.includes(jwt));
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify(newJwtList));
|
||||
const { addressList } = await jwtListToAddressData(c, newJwtList);
|
||||
return await ctx.reply(`清理无效地址成功:\n\n`
|
||||
+ `当前地址列表:\n\n`
|
||||
+ addressList.map(a => `地址: ${a}`).join("\n")
|
||||
const { addressList } = await jwtListToAddressData(c, newJwtList, msgs);
|
||||
return await ctx.reply(`${msgs.TgCleanSuccessMsg}\n\n`
|
||||
+ `${msgs.TgCurrentAddressListMsg}\n\n`
|
||||
+ addressList.map(a => `${msgs.TgAddressMsg} ${a}`).join("\n")
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`清理无效地址失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgCleanFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("lang", async (ctx: TgContext) => {
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
|
||||
// Check if user language config is enabled
|
||||
if (!getBooleanValue(c.env.TG_ALLOW_USER_LANG)) {
|
||||
return await ctx.reply(msgs.TgLangFeatureDisabledMsg);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const lang = ctx?.message?.text.slice("/lang".length).trim().toLowerCase();
|
||||
if (lang === 'zh' || lang === 'en') {
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:lang:${userId}`, lang);
|
||||
return await ctx.reply(`${msgs.TgLangSetSuccessMsg} ${lang === 'zh' ? '中文' : 'English'}`);
|
||||
}
|
||||
|
||||
const currentLang = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:lang:${userId}`);
|
||||
return await ctx.reply(
|
||||
`${msgs.TgCurrentLangMsg} ${currentLang || 'auto'}\n`
|
||||
+ `${msgs.TgSelectLangMsg}\n`
|
||||
+ `/lang zh - 中文\n`
|
||||
+ `/lang en - English`
|
||||
);
|
||||
});
|
||||
|
||||
const queryMail = async (ctx: TgContext, queryAddress: string, mailIndex: number, edit: boolean) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (!queryAddress && addressList.length > 0) {
|
||||
queryAddress = addressList[0];
|
||||
}
|
||||
if (!(queryAddress in addressIdMap)) {
|
||||
return await ctx.reply(`未绑定此地址 ${queryAddress}`);
|
||||
return await ctx.reply(`${msgs.TgNotBoundAddressMsg} ${queryAddress}`);
|
||||
}
|
||||
const address_id = addressIdMap[queryAddress];
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return await ctx.reply("无效地址");
|
||||
return await ctx.reply(msgs.TgInvalidAddressMsg);
|
||||
}
|
||||
const { raw, id: mailId, created_at } = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where address = ? `
|
||||
@@ -233,47 +305,49 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
).bind(
|
||||
queryAddress, mailIndex
|
||||
).first<{ raw: string, id: string, created_at: string }>() || {};
|
||||
const { mail } = raw ? await parseMail({ rawEmail: raw }, queryAddress, created_at) : { mail: "已经没有邮件了" };
|
||||
const { mail } = raw ? await parseMail(msgs, { rawEmail: raw }, queryAddress, created_at) : { mail: msgs.TgNoMoreMailsMsg };
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
|
||||
miniAppButtons.push(Markup.button.webApp(msgs.TgViewMailBtnMsg, url.toString()));
|
||||
}
|
||||
if (edit) {
|
||||
return await ctx.editMessageText(mail || "无邮件",
|
||||
return await ctx.editMessageText(mail || msgs.TgNoMailMsg,
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
Markup.button.callback(msgs.TgPrevBtnMsg, `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
Markup.button.callback(msgs.TgNextBtnMsg, `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
return await ctx.reply(mail || "无邮件",
|
||||
return await ctx.reply(mail || msgs.TgNoMailMsg,
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
Markup.button.callback(msgs.TgPrevBtnMsg, `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
Markup.button.callback(msgs.TgNextBtnMsg, `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bot.command("mails", async ctx => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
try {
|
||||
const queryAddress = ctx?.message?.text.slice("/mails".length).trim();
|
||||
return await queryMail(ctx, queryAddress, 0, false);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`获取邮件失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on(callbackQuery("data"), async ctx => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
// Use ctx.callbackQuery.data
|
||||
try {
|
||||
const data = ctx.callbackQuery.data;
|
||||
@@ -283,8 +357,8 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`获取邮件失败: ${(e as Error).message}`, e);
|
||||
return await ctx.answerCbQuery(`获取邮件失败: ${(e as Error).message}`);
|
||||
console.log(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`, e);
|
||||
return await ctx.answerCbQuery(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
@@ -293,11 +367,12 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
}
|
||||
|
||||
|
||||
export async function initTelegramBotCommands(bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(COMMANDS);
|
||||
export async function initTelegramBotCommands(c: Context<HonoCustomType>, bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(getTelegramCommands(c));
|
||||
}
|
||||
|
||||
const parseMail = async (
|
||||
msgs: LocaleMessages,
|
||||
parsedEmailContext: ParsedEmailContext,
|
||||
address: string, created_at: string | undefined | null
|
||||
) => {
|
||||
@@ -308,20 +383,20 @@ const parseMail = async (
|
||||
const parsedEmail = await commonParseMail(parsedEmailContext);
|
||||
let parsedText = parsedEmail?.text || "";
|
||||
if (parsedText.length && parsedText.length > 1000) {
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + "\n\n...\n消息过长请到miniapp查看";
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + `\n\n...\n${msgs.TgMsgTooLongMsg}`;
|
||||
}
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `From: ${parsedEmail?.sender || "无发件人"}\n`
|
||||
mail: `From: ${parsedEmail?.sender || msgs.TgNoSenderMsg}\n`
|
||||
+ `To: ${address}\n`
|
||||
+ (created_at ? `Date: ${created_at}\n` : "")
|
||||
+ `Subject: ${parsedEmail?.subject}\n`
|
||||
+ `Content:\n${parsedText || "解析失败,请打开 mini app 查看"}`
|
||||
+ `Content:\n${parsedText || msgs.TgParseFailedViewInAppMsg}`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `解析邮件失败: ${(e as Error).message}`
|
||||
mail: `${msgs.TgParseMailFailedMsg} ${(e as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -336,10 +411,6 @@ export async function sendMailToTelegram(
|
||||
return;
|
||||
}
|
||||
const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
|
||||
const { mail } = await parseMail(parsedEmailContext, address, new Date().toUTCString());
|
||||
if (!mail) {
|
||||
return;
|
||||
}
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const globalPush = settings?.enableGlobalMailPush && settings?.globalMailPushList;
|
||||
if (!userId && !globalPush) {
|
||||
@@ -349,28 +420,31 @@ export async function sendMailToTelegram(
|
||||
`SELECT id FROM raw_mails where address = ? and message_id = ?`
|
||||
).bind(address, message_id).first<string>("id");
|
||||
const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN);
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
|
||||
}
|
||||
|
||||
const buildAndSend = async (targetUserId: string, msgs: LocaleMessages) => {
|
||||
const { mail } = await parseMail(msgs, parsedEmailContext, address, new Date().toUTCString());
|
||||
if (!mail) return;
|
||||
const buttons = [];
|
||||
if (settings?.miniAppUrl && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
buttons.push(Markup.button.webApp(msgs.TgViewMailBtnMsg, url.toString()));
|
||||
}
|
||||
await bot.telegram.sendMessage(targetUserId, mail, {
|
||||
...Markup.inlineKeyboard([...buttons])
|
||||
});
|
||||
};
|
||||
|
||||
if (globalPush) {
|
||||
const globalMsgs = i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
for (const pushId of settings.globalMailPushList) {
|
||||
await bot.telegram.sendMessage(pushId, mail, {
|
||||
...Markup.inlineKeyboard([
|
||||
...miniAppButtons,
|
||||
])
|
||||
});
|
||||
await buildAndSend(pushId, globalMsgs);
|
||||
}
|
||||
}
|
||||
if (!userId) {
|
||||
return;
|
||||
|
||||
if (userId) {
|
||||
const userMsgs = await getTgMessages(c, undefined, userId);
|
||||
await buildAndSend(userId, userMsgs);
|
||||
}
|
||||
await bot.telegram.sendMessage(userId, mail, {
|
||||
...Markup.inlineKeyboard([
|
||||
...miniAppButtons,
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
1
worker/src/types.d.ts
vendored
1
worker/src/types.d.ts
vendored
@@ -84,6 +84,7 @@ type Bindings = {
|
||||
TELEGRAM_BOT_TOKEN: string
|
||||
TG_MAX_ADDRESS: number | undefined
|
||||
TG_BOT_INFO: string | object | undefined
|
||||
TG_ALLOW_USER_LANG: string | boolean | undefined
|
||||
|
||||
// webhook config
|
||||
FRONTEND_URL: string | undefined
|
||||
|
||||
@@ -32,22 +32,23 @@ const UserBindAddressModule = {
|
||||
c: Context<HonoCustomType>,
|
||||
user_id: number | string, address_id: number | string
|
||||
) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("No address or user token", 400)
|
||||
return c.text(msgs.NoAddressOrUserTokenMsg, 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// check if binded
|
||||
const db_user_address_id = await c.env.DB.prepare(
|
||||
@@ -66,7 +67,7 @@ const UserBindAddressModule = {
|
||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||
).bind(user_id).first<{ count: number }>() || { count: 0 };
|
||||
if (count >= maxAddressCount) {
|
||||
return c.text("Max address count reached", 400)
|
||||
return c.text(msgs.MaxAddressCountReachedMsg, 400)
|
||||
}
|
||||
}
|
||||
// bind
|
||||
@@ -75,36 +76,37 @@ const UserBindAddressModule = {
|
||||
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (error.message && error.message.includes("UNIQUE")) {
|
||||
return c.text("Address already binded, please unbind first", 400)
|
||||
return c.text(msgs.AddressAlreadyBindedMsg, 400)
|
||||
}
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
unbind: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id } = await c.req.json();
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// unbind
|
||||
try {
|
||||
@@ -112,10 +114,10 @@ const UserBindAddressModule = {
|
||||
`DELETE FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -167,18 +169,19 @@ const UserBindAddressModule = {
|
||||
return results || [];
|
||||
},
|
||||
getBindedAddressJwt: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address_id } = c.req.param();
|
||||
// check binded
|
||||
const { user_id } = c.get("userPayload");
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400)
|
||||
}
|
||||
// check users_address if address binded
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address WHERE address_id = ? and user_id = ?`
|
||||
).bind(address_id, user_id).first("user_id");
|
||||
if (!db_user_id) {
|
||||
return c.text("Address not binded", 400)
|
||||
return c.text(msgs.AddressNotBindedMsg, 400)
|
||||
}
|
||||
// generate jwt
|
||||
const name = await c.env.DB.prepare(
|
||||
@@ -193,6 +196,7 @@ const UserBindAddressModule = {
|
||||
})
|
||||
},
|
||||
transferAddress: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id, target_user_email } = await c.req.json();
|
||||
// check if address exists
|
||||
@@ -200,21 +204,21 @@ const UserBindAddressModule = {
|
||||
`SELECT name FROM address where id = ?`
|
||||
).bind(address_id).first<string>("name");
|
||||
if (!address) {
|
||||
return c.text("Address not found", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// check if target user exists
|
||||
const target_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where user_email = ?`
|
||||
).bind(target_user_email).first<number>("id");
|
||||
if (!target_user_id) {
|
||||
return c.text("Target user not found", 400)
|
||||
return c.text(msgs.TargetUserNotFoundMsg, 400)
|
||||
}
|
||||
// check target user binded address count
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
@@ -228,14 +232,14 @@ const UserBindAddressModule = {
|
||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||
).bind(target_user_id).first<{ count: number }>() || { count: 0 };
|
||||
if (count >= maxAddressCount) {
|
||||
return c.text("Target User Max address count reached", 400)
|
||||
return c.text(msgs.MaxAddressCountReachedMsg, 400)
|
||||
}
|
||||
}
|
||||
// check if binded
|
||||
const db_user_address_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).first("user_id");
|
||||
if (!db_user_address_id) return c.text("Address not binded", 400)
|
||||
if (!db_user_address_id) return c.text(msgs.AddressNotBindedMsg, 400)
|
||||
// unbind telegram address
|
||||
await unbindTelegramByAddress(c, address);
|
||||
// unbind user address
|
||||
@@ -244,10 +248,10 @@ const UserBindAddressModule = {
|
||||
`DELETE FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text("Failed to unbind user", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
// delete address
|
||||
await c.env.DB.prepare(
|
||||
@@ -258,7 +262,7 @@ const UserBindAddressModule = {
|
||||
`INSERT INTO address(name) VALUES(?)`
|
||||
).bind(address).run();
|
||||
if (!newAddressSuccess) {
|
||||
throw new Error("Failed to create address")
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
await updateAddressUpdatedAt(c, address);
|
||||
// find new address id
|
||||
@@ -266,7 +270,7 @@ const UserBindAddressModule = {
|
||||
`SELECT id FROM address WHERE name = ?`
|
||||
).bind(address).first<number | null | undefined>("id");
|
||||
if (!new_address_id) {
|
||||
throw new Error("Failed to find new address id")
|
||||
throw new Error(msgs.OperationFailedMsg)
|
||||
}
|
||||
// bind
|
||||
try {
|
||||
@@ -274,14 +278,14 @@ const UserBindAddressModule = {
|
||||
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
|
||||
).bind(target_user_id, new_address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (error.message && error.message.includes("UNIQUE")) {
|
||||
return c.text("Address already binded, please unbind first", 400)
|
||||
return c.text(msgs.AddressAlreadyBindedMsg, 400)
|
||||
}
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ export default {
|
||||
getOauth2LoginUrl: async (c: Context<HonoCustomType>) => {
|
||||
const settings = await getJsonSetting<UserOauth2Settings[]>(c, CONSTANTS.OAUTH2_SETTINGS_KEY);
|
||||
const { clientID, state } = c.req.query();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const setting = settings?.find(s => s.clientID === clientID);
|
||||
if (!setting) {
|
||||
return c.text(msgs.Oauth2ClientIDNotFoundMsg, 400);
|
||||
@@ -22,8 +21,7 @@ export default {
|
||||
},
|
||||
oauth2Login: async (c: Context<HonoCustomType>) => {
|
||||
const { clientID, code } = await c.req.json<{ clientID?: string, code?: string }>();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!clientID || !code) {
|
||||
return c.text(msgs.Oauth2CliendIDOrCodeMissingMsg, 400);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Passkey } from '../models';
|
||||
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
|
||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export default {
|
||||
getPassKeys: async (c: Context<HonoCustomType>) => {
|
||||
@@ -20,10 +21,11 @@ export default {
|
||||
return c.json(results);
|
||||
},
|
||||
renamePassKey: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const user = c.get("userPayload");
|
||||
const { passkey_id, passkey_name } = await c.req.json();
|
||||
if (!passkey_name || passkey_name.length > 255) {
|
||||
return c.text("Invalid passkey name", 400);
|
||||
return c.text(msgs.InvalidPasskeyNameMsg, 400);
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE user_passkeys SET passkey_name = ? WHERE user_id = ? AND passkey_id = ?`
|
||||
@@ -71,6 +73,7 @@ export default {
|
||||
return c.json(options);
|
||||
},
|
||||
registerResponse: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const user = c.get("userPayload");
|
||||
const { credential, origin, passkey_name } = await c.req.json();
|
||||
// Verify the registration response
|
||||
@@ -90,7 +93,7 @@ export default {
|
||||
const { verified, registrationInfo } = verification;
|
||||
|
||||
if (!verified || !registrationInfo) {
|
||||
return c.text("Registration failed", 400);
|
||||
return c.text(msgs.RegistrationFailedMsg, 400);
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -131,10 +134,11 @@ export default {
|
||||
return c.json(options);
|
||||
},
|
||||
authenticateResponse: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { domain, credential, origin } = await c.req.json();
|
||||
const passkey_id = credential?.id;
|
||||
if (!passkey_id) {
|
||||
return c.text("Invalid request", 400);
|
||||
return c.text(msgs.InvalidInputMsg, 400);
|
||||
}
|
||||
const { user_id, counter, passkey } = await c.env.DB.prepare(
|
||||
`SELECT user_id, counter, passkey FROM user_passkeys WHERE passkey_id = ?`
|
||||
@@ -142,7 +146,7 @@ export default {
|
||||
counter: number; passkey: string; user_id: number;
|
||||
}>() || {};
|
||||
if (!passkey) {
|
||||
return c.text("Passkey not found", 404);
|
||||
return c.text(msgs.PasskeyNotFoundMsg, 404);
|
||||
}
|
||||
const passkeyData = JSON.parse(passkey) as Passkey;
|
||||
// Verify the registration response
|
||||
@@ -166,7 +170,7 @@ export default {
|
||||
});
|
||||
const { verified, authenticationInfo } = verification;
|
||||
if (!verified) {
|
||||
return c.text("Authentication failed", 400);
|
||||
return c.text(msgs.AuthenticationFailedMsg, 400);
|
||||
}
|
||||
|
||||
if (authenticationInfo) {
|
||||
@@ -186,7 +190,7 @@ export default {
|
||||
`SELECT user_email FROM users WHERE id = ?`
|
||||
).bind(user_id).first<{ user_email: string }>() || {};
|
||||
if (!user_email) {
|
||||
return c.text("User not found", 404);
|
||||
return c.text(msgs.UserNotFoundMsg, 404);
|
||||
}
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
|
||||
@@ -31,8 +31,7 @@ export default {
|
||||
},
|
||||
settings: async (c: Context<HonoCustomType>) => {
|
||||
const user = c.get("userPayload");
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
|
||||
@@ -10,8 +10,7 @@ import { sendMail } from "../mails_api/send_mail_api";
|
||||
export default {
|
||||
verifyCode: async (c: Context<HonoCustomType>) => {
|
||||
const { email, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
@@ -61,8 +60,7 @@ export default {
|
||||
register: async (c: Context<HonoCustomType>) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check enable
|
||||
if (!settings.enable) {
|
||||
return c.text(msgs.UserRegistrationDisabledMsg, 403);
|
||||
@@ -154,8 +152,7 @@ export default {
|
||||
},
|
||||
login: async (c: Context<HonoCustomType>) => {
|
||||
const { email, password } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!email || !password) return c.text(msgs.InvalidEmailOrPasswordMsg, 400);
|
||||
const { id: user_id, password: dbPassword } = await c.env.DB.prepare(
|
||||
`SELECT id, password FROM users where user_email = ?`
|
||||
|
||||
Reference in New Issue
Block a user