diff --git a/.env.example b/.env.example index fd7145c..5191c3c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ AUTH_GITHUB_ID = "" AUTH_GITHUB_SECRET = "" -AUTH_SECRET = "" -NEXT_PUBLIC_EMAIL_DOMAIN = "" \ No newline at end of file +AUTH_SECRET = "" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7f7983a..6e98ce7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -139,7 +139,6 @@ jobs: env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - NEXT_PUBLIC_EMAIL_DOMAIN: ${{ secrets.NEXT_PUBLIC_EMAIL_DOMAIN || vars.NEXT_PUBLIC_EMAIL_DOMAIN }} run: pnpm run deploy:pages # Deploy email worker if changed or manually triggered diff --git a/README.md b/README.md index a354732..31b0e34 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ 技术栈 • 本地运行 • 部署 • - Cloudflare 邮件路由配置 • + 邮箱域名配置 • 权限系统 • Webhook 集成 • 环境变量 • @@ -221,9 +221,15 @@ pnpm deploy:cleanup - 在 Settings 中选择变量和机密 - 添加 AUTH_GITHUB_ID, AUTH_GITHUB_SECRET, AUTH_SECRET -## Cloudflare 邮件路由配置 -在部署完成后,需要在 Cloudflare 控制台配置邮件路由,将收到的邮件转发给 Email Worker 处理。 +## 邮箱域名配置 + +在 MoeMail 个人中心页面,可以配置网站的邮箱域名,支持多域名配置,多个域名用逗号分隔 + + +### Cloudflare 邮件路由配置 + +为了使邮箱域名生效,还需要在 Cloudflare 控制台配置邮件路由,将收到的邮件转发给 Email Worker 处理。 1. 登录 [Cloudflare 控制台](https://dash.cloudflare.com/) 2. 选择您的域名 @@ -344,14 +350,12 @@ pnpx cloudflared tunnel --url http://localhost:3001 - `AUTH_GITHUB_SECRET`: GitHub OAuth App Secret - `AUTH_SECRET`: NextAuth Secret,用来加密 session,请设置一个随机字符串 -### 邮箱配置 -- `NEXT_PUBLIC_EMAIL_DOMAIN`: 邮箱域名,支持多域名,用逗号分隔 (例如: moemail.app,bitibiti.com) - ### Cloudflare 配置 - `CLOUDFLARE_API_TOKEN`: Cloudflare API Token - `CLOUDFLARE_ACCOUNT_ID`: Cloudflare Account ID - `DATABASE_NAME`: D1 数据库名称 - `DATABASE_ID`: D1 数据库 ID +- `KV_NAMESPACE_ID`: Cloudflare KV namespace ID,用于存储网站配置 ## Github OAuth App 配置 diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 0fb7d46..afbb205 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -4,18 +4,33 @@ import { getRequestContext } from "@cloudflare/next-on-pages" export const runtime = "edge" export async function GET() { - const config = await getRequestContext().env.SITE_CONFIG.get("DEFAULT_ROLE") + const env = getRequestContext().env + const [defaultRole, emailDomains] = await Promise.all([ + env.SITE_CONFIG.get("DEFAULT_ROLE"), + env.SITE_CONFIG.get("EMAIL_DOMAINS") + ]) - return Response.json({ defaultRole: config || ROLES.CIVILIAN }) + return Response.json({ + defaultRole: defaultRole || ROLES.CIVILIAN, + emailDomains: emailDomains || "" + }) } export async function POST(request: Request) { - const { defaultRole } = await request.json() as { defaultRole: Exclude } + const { defaultRole, emailDomains } = await request.json() as { + defaultRole: Exclude, + emailDomains: string + } if (![ROLES.KNIGHT, ROLES.CIVILIAN].includes(defaultRole)) { return Response.json({ error: "无效的角色" }, { status: 400 }) } - await getRequestContext().env.SITE_CONFIG.put("DEFAULT_ROLE", defaultRole) + const env = getRequestContext().env + await Promise.all([ + env.SITE_CONFIG.put("DEFAULT_ROLE", defaultRole), + env.SITE_CONFIG.put("EMAIL_DOMAINS", emailDomains) + ]) + return Response.json({ success: true }) } \ No newline at end of file diff --git a/app/api/emails/domains/route.ts b/app/api/emails/domains/route.ts index b3dbd29..0a40e89 100644 --- a/app/api/emails/domains/route.ts +++ b/app/api/emails/domains/route.ts @@ -1,20 +1,13 @@ -import { EMAIL_CONFIG } from "@/config" +import { getRequestContext } from "@cloudflare/next-on-pages" import { NextResponse } from "next/server" export const runtime = "edge" export async function GET() { try { - const domains = EMAIL_CONFIG.DOMAINS + const domainString = await getRequestContext().env.SITE_CONFIG.get("EMAIL_DOMAINS") - if (domains.length === 0) { - return NextResponse.json( - { error: "无效的域名列表" }, - { status: 400 } - ) - } - - return NextResponse.json({ domains }) + return NextResponse.json({ domains: domainString ? domainString.split(',') : ["moemail.app"] }) } catch (error) { console.error('Failed to fetch domains:', error) return NextResponse.json( diff --git a/app/api/emails/generate/route.ts b/app/api/emails/generate/route.ts index 5db6f60..ea3a06f 100644 --- a/app/api/emails/generate/route.ts +++ b/app/api/emails/generate/route.ts @@ -6,6 +6,7 @@ import { emails } from "@/lib/schema" import { eq, and, gt, sql } from "drizzle-orm" import { EXPIRY_OPTIONS } from "@/types/email" import { EMAIL_CONFIG } from "@/config" +import { getRequestContext } from "@cloudflare/next-on-pages" export const runtime = "edge" @@ -14,7 +15,6 @@ export async function POST(request: Request) { const session = await auth() try { - // Check current number of active emails for user const activeEmailsCount = await db .select({ count: sql`count(*)` }) .from(emails) @@ -45,7 +45,10 @@ export async function POST(request: Request) { ) } - if (!EMAIL_CONFIG.DOMAINS.includes(domain)) { + const domainString = await getRequestContext().env.SITE_CONFIG.get("EMAIL_DOMAINS") + const domains = domainString ? domainString.split(',') : ["moemail.app"] + + if (!domains || !domains.includes(domain)) { return NextResponse.json( { error: "无效的域名" }, { status: 400 } diff --git a/app/components/emails/create-dialog.tsx b/app/components/emails/create-dialog.tsx index f715b21..ba9076c 100644 --- a/app/components/emails/create-dialog.tsx +++ b/app/components/emails/create-dialog.tsx @@ -17,10 +17,6 @@ interface CreateDialogProps { onEmailCreated: () => void } -interface DomainResponse { - domains: string[] -} - export function CreateDialog({ onEmailCreated }: CreateDialogProps) { const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) @@ -89,7 +85,7 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) { const fetchDomains = async () => { const response = await fetch("/api/emails/domains"); - const data = (await response.json()) as DomainResponse; + const data = (await response.json()) as { domains: string[] }; setDomains(data.domains || []); setCurrentDomain(data.domains[0] || ""); }; diff --git a/app/components/profile/config-panel.tsx b/app/components/profile/config-panel.tsx index bb94107..c718466 100644 --- a/app/components/profile/config-panel.tsx +++ b/app/components/profile/config-panel.tsx @@ -5,6 +5,7 @@ import { Settings } from "lucide-react" import { useToast } from "@/components/ui/use-toast" import { useState, useEffect } from "react" import { Role, ROLES } from "@/lib/permissions" +import { Input } from "@/components/ui/input" import { Select, SelectContent, @@ -15,6 +16,7 @@ import { export function ConfigPanel() { const [defaultRole, setDefaultRole] = useState("") + const [emailDomains, setEmailDomains] = useState("") const [loading, setLoading] = useState(false) const { toast } = useToast() @@ -25,8 +27,12 @@ export function ConfigPanel() { const fetchConfig = async () => { const res = await fetch("/api/config") if (res.ok) { - const data = await res.json() as { defaultRole: Exclude } + const data = await res.json() as { + defaultRole: Exclude, + emailDomains: string + } setDefaultRole(data.defaultRole) + setEmailDomains(data.emailDomains) } } @@ -36,14 +42,14 @@ export function ConfigPanel() { const res = await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ defaultRole }), + body: JSON.stringify({ defaultRole, emailDomains }), }) if (!res.ok) throw new Error("保存失败") toast({ title: "保存成功", - description: "默认角色设置已更新", + description: "网站设置已更新", }) } catch (error) { toast({ @@ -75,13 +81,26 @@ export function ConfigPanel() { 平民 - - 保存 - + + + 邮箱域名: + + setEmailDomains(e.target.value)} + placeholder="多个域名用逗号分隔,如: moemail.app,bitibiti.com" + /> + + + + + 保存 + ) diff --git a/app/config/email.ts b/app/config/email.ts index ce40449..7f2b065 100644 --- a/app/config/email.ts +++ b/app/config/email.ts @@ -1,9 +1,6 @@ -const DOMAINS = typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_EMAIL_DOMAIN ? process.env.NEXT_PUBLIC_EMAIL_DOMAIN : 'moemail.app' - export const EMAIL_CONFIG = { MAX_ACTIVE_EMAILS: 30, // Maximum number of active emails POLL_INTERVAL: 10_000, // Polling interval in milliseconds - DOMAINS: DOMAINS.split(','), // Email domains array } as const export type EmailConfig = typeof EMAIL_CONFIG \ No newline at end of file diff --git a/scripts/generate-test-data.ts b/scripts/generate-test-data.ts index 6dd957e..ac2216e 100644 --- a/scripts/generate-test-data.ts +++ b/scripts/generate-test-data.ts @@ -1,13 +1,11 @@ import { drizzle } from 'drizzle-orm/d1' import { emails, messages } from '../app/lib/schema' import { nanoid } from 'nanoid' -import { EMAIL_CONFIG} from '../app/config' const TEST_USER_ID = '4e4c1d5d-a3c9-407a-8808-2a2424b38c62' interface Env { DB: D1Database - NEXT_PUBLIC_EMAIL_DOMAIN: string } const MAX_EMAIL_COUNT = 5 @@ -22,7 +20,7 @@ async function generateTestData(env: Env) { // 生成测试邮箱 const testEmails = Array.from({ length: MAX_EMAIL_COUNT }).map(() => ({ id: crypto.randomUUID(), - address: `${nanoid(6)}@${EMAIL_CONFIG.DOMAINS[0]}`, + address: `${nanoid(6)}@moemail.app`, userId: TEST_USER_ID, createdAt: now, expiresAt: new Date(now.getTime() + 24 * 60 * 60 * 1000),