diff --git a/frontend/src/components/modal.tsx b/frontend/src/components/modal.tsx
index b204e6c..6a5d1d9 100644
--- a/frontend/src/components/modal.tsx
+++ b/frontend/src/components/modal.tsx
@@ -1,63 +1,76 @@
-import { Link } from "react-router-dom";
-import Twitter from "./icons/Twitter.tsx";
-import GitHub from "./icons/GitHub.tsx";
-import MailIcon from "./icons/MailIcon.tsx";
-import WrdoLogo from "./icons/Wrdo.tsx";
+import { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react';
+// fix: 将具名导入 { useWindowSize } 更改为默认导入 useWindowSize
+import useWindowSize from '../hooks/use-window-size';
+import Leaflet from './leaflet';
-/**
- * 网站的页脚组件
- */
-export function Footer() {
- return (
-
-
- VMAIL.DEV
- {" "}
-
- © 2024 Products of{" "}
-
- oiov
-
- .
-
-
-
+// 将默认导出(export default)改为具名导出(export)
+export function Modal({
+ children,
+ showModal,
+ setShowModal,
+}: {
+ children: React.ReactNode;
+ showModal: boolean;
+ setShowModal: Dispatch>;
+}) {
+ const modalRef = useRef(null);
+ const { isMobile } = useWindowSize();
+
+ // close modal on click outside
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
+ setShowModal(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [modalRef, setShowModal]);
+
+
+ const onKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ setShowModal(false);
+ }
+ },
+ [setShowModal],
);
-}
+
+ useEffect(() => {
+ document.addEventListener("keydown", onKeyDown);
+ return () => document.removeEventListener("keydown", onKeyDown);
+ }, [onKeyDown]);
+
+ if (isMobile) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+ <>
+ {showModal && (
+
+ )}
+ >
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/password.tsx b/frontend/src/components/password.tsx
index d7172e3..1e7eb07 100644
--- a/frontend/src/components/password.tsx
+++ b/frontend/src/components/password.tsx
@@ -2,38 +2,42 @@ import {
Dispatch,
SetStateAction,
useCallback,
- useEffect,
useMemo,
useState,
} from "react";
-import Modal from "./modal";
-import { Form, useNavigation } from "@remix-run/react";
+// fix: 使用具名导入来修复构建错误
+import { Modal } from "./modal";
import { useTranslation } from "react-i18next";
import Close from "./icons/Close";
import toast from "react-hot-toast";
-export default function PasswordModal({
- password,
- showPasswordModal,
- setShowPasswordModal,
-}: {
- password: string;
+// 定义组件的 props 类型
+interface PasswordModalProps {
showPasswordModal: boolean;
setShowPasswordModal: Dispatch>;
-}) {
- const navigation = useNavigation();
+ onLogin: (password: string) => Promise; // feat: 添加 onLogin 回调处理登录逻辑
+ isLoggingIn: boolean; // feat: 添加状态以在 UI 中反映登录过程
+}
+
+export default function PasswordModal({
+ showPasswordModal,
+ setShowPasswordModal,
+ onLogin,
+ isLoggingIn,
+}: PasswordModalProps) {
const { t } = useTranslation();
+ const [password, setPassword] = useState(""); // 状态:存储输入的密码
- const [isSubmitted, setIsSubmitted] = useState(false);
-
- useEffect(() => {
- if (showPasswordModal && navigation.state === "submitting") {
- setIsSubmitted(true);
+ // 提交处理器
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!password) {
+ toast.error(t("Password is required"));
+ return;
}
- if (showPasswordModal && navigation.state === "idle" && isSubmitted) {
- setShowPasswordModal(false);
- }
- }, [navigation.state, showPasswordModal]);
+ await onLogin(password);
+ // 登录成功或失败的 toast 提示已在 Home.tsx 中处理
+ };
return (
@@ -51,9 +55,10 @@ export default function PasswordModal({
{t("Save your password and continue using this email in 1 day")}
-
+
+
);
}
-export function usePasswordModal(password: string) {
+// 自定义 Hook,用于管理密码模态框的可见性
+export function usePasswordModal() {
const [showPasswordModal, setShowPasswordModal] = useState(false);
- const SenderModalCallback = useCallback(() => {
- return (
-
- );
- }, [showPasswordModal, setShowPasswordModal]);
+ // 使用 useCallback 优化 PasswordModal 组件的创建
+ const PasswordModalCallback = useCallback(
+ ({ onLogin, isLoggingIn }: Omit) => {
+ return (
+
+ );
+ },
+ [showPasswordModal] // 依赖项
+ );
return useMemo(
- () => ({ setShowPasswordModal, PasswordModal: SenderModalCallback }),
- [setShowPasswordModal, SenderModalCallback]
+ () => ({
+ showPasswordModal,
+ setShowPasswordModal,
+ PasswordModal: PasswordModalCallback
+ }),
+ [setShowPasswordModal, PasswordModalCallback, showPasswordModal]
);
}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 1bb9d1a..a1cc08a 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -8,10 +8,15 @@ import { Toaster, toast } from 'react-hot-toast';
import { MailList } from '../components/MailList.tsx';
import { CopyButton } from '../components/CopyButton.tsx';
-import { getEmails, deleteEmails } from '../services/api.ts';
+// feat: 导入 loginByPassword
+import { getEmails, deleteEmails, loginByPassword } from '../services/api.ts';
import { useConfig } from '../hooks/useConfig.ts';
import { getRandomCharacter } from '../lib/utlis.ts';
+// feat: 导入密码模态框和相关 hook
+import { usePasswordModal } from '../components/password.tsx';
+import PasswordIcon from '../components/icons/Password.tsx'; // 假设你有一个密码图标
+
// 图标导入
import ShieldCheck from "../components/icons/ShieldCheck.tsx";
import Cloudflare from "../components/icons/Cloudflare.tsx";
@@ -32,6 +37,10 @@ export function Home() {
const [turnstileToken, setTurnstileToken] = useState('');
const [selectedIds, setSelectedIds] = useState([]);
+ // feat: 初始化密码模态框
+ const { PasswordModal, setShowPasswordModal } = usePasswordModal();
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
+
// 使用 React Query 获取邮件列表
const { data: emails = [], isLoading, isFetching, refetch } = useQuery({
queryKey: ['emails', address],
@@ -42,7 +51,7 @@ export function Home() {
refetchInterval: 20000,
// 当查询出错时,自动刷新会停止,这里添加错误提示方便调试
onError: (err: Error) => {
- toast.error(`获取邮件失败: ${err.message}`, { duration: 5000 });
+ toast.error(`${t("Failed to get emails")}: ${err.message}`, { duration: 5000 });
},
// 失败后不自动重试
retry: false,
@@ -71,28 +80,51 @@ export function Home() {
const deleteMutation = useMutation({
mutationFn: (ids: string[]) => deleteEmails(ids, turnstileToken),
onSuccess: () => {
- toast.success('邮件已删除');
+ toast.success(t('Emails deleted'));
setSelectedIds([]); // 清空选择
// 使邮件列表的查询失效,触发一次刷新
queryClient.invalidateQueries({ queryKey: ['emails', address] });
},
onError: () => {
- toast.error('删除失败');
+ toast.error(t('Deletion failed'));
}
});
// 定义 handleDeleteEmails 函数
const handleDeleteEmails = (ids: string[]) => {
if (ids.length === 0) {
- toast.error('请选择要删除的邮件');
+ toast.error(t('Please select emails to delete'));
return;
}
deleteMutation.mutate(ids);
};
+ // feat: 处理密码登录的函数
+ const handleLogin = async (password: string) => {
+ if (!turnstileToken) {
+ toast.error(t('No captcha response'));
+ return;
+ }
+ setIsLoggingIn(true);
+ try {
+ const data = await loginByPassword(password, turnstileToken);
+ Cookies.set('userMailbox', data.address, { expires: 1 });
+ setAddress(data.address);
+ setShowPasswordModal(false); // 关闭模态框
+ toast.success(t("Login successful"));
+ } catch (error: any) {
+ toast.error(`${t("Login failed")}: ${error.message}`);
+ } finally {
+ setIsLoggingIn(false);
+ }
+ };
+
+
return (
+ {/* feat: 渲染密码模态框 */}
+
{/* 左侧信息面板 */}
@@ -135,6 +167,13 @@ export function Home() {
className="py-2.5 rounded-md w-full bg-cyan-600 hover:opacity-90 disabled:cursor-not-allowed disabled:bg-zinc-500">
{t("Create temporary email")}
+ {/* feat: 添加登录按钮 */}
+
setShowPasswordModal(true)}>
+
+ {t("Have a password? Login.")}
+
)}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index 78ae17d..099d7b4 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -1,28 +1,59 @@
-import axios from 'axios';
-import { Email } from 'database/schema'; // 假设您可以从数据库包中导入类型
+// 导入 Email 类型,'database' 是您 monorepo 中的包名
+import type { Email } from 'database';
+
+const API_BASE_URL = '/api';
// 定义 Turnstile 人机验证所需的 token
+// 恢复此接口定义,以备将来使用
interface ApiPayload {
token: string;
[key: string]: any;
}
// 获取邮件列表
-export const getEmails = async (address: string, token: string): Promise
=> {
- const response = await axios.post('/api/emails', { address, token });
- return response.data;
-};
+export async function getEmails(address: string, token: string): Promise {
+ const response = await fetch(`${API_BASE_URL}/emails`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ address, token }),
+ });
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.json();
+}
-// 获取单个邮件详情
-export const getEmailById = async (id: string): Promise => {
- const response = await axios.get(`/api/emails/${id}`);
- return response.data;
-};
+// feat: 添加获取单封邮件详情的函数
+export async function getEmailById(id: string): Promise {
+ const response = await fetch(`${API_BASE_URL}/emails/${id}`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch email');
+ }
+ return response.json();
+}
// 删除邮件
-export const deleteEmails = async (ids: string[], token: string): Promise => {
- const response = await axios.post('/api/delete-emails', { ids, token });
- return response.data;
-};
+export async function deleteEmails(ids: string[], token: string): Promise {
+ const response = await fetch(`${API_BASE_URL}/delete-emails`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ids, token }),
+ });
+ if (!response.ok) {
+ throw new Error('Failed to delete emails');
+ }
+}
-// ... 您可以在此添加更多 API 调用函数
\ No newline at end of file
+// feat: 添加密码登录函数
+export async function loginByPassword(password: string, token: string): Promise<{ address: string }> {
+ const response = await fetch(`${API_BASE_URL}/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ password, token }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Login failed');
+ }
+ return response.json();
+}
\ No newline at end of file
diff --git a/packages/database/dao.ts b/packages/database/dao.ts
index 7da129b..25fdb8c 100644
--- a/packages/database/dao.ts
+++ b/packages/database/dao.ts
@@ -1,4 +1,4 @@
-import { count, desc, eq, and, inArray } from "drizzle-orm";
+import { count, desc, eq, and, inArray, lt } from "drizzle-orm";
import { LibSQLDatabase } from "drizzle-orm/libsql";
import { emails, InsertEmail } from "./schema";
@@ -86,4 +86,24 @@ export async function deleteEmails(db: LibSQLDatabase, ids: string[]) {
console.error(e);
return { count: 0 };
}
+}
+
+/**
+ * 新增函数:根据提供的过期时间删除此时间之前的所有邮件。
+ * @param db Drizzle 数据库实例。
+ * @param expirationTime 一个 Date 对象,表示过期时间点。
+ * @returns 返回一个包含已删除邮件数量的对象,或在出错时返回 { count: 0 }。
+ */
+export async function deleteExpiredEmails(db: LibSQLDatabase, expirationTime: Date) {
+ try {
+ // 使用Drizzle的lt(小于)操作符来比较createdAt字段和expirationTime
+ const result = await db.delete(emails).where(lt(emails.createdAt, expirationTime)).execute();
+ // 返回受影响的行数,即已删除的邮件数量
+ return { count: result.rowsAffected };
+ } catch (e) {
+ // 如果在删除过程中发生错误,则在控制台打印错误信息
+ console.error('清理过期邮件失败:', e);
+ // 并返回一个表示删除数量为0的对象
+ return { count: 0 };
+ }
}
\ No newline at end of file
diff --git a/worker/src/index.ts b/worker/src/index.ts
index a1e4394..364bdce 100644
--- a/worker/src/index.ts
+++ b/worker/src/index.ts
@@ -2,7 +2,8 @@ import { Hono } from 'hono';
import { serveStatic } from 'hono/cloudflare-workers';
import { cors } from 'hono/cors';
// 路径修正:使用相对路径并导入正确的函数
-import { deleteEmails, findEmailById, getEmailsByMessageTo, insertEmail } from '../../packages/database/dao';
+// feat: 导入 getEmailByPassword 函数
+import { deleteEmails, findEmailById, getEmailByPassword, getEmailsByMessageTo, insertEmail, deleteExpiredEmails } from '../../packages/database/dao';
import { getD1DB } from '../../packages/database/db';
import { InsertEmail, insertEmailSchema } from '../../packages/database/schema';
import { nanoid } from 'nanoid/non-secure';
@@ -26,9 +27,26 @@ const app = new Hono<{ Bindings: Env }>();
// 配置 CORS
app.use('/api/*', cors());
+// fix: 修复重复读取请求体的问题,并增强健壮性
// Turnstile (人机验证) 中间件
const turnstile = async (c, next) => {
- const body = await c.req.json().catch(() => ({}));
+ let body: any = {}; // 默认值为空对象
+ try {
+ // 尝试读取请求体文本。如果请求体为空,.text() 会返回空字符串,不会像 .json() 那样报错。
+ const rawBody = await c.req.text();
+ if (rawBody) {
+ // 如果请求体不为空,则解析为 JSON
+ body = JSON.parse(rawBody);
+ }
+ } catch (e) {
+ // 如果 JSON.parse 失败,说明请求体不是有效的 JSON
+ // 这种情况我们依然继续,因为 token 可能在 header 中。body 维持为空对象。
+ console.error("无法将请求体解析为 JSON:", e);
+ }
+
+ // 将解析后的 body (或空对象) 存入上下文,供后续的处理器使用
+ c.set('parsedBody', body);
+
const token = body.token || c.req.header('cf-turnstile-token');
const ip = c.req.header('CF-Connecting-IP');
@@ -48,6 +66,8 @@ const turnstile = async (c, next) => {
const data = await res.json();
if (!data.success) {
+ // feat: 增加详细的错误日志,方便调试
+ console.error("Turnstile 验证失败:", data['error-codes']);
return c.json({ message: 'token is invalid' }, 400);
}
@@ -60,7 +80,11 @@ const api = app.basePath('/api');
// 获取邮件列表
api.post('/emails', turnstile, async (c) => {
const db = getD1DB(c.env.DB);
- const { address } = await c.req.json();
+ // fix: 从上下文中获取已解析的请求体,并增加健壮性处理
+ const { address } = c.get('parsedBody') || {};
+ if (!address) {
+ return c.json({ message: 'address is required' }, 400);
+ }
// 函数调用修正:使用 getEmailsByMessageTo 函数
const emails = await getEmailsByMessageTo(db, address as string);
return c.json(emails);
@@ -72,17 +96,39 @@ api.get('/emails/:id', async (c) => {
const { id } = c.req.param();
// 函数调用修正:使用 findEmailById 函数
const email = await findEmailById(db, id);
+ if (!email) {
+ return c.json({ message: 'Email not found'}, 404);
+ }
return c.json(email);
});
// 删除邮件
api.post('/delete-emails', turnstile, async (c) => {
const db = getD1DB(c.env.DB);
- const { ids } = await c.req.json();
+ // fix: 从上下文中获取已解析的请求体,并增加健壮性处理
+ const { ids } = c.get('parsedBody') || {};
+ if (!ids || !Array.isArray(ids) || ids.length === 0) {
+ return c.json({ message: 'ids are required' }, 400);
+ }
const result = await deleteEmails(db, ids as string[]);
return c.json(result);
});
+// feat: 添加登录路由
+api.post('/login', turnstile, async (c) => {
+ const db = getD1DB(c.env.DB);
+ // fix: 从上下文中获取已解析的请求体,并增加健壮性处理
+ const { password } = c.get('parsedBody') || {};
+ if (!password) {
+ return c.json({ message: 'Password is required' }, 400);
+ }
+ const email = await getEmailByPassword(db, password as string);
+ if (!email) {
+ return c.json({ message: 'Invalid password' }, 404);
+ }
+ return c.json({ address: email.messageTo });
+});
+
// 前端配置接口
app.get('/config', (c) => {
@@ -115,7 +161,7 @@ export default {
const email = insertEmailSchema.parse(newEmail);
await insertEmail(db, email);
} catch (e) {
- console.error('Failed to process email:', e);
+ console.error('处理邮件失败:', e);
}
},
@@ -133,8 +179,8 @@ export default {
// 定时任务 (清理过期邮件)
async scheduled(event, env, ctx) {
const db = getD1DB(env.DB);
+ // 清理一小时前的邮件
const oneHourAgo = new Date(Date.now() - 1000 * 60 * 60);
- // 注意: `deleteExpiredEmails` 是一个假设的函数,您需要在 `database/dao.ts` 中实现它
- // await deleteExpiredEmails(db, oneHourAgo);
+ await deleteExpiredEmails(db, oneHourAgo);
},
};
\ No newline at end of file