Files
cloudflare_temp_email/worker/src/user_api/bind_address.ts
T
Dream Hunter 5dbb6107dd 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
2026-08-25 14:14:42 +08:00

276 lines
10 KiB
TypeScript

import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
import { isAddressCountLimitReached } from "../utils"
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");
const { address_id } = c.get("jwtPayload");
return await UserBindAddressModule.bindByID(c, user_id, address_id)
},
bindByID: async (
c: Context<HonoCustomType>,
user_id: number | string, address_id: number | string
) => {
const msgs = i18n.getMessagesbyContext(c);
if (!address_id || !user_id) {
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(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(msgs.UserNotFoundMsg, 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.json({ success: true })
// check if binded address count
const userRole = c.get("userRolePayload");
if (await isAddressCountLimitReached(c, user_id, userRole)) {
return c.text(msgs.MaxAddressCountReachedMsg, 400)
}
// bind
try {
const { success } = await c.env.DB.prepare(
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
).bind(user_id, address_id).run();
if (!success) {
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
const error = e as Error;
if (error.message && error.message.includes("UNIQUE")) {
return c.text(msgs.AddressAlreadyBindedMsg, 400)
}
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(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(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(msgs.UserNotFoundMsg, 400)
}
// unbind
try {
const { success } = await c.env.DB.prepare(
`DELETE FROM users_address where user_id = ? and address_id = ?`
).bind(user_id, address_id).run();
if (!success) {
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({ success: true })
},
getBindedAddresses: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload");
const { limit, offset } = c.req.query();
const params = [String(user_id)];
const fromQuery = ` FROM address a`
+ ` JOIN users_address ua ON ua.address_id = a.id`
+ ` WHERE ua.user_id = ?`;
return await handleListQuery(
c,
`SELECT a.*,`
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
+ fromQuery,
`SELECT COUNT(*) AS count${fromQuery}`,
params,
limit ?? 20,
offset ?? 0,
'a.id DESC',
['password'],
);
},
getBindedAddressesById: async (
c: Context<HonoCustomType>, user_id: number | string
): Promise<{
id: number;
name: string;
mail_count: number;
send_count: number;
created_at: string;
updated_at: string;
}[]> => {
const msgs = i18n.getMessagesbyContext(c);
if (!user_id) {
throw new Error(msgs.UserNotFoundMsg);
}
// select binded address
const { results } = await c.env.DB.prepare(
`SELECT a.*,`
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
+ ` FROM address a `
+ ` JOIN users_address ua `
+ ` ON ua.address_id = a.id `
+ ` WHERE ua.user_id = ?`
+ ` ORDER BY a.id DESC`
).bind(user_id).all<{
id: number;
name: string;
mail_count: number;
send_count: number;
created_at: string;
updated_at: string;
}>();
return (results || []).map((row) => hideObjectFields(row, ['password']));
},
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(msgs.InvalidAddressOrUserTokenMsg, 400)
}
const name = await getBindedAddressById(c, user_id, address_id);
if (!name) {
return c.text(msgs.AddressNotBindedMsg, 400)
}
const jwt = await Jwt.sign({
address: name,
address_id: address_id
}, c.env.JWT_SECRET, "HS256")
return c.json({
jwt: jwt
})
},
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
const address = await c.env.DB.prepare(
`SELECT name FROM address where id = ?`
).bind(address_id).first<string>("name");
if (!address) {
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(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(msgs.TargetUserNotFoundMsg, 400)
}
// check target user binded address count
const userRoleObj = await commonGetUserRole(c, target_user_id);
if (await isAddressCountLimitReached(c, target_user_id, userRoleObj?.role)) {
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(msgs.AddressNotBindedMsg, 400)
// unbind telegram address
await unbindTelegramByAddress(c, address);
// unbind user address
try {
const { success } = await c.env.DB.prepare(
`DELETE FROM users_address where user_id = ? and address_id = ?`
).bind(user_id, address_id).run();
if (!success) {
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
return c.text(msgs.OperationFailedMsg, 500)
}
// delete address
await c.env.DB.prepare(
`DELETE FROM address WHERE id = ? `
).bind(address_id).run();
// new address
const { success: newAddressSuccess } = await c.env.DB.prepare(
`INSERT INTO address(name) VALUES(?)`
).bind(address).run();
if (!newAddressSuccess) {
throw new Error(msgs.FailedCreateAddressMsg)
}
await updateAddressUpdatedAt(c, address);
// find new address id
const new_address_id = await c.env.DB.prepare(
`SELECT id FROM address WHERE name = ?`
).bind(address).first<number | null | undefined>("id");
if (!new_address_id) {
throw new Error(msgs.OperationFailedMsg)
}
// bind
try {
const { success } = await c.env.DB.prepare(
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
).bind(target_user_id, new_address_id).run();
if (!success) {
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
const error = e as Error;
if (error.message && error.message.includes("UNIQUE")) {
return c.text(msgs.AddressAlreadyBindedMsg, 400)
}
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({ success: true })
}
}
export default UserBindAddressModule;