mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 07:27:27 +08:00
feat: add user send mail and sent box (#1122)
* feat: add user send mail client * fix: align user mail navigation * fix: shorten address credential action * test: cover user mail ownership boundaries * fix: address user mail review feedback * fix: disambiguate user mail e2e heading * fix: minimize shared sent box changes * refactor: isolate user send mail page * refactor: reuse bound address lookup * fix: clarify user sent box naming * refactor: decouple user send API from roles * fix: align user send mail behavior * fix: align user send role and rate limits * test: isolate user send rate limits * test: initialize rate limit worker database * refactor: simplify user send rate limit * refactor: inline user send rate limit path * refactor: simplify user send limiter key * refactor: keep existing rate limit behavior * style: simplify user send rate limit condition * style: group user send rate limit condition * fix: bind user role token to account * fix: keep user sender selection available
This commit is contained in:
@@ -6,6 +6,22 @@ import { unbindTelegramByAddress } from '../telegram_api/common';
|
||||
import i18n from '../i18n';
|
||||
import { updateAddressUpdatedAt, commonGetUserRole, handleListQuery, hideObjectFields } from '../common';
|
||||
|
||||
export const getBindedAddressById = async (
|
||||
c: Context<HonoCustomType>,
|
||||
user_id: number | string,
|
||||
address_id: number | string
|
||||
): Promise<string | null> => {
|
||||
if (!user_id || !address_id) {
|
||||
return null;
|
||||
}
|
||||
const address = await c.env.DB.prepare(
|
||||
`SELECT a.name FROM users_address ua`
|
||||
+ ` JOIN address a ON a.id = ua.address_id`
|
||||
+ ` WHERE ua.user_id = ? AND ua.address_id = ?`
|
||||
).bind(user_id, address_id).first<string>('name');
|
||||
return address ?? null;
|
||||
}
|
||||
|
||||
const UserBindAddressModule = {
|
||||
bind: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
@@ -158,17 +174,10 @@ const UserBindAddressModule = {
|
||||
if (!address_id || !user_id) {
|
||||
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) {
|
||||
const name = await getBindedAddressById(c, user_id, address_id);
|
||||
if (!name) {
|
||||
return c.text(msgs.AddressNotBindedMsg, 400)
|
||||
}
|
||||
// generate jwt
|
||||
const name = await c.env.DB.prepare(
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(address_id).first("name");
|
||||
const jwt = await Jwt.sign({
|
||||
address: name,
|
||||
address_id: address_id
|
||||
|
||||
@@ -6,6 +6,7 @@ import bind_address from './bind_address';
|
||||
import passkey from './passkey';
|
||||
import oauth2 from './oauth2';
|
||||
import user_mail_api from './user_mail_api';
|
||||
import user_send_mail_api from './user_send_mail_api';
|
||||
|
||||
export const api = new Hono<HonoCustomType>();
|
||||
|
||||
@@ -17,6 +18,13 @@ api.get('/user_api/settings', settings.settings);
|
||||
api.get('/user_api/mails', user_mail_api.getMails);
|
||||
api.delete('/user_api/mails/:id', user_mail_api.deleteMail);
|
||||
|
||||
// send mail api
|
||||
api.get('/user_api/address/:address_id/settings', user_send_mail_api.settings);
|
||||
api.post('/user_api/address/:address_id/request_send_mail_access', user_send_mail_api.requestAccess);
|
||||
api.post('/user_api/address/:address_id/send_mail', user_send_mail_api.send);
|
||||
api.get('/user_api/sendbox', user_send_mail_api.listUserSendbox);
|
||||
api.delete('/user_api/sendbox/:mail_id', user_send_mail_api.removeUserSendboxMail);
|
||||
|
||||
// user api
|
||||
api.post('/user_api/login', user.login);
|
||||
api.post('/user_api/verify_code', user.verifyCode);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import { handleListQuery } from "../common";
|
||||
import i18n from "../i18n";
|
||||
import { sendMail } from "../mails_api/send_mail_api";
|
||||
import {
|
||||
getSendBalanceState,
|
||||
requestSendMailAccess,
|
||||
} from "../mails_api/send_balance";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { getBindedAddressById } from "./bind_address";
|
||||
|
||||
const getAddressOrError = async (
|
||||
c: Context<HonoCustomType>
|
||||
): Promise<string | Response> => {
|
||||
const addressId = Number(c.req.param("address_id"));
|
||||
if (!Number.isInteger(addressId) || addressId <= 0) {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
return c.text(msgs.AddressNotBindedMsg, 400);
|
||||
}
|
||||
const { user_id } = c.get("userPayload");
|
||||
const address = await getBindedAddressById(c, user_id, addressId);
|
||||
if (address) {
|
||||
return address;
|
||||
}
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
return c.text(msgs.AddressNotBindedMsg, 400);
|
||||
}
|
||||
|
||||
const settings = async (c: Context<HonoCustomType>): Promise<Response> => {
|
||||
const address = await getAddressOrError(c);
|
||||
if (address instanceof Response) {
|
||||
return address;
|
||||
}
|
||||
const { balance } = await getSendBalanceState(c, address);
|
||||
return c.json({
|
||||
address,
|
||||
send_balance: balance || 0,
|
||||
});
|
||||
}
|
||||
|
||||
const requestAccess = async (c: Context<HonoCustomType>): Promise<Response> => {
|
||||
const address = await getAddressOrError(c);
|
||||
if (address instanceof Response) {
|
||||
return address;
|
||||
}
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const result = await requestSendMailAccess(c, address);
|
||||
if (result.status === "ok") {
|
||||
return c.json({ status: "ok" });
|
||||
}
|
||||
if (result.status === "already_requested") {
|
||||
return c.text(msgs.AlreadyRequestedMsg, 400);
|
||||
}
|
||||
return c.text(msgs.OperationFailedMsg, 500);
|
||||
}
|
||||
|
||||
const send = async (c: Context<HonoCustomType>): Promise<Response> => {
|
||||
const address = await getAddressOrError(c);
|
||||
if (address instanceof Response) {
|
||||
return address;
|
||||
}
|
||||
try {
|
||||
const reqJson = await c.req.json();
|
||||
await sendMail(c, address, reqJson);
|
||||
} catch (error) {
|
||||
console.error("Failed to send user mail", error);
|
||||
return c.text(`Failed to send mail ${(error as Error).message}`, 400);
|
||||
}
|
||||
return c.json({ status: "ok" });
|
||||
}
|
||||
|
||||
const listUserSendbox = async (c: Context<HonoCustomType>): Promise<Response> => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address, limit, offset } = c.req.query();
|
||||
const filters = ["ua.user_id = ?"];
|
||||
const params = [String(user_id)];
|
||||
if (address) {
|
||||
filters.push("sb.address = ?");
|
||||
params.push(address);
|
||||
}
|
||||
const fromQuery = ` FROM users_address ua`
|
||||
+ ` JOIN address a ON a.id = ua.address_id`
|
||||
+ ` JOIN sendbox sb ON sb.address = a.name`
|
||||
+ ` WHERE ${filters.join(" AND ")}`;
|
||||
return await handleListQuery(c,
|
||||
`SELECT sb.*${fromQuery}`,
|
||||
`SELECT count(*) as count${fromQuery}`,
|
||||
params, limit, offset, "sb.id desc"
|
||||
);
|
||||
}
|
||||
|
||||
const removeUserSendboxMail = async (c: Context<HonoCustomType>): Promise<Response> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403);
|
||||
}
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { mail_id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE id = ?`
|
||||
+ ` AND EXISTS (`
|
||||
+ `SELECT 1 FROM users_address ua`
|
||||
+ ` JOIN address a ON a.id = ua.address_id`
|
||||
+ ` WHERE ua.user_id = ? AND a.name = sendbox.address`
|
||||
+ `)`
|
||||
).bind(mail_id, user_id).run();
|
||||
return c.json({ success });
|
||||
}
|
||||
|
||||
export default {
|
||||
settings,
|
||||
requestAccess,
|
||||
send,
|
||||
listUserSendbox,
|
||||
removeUserSendboxMail,
|
||||
};
|
||||
+10
-3
@@ -65,6 +65,7 @@ app.use('/*', async (c, next) => {
|
||||
c.req.path.startsWith("/api/new_address")
|
||||
|| c.req.path.startsWith("/api/send_mail")
|
||||
|| c.req.path.startsWith("/external/api/send_mail")
|
||||
|| (c.req.path.startsWith("/user_api/address/") && c.req.path.endsWith("/send_mail"))
|
||||
|| c.req.path.startsWith("/user_api/register")
|
||||
|| c.req.path.startsWith("/user_api/verify_code")
|
||||
) {
|
||||
@@ -125,7 +126,8 @@ const checkUserPayload = async (
|
||||
}
|
||||
|
||||
const checkoutUserRolePayload = async (
|
||||
c: Context<HonoCustomType>
|
||||
c: Context<HonoCustomType>,
|
||||
userId?: number
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const token = c.req.raw.headers.get("x-user-access-token");
|
||||
@@ -138,6 +140,7 @@ const checkoutUserRolePayload = async (
|
||||
return;
|
||||
}
|
||||
if (typeof payload?.user_role !== "string") return;
|
||||
if (userId !== undefined && payload.user_id !== userId) return;
|
||||
c.set("userRolePayload", payload.user_role);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -202,8 +205,12 @@ app.use('/user_api/*', async (c, next) => {
|
||||
console.error(e);
|
||||
return c.text(msgs.UserTokenExpiredMsg, 401)
|
||||
}
|
||||
if (c.req.path.startsWith("/user_api/bind_address")) {
|
||||
await checkoutUserRolePayload(c);
|
||||
if (
|
||||
c.req.path.startsWith("/user_api/bind_address")
|
||||
|| c.req.path.startsWith("/user_api/address/")
|
||||
) {
|
||||
const { user_id } = c.get("userPayload");
|
||||
await checkoutUserRolePayload(c, user_id);
|
||||
}
|
||||
if (c.req.path.startsWith('/user_api/bind_address')
|
||||
&& c.req.method === 'POST'
|
||||
|
||||
Reference in New Issue
Block a user