feat: 添加通过密码登录的功能
This commit is contained in:
1 parent
89302e1afb
commit
4f010d271e
6 files changed
+296
-146
No files matched your search
@@ -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 (
|
||||
<div className="text-white w-full mt-auto flex flex-col items-center justify-between px-5 pt-16 mb-10 md:px-10 mx-auto sm:flex-row">
|
||||
<Link to="/" className="text-xl font-black leading-none select-none logo">
|
||||
VMAIL.DEV
|
||||
</Link>{" "}
|
||||
<p className="mt-4 text-sm text-gray-400 sm:ml-4 sm:pl-4 sm:border-l sm:border-gray-200 sm:mt-0">
|
||||
© 2024 Products of{" "}
|
||||
<a
|
||||
className="font-semibold underline hover:text-gray-600"
|
||||
href="https://www.oiov.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
oiov
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="inline-flex justify-center mt-4 space-x-5 sm:ml-auto sm:mt-0 sm:justify-start">
|
||||
{/* 社交媒体和联系方式链接 */}
|
||||
<a
|
||||
href="https://wr.do"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="WR.DO"
|
||||
className="text-gray-400 hover:text-gray-500 scale-[1.2]">
|
||||
<WrdoLogo className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="mailto:hi@oiov.dev"
|
||||
title="Email"
|
||||
className="text-gray-400 hover:text-gray-500">
|
||||
<MailIcon className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/yesmoree"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Twitter"
|
||||
className="text-gray-400 hover:text-gray-500">
|
||||
<Twitter />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/oiov/vmail"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Github"
|
||||
className="text-gray-400 hover:text-gray-500">
|
||||
<GitHub />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
// 将默认导出(export default)改为具名导出(export)
|
||||
export function Modal({
|
||||
children,
|
||||
showModal,
|
||||
setShowModal,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
showModal: boolean;
|
||||
setShowModal: Dispatch<SetStateAction<boolean>>;
|
||||
}) {
|
||||
const modalRef = useRef<HTMLDivElement>(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 (
|
||||
<Leaflet show={showModal} setShow={setShowModal}>
|
||||
{children}
|
||||
</Leaflet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 flex items-center justify-center"
|
||||
style={{
|
||||
backdropFilter: "blur(4px)",
|
||||
WebkitBackdropFilter: "blur(4px)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative z-50 w-full max-w-lg"
|
||||
ref={modalRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<SetStateAction<boolean>>;
|
||||
}) {
|
||||
const navigation = useNavigation();
|
||||
onLogin: (password: string) => Promise<void>; // 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 (
|
||||
<Modal showModal={showPasswordModal} setShowModal={setShowPasswordModal}>
|
||||
@@ -51,9 +55,10 @@ export default function PasswordModal({
|
||||
{t("Save your password and continue using this email in 1 day")}
|
||||
</p>
|
||||
</div>
|
||||
<Form method="POST" className="flex flex-col mt-4 space-y-4 px-4">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col mt-4 space-y-4 px-4">
|
||||
<input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="text"
|
||||
name="password"
|
||||
placeholder={t("Enter your password *")}
|
||||
@@ -66,55 +71,51 @@ export default function PasswordModal({
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-yellow-600">
|
||||
{t(
|
||||
"Remember your password, otherwise your email will expire and cannot be retrieved"
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
|
||||
{password && password.length > 0 ? (
|
||||
<button
|
||||
type="submit"
|
||||
name="_action"
|
||||
value="stop"
|
||||
className="py-2.5 text-white rounded-md w-full bg-red-500 hover:opacity-90 disabled:cursor-not-allowed disabled:bg-zinc-500">
|
||||
{t("Log out")}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
value="login"
|
||||
name="_action"
|
||||
disabled={navigation.state != "idle"}
|
||||
className="py-2.5 text-white rounded-md w-full bg-cyan-600 hover:opacity-90 disabled:cursor-not-allowed disabled:bg-zinc-500">
|
||||
{navigation.state === "submitting"
|
||||
? t("Submitting...")
|
||||
: t("Login")}
|
||||
</button>
|
||||
)}
|
||||
</Form>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoggingIn}
|
||||
className="py-2.5 text-white rounded-md w-full bg-cyan-600 hover:opacity-90 disabled:cursor-not-allowed disabled:bg-zinc-500">
|
||||
{isLoggingIn
|
||||
? t("Submitting...")
|
||||
: t("Login")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePasswordModal(password: string) {
|
||||
// 自定义 Hook,用于管理密码模态框的可见性
|
||||
export function usePasswordModal() {
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
|
||||
const SenderModalCallback = useCallback(() => {
|
||||
return (
|
||||
<PasswordModal
|
||||
password={password}
|
||||
showPasswordModal={showPasswordModal}
|
||||
setShowPasswordModal={setShowPasswordModal}
|
||||
/>
|
||||
);
|
||||
}, [showPasswordModal, setShowPasswordModal]);
|
||||
// 使用 useCallback 优化 PasswordModal 组件的创建
|
||||
const PasswordModalCallback = useCallback(
|
||||
({ onLogin, isLoggingIn }: Omit<PasswordModalProps, 'showPasswordModal' | 'setShowPasswordModal'>) => {
|
||||
return (
|
||||
<PasswordModal
|
||||
showPasswordModal={showPasswordModal}
|
||||
setShowPasswordModal={setShowPasswordModal}
|
||||
onLogin={onLogin}
|
||||
isLoggingIn={isLoggingIn}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[showPasswordModal] // 依赖项
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({ setShowPasswordModal, PasswordModal: SenderModalCallback }),
|
||||
[setShowPasswordModal, SenderModalCallback]
|
||||
() => ({
|
||||
showPasswordModal,
|
||||
setShowPasswordModal,
|
||||
PasswordModal: PasswordModalCallback
|
||||
}),
|
||||
[setShowPasswordModal, PasswordModalCallback, showPasswordModal]
|
||||
);
|
||||
}
|
||||
@@ -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<string>('');
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
|
||||
// feat: 初始化密码模态框
|
||||
const { PasswordModal, setShowPasswordModal } = usePasswordModal();
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
|
||||
// 使用 React Query 获取邮件列表
|
||||
const { data: emails = [], isLoading, isFetching, refetch } = useQuery<Email[]>({
|
||||
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 (
|
||||
<div className="h-full flex flex-col gap-4 md:flex-row justify-center items-start mt-24 mx-6 md:mx-10">
|
||||
<Toaster position="top-center" />
|
||||
{/* feat: 渲染密码模态框 */}
|
||||
<PasswordModal onLogin={handleLogin} isLoggingIn={isLoggingIn} />
|
||||
<div className="flex flex-col text-white items-start w-full md:w-[350px] mx-auto gap-2">
|
||||
{/* 左侧信息面板 */}
|
||||
<div className="w-full mb-4 md:max-w-[350px] shrink-0 group group-hover:before:duration-500 group-hover:after:duration-500 after:duration-500 hover:border-cyan-600 hover:before:[box-shadow:_20px_20px_20px_30px_#a21caf] duration-500 before:duration-500 hover:duration-500 hover:after:-right-8 hover:before:right-12 hover:before:-bottom-8 hover:before:blur origin-left hover:decoration-2 relative bg-neutral-800 h-full border text-left p-4 rounded-lg overflow-hidden border-cyan-50/20 before:absolute before:w-12 before:h-12 before:content[''] before:right-1 before:top-1 before:z-10 before:bg-violet-500 before:rounded-full before:blur-lg after:absolute after:z-10 after:w-20 after:h-20 after:content[''] after:bg-rose-300 after:right-8 after:top-3 after:rounded-full after:blur-lg">
|
||||
@@ -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")}
|
||||
</button>
|
||||
{/* feat: 添加登录按钮 */}
|
||||
<p
|
||||
className="mt-4 text-sm text-cyan-500 cursor-pointer"
|
||||
onClick={() => setShowPasswordModal(true)}>
|
||||
<PasswordIcon className="inline-block w-4 h-4 mr-2" />
|
||||
{t("Have a password? Login.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<Email[]> => {
|
||||
const response = await axios.post('/api/emails', { address, token });
|
||||
return response.data;
|
||||
};
|
||||
export async function getEmails(address: string, token: string): Promise<Email[]> {
|
||||
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<Email> => {
|
||||
const response = await axios.get(`/api/emails/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
// feat: 添加获取单封邮件详情的函数
|
||||
export async function getEmailById(id: string): Promise<Email> {
|
||||
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<any> => {
|
||||
const response = await axios.post('/api/delete-emails', { ids, token });
|
||||
return response.data;
|
||||
};
|
||||
export async function deleteEmails(ids: string[], token: string): Promise<void> {
|
||||
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 调用函数
|
||||
// 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();
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
+53
-7
@@ -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);
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user