From 9ad3115833938415cc09f2235d1b490256916361 Mon Sep 17 00:00:00 2001 From: beilunyang <786220806@qq.com> Date: Mon, 10 Feb 2025 11:25:25 +0800 Subject: [PATCH] feat: Implement OpenAPI with API Key authentication and role-based access control --- README.md | 144 +++-- app/api/admin-contact/route.ts | 12 + app/api/api-keys/[id]/route.ts | 91 ++++ app/api/api-keys/route.ts | 75 +++ app/api/config/route.ts | 20 +- app/api/emails/[id]/[messageId]/route.ts | 22 +- app/api/emails/[id]/route.ts | 25 +- app/api/emails/generate/route.ts | 10 +- app/api/emails/route.ts | 7 +- app/api/roles/promote/route.ts | 13 +- app/components/no-permission-dialog.tsx | 10 +- app/components/profile/api-key-panel.tsx | 432 +++++++++++++++ app/components/profile/config-panel.tsx | 20 +- app/components/profile/profile-card.tsx | 5 +- app/components/profile/promote-panel.tsx | 163 +++--- app/components/profile/webhook-config.tsx | 17 +- app/components/ui/dialog.tsx | 33 ++ app/hooks/use-admin-contact.ts | 38 ++ app/lib/apiKey.ts | 57 ++ app/lib/auth.ts | 1 + app/lib/permissions.ts | 7 + app/lib/schema.ts | 20 +- drizzle/0009_overconfident_gateway.sql | 13 + drizzle/0010_brief_stellaris.sql | 2 + drizzle/meta/0009_snapshot.json | 608 ++++++++++++++++++++++ drizzle/meta/0010_snapshot.json | 608 ++++++++++++++++++++++ drizzle/meta/_journal.json | 14 + middleware.ts | 16 +- 28 files changed, 2339 insertions(+), 144 deletions(-) create mode 100644 app/api/admin-contact/route.ts create mode 100644 app/api/api-keys/[id]/route.ts create mode 100644 app/api/api-keys/route.ts create mode 100644 app/components/profile/api-key-panel.tsx create mode 100644 app/hooks/use-admin-contact.ts create mode 100644 app/lib/apiKey.ts create mode 100644 drizzle/0009_overconfident_gateway.sql create mode 100644 drizzle/0010_brief_stellaris.sql create mode 100644 drizzle/meta/0009_snapshot.json create mode 100644 drizzle/meta/0010_snapshot.json diff --git a/README.md b/README.md index 3aeccf5..4bcb93a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ 邮箱域名配置权限系统Webhook 集成 • + OpenAPI环境变量Github OAuth App 配置贡献 • @@ -47,6 +48,7 @@ - 🎉 **可爱的 UI**:简洁可爱萌萌哒 UI 界面 - 🔔 **Webhook 通知**:支持通过 webhook 接收新邮件通知 - 🛡️ **权限系统**:支持基于角色的权限控制系统 +- 🔑 **OpenAPI**:支持通过 API Key 访问 OpenAPI ## 技术栈 @@ -276,48 +278,39 @@ pnpm deploy:cleanup 本项目采用基于角色的权限控制系统(RBAC)。 -### 权限配置 +### 角色配置 新用户默认角色由皇帝在个人中心的网站设置中配置: +- 公爵:新用户将获得临时邮箱、Webhook 配置权限以及 API Key 管理权限 - 骑士:新用户将获得临时邮箱和 Webhook 配置权限 -- 平民:新用户无任何权限,需要等待皇帝册封为骑士 - -### 角色管理 - -1. **册封骑士** - - 皇帝可以在个人中心的角色管理面板中册封骑士 - - 通过用户邮箱查找并册封 - - 平民被册封后将获得临时邮箱和 Webhook 配置权限 - - 不能册封已经是骑士的用户 - - 不能册封皇帝 - -2. **贬为平民** - - 皇帝可以将骑士贬为平民 - - 被贬为平民后将失去所有权限 - - 不能贬低已经是平民的用户 - - 不能贬低皇帝 +- 平民:新用户无任何权限,需要等待皇帝册封为骑士或公爵 ### 角色等级 -系统包含三个角色等级: +系统包含四个角色等级: 1. **皇帝(Emperor)** - 网站所有者 - 拥有所有权限 - - 可以配置新用户默认角色 - - 可以册封骑士或将骑士贬为平民 - - 每个站点仅允许一位皇帝 + - 每个站点只能有一个皇帝 -2. **骑士(Knight)** +2. **公爵(Duke)** + - 超级用户 + - 可以使用临时邮箱功能 + - 可以配置 Webhook + - 可以使用创建 API Key 调用 OpenAPI + - 可以被皇帝贬为骑士或平民 + +3. **骑士(Knight)** - 高级用户 - 可以使用临时邮箱功能 - 可以配置 Webhook - - 可以被皇帝贬为平民 + - 可以被皇帝贬为平民或册封为公爵 3. **平民(Civilian)** - 普通用户 - 无任何权限 - - 可以被皇帝册封为骑士 + - 可以被皇帝册封为骑士或者公爵 ### 角色升级 @@ -325,9 +318,16 @@ pnpm deploy:cleanup - 第一个访问 `/api/roles/init-emperor` 接口的用户将成为皇帝,即网站所有者 - 站点已有皇帝后,无法再提升其他用户为皇帝 -2. **成为骑士** - - 皇帝在个人中心页面对平民进行册封 - - 或由皇帝设置新用户默认为骑士角色 +2. **角色变更** + - 皇帝可以在个人中心页面将其他用户设为公爵、骑士或平民 + +### 权限说明 + +- **邮箱管理**:创建和管理临时邮箱 +- **Webhook 管理**:配置邮件通知的 Webhook +- **API Key 管理**:创建和管理 API 访问密钥 +- **用户管理**:升降用户角色 +- **系统配置**:管理系统全局设置 ## Webhook 集成 @@ -380,6 +380,90 @@ pnpx cloudflared tunnel --url http://localhost:3001 - Webhook 接口应在 10 秒内响应 - 非 2xx 响应码会触发重试 +## OpenAPI + +本项目提供了 OpenAPI 接口,支持通过 API Key 进行访问。API Key 可以在个人中心页面创建(需要是公爵或皇帝角色)。 + +### 使用 API Key + +在请求头中添加 API Key: +```http +X-API-Key: YOUR_API_KEY +``` + +### API 接口 + +#### 创建临时邮箱 +```http +POST /api/emails/generate +Content-Type: application/json + +{ + "name": "test", + "expiryTime": 3600000, + "domain": "moemail.app" +} +``` +参数说明: +- `name`: 邮箱前缀,可选 +- `expiryTime`: 有效期(毫秒),可选值:3600000(1小时)、86400000(1天)、604800000(7天)、0(永久) +- `domain`: 邮箱域名,可通过 `/api/emails/domains` 获取可用域名列表 + +#### 获取邮箱列表 +```http +GET /api/emails?cursor=xxx&limit=10 +``` +参数说明: +- `cursor`: 分页游标,可选 +- `limit`: 每页数量,默认 10 + +#### 获取单个邮箱 +```http +GET /api/emails/{emailId} +``` + +#### 删除邮箱 +```http +DELETE /api/emails/{emailId} +``` + +#### 获取邮件列表 +```http +GET /api/emails/{emailId}/messages?cursor=xxx&limit=10 +``` +参数说明: +- `cursor`: 分页游标,可选 +- `limit`: 每页数量,默认 10 + +#### 获取单封邮件 +```http +GET /api/emails/{emailId}/messages/{messageId} +``` + +### 使用示例 + +使用 curl 创建临时邮箱: +```bash +curl -X POST https://your-domain.com/api/emails/generate \ + -H "X-API-Key: YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "test", + "expiryTime": 3600000, + "domain": "moemail.app" + }' +``` + +使用 JavaScript 获取邮件列表: +```javascript +const res = await fetch('https://your-domain.com/api/emails/your-email-id/messages', { + headers: { + 'X-API-Key': 'YOUR_API_KEY' + } +}); +const data = await res.json(); +``` + ## 环境变量 本项目使用以下环境变量: @@ -414,9 +498,9 @@ pnpx cloudflared tunnel --url http://localhost:3001 本项目采用 [MIT](LICENSE) 许可证 ## 交流群 - +
-如二维码失效,请添加我的个人微信(hansenones),并备注 “MoeMail” 加入微信交流群 +如二维码失效,请添加我的个人微信(hansenones),并备注 "MoeMail" 加入微信交流群 ## 支持 @@ -428,4 +512,4 @@ pnpx cloudflared tunnel --url http://localhost:3001

Buy Me A Coffee - +如二维码失效,请添加我的个人微信(hansenones),并备注 “MoeMail” 加入微信交流群 diff --git a/app/api/admin-contact/route.ts b/app/api/admin-contact/route.ts new file mode 100644 index 0000000..e64fa72 --- /dev/null +++ b/app/api/admin-contact/route.ts @@ -0,0 +1,12 @@ +import { getRequestContext } from "@cloudflare/next-on-pages" + +export const runtime = "edge" + +export async function GET() { + const env = getRequestContext().env + const adminContact = await env.SITE_CONFIG.get("ADMIN_CONTACT") + + return Response.json({ + adminContact: adminContact || "" + }) +} \ No newline at end of file diff --git a/app/api/api-keys/[id]/route.ts b/app/api/api-keys/[id]/route.ts new file mode 100644 index 0000000..67a1ec9 --- /dev/null +++ b/app/api/api-keys/[id]/route.ts @@ -0,0 +1,91 @@ +import { auth } from "@/lib/auth" +import { createDb } from "@/lib/db" +import { apiKeys } from "@/lib/schema" +import { NextResponse } from "next/server" +import { checkPermission } from "@/lib/auth" +import { PERMISSIONS } from "@/lib/permissions" +import { eq, and } from "drizzle-orm" + +export const runtime = "edge" + +export async function DELETE( + _request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const hasPermission = await checkPermission(PERMISSIONS.MANAGE_API_KEY) + if (!hasPermission) { + return NextResponse.json({ error: "权限不足" }, { status: 403 }) + } + try { + const db = createDb() + const session = await auth() + const { id } = await params + + const result = await db.delete(apiKeys) + .where( + and( + eq(apiKeys.id, id), + eq(apiKeys.userId, session!.user.id!) + ) + ) + .returning() + + if (!result.length) { + return NextResponse.json( + { error: "API Key 不存在或无权删除" }, + { status: 404 } + ) + } + + return NextResponse.json({ success: true }) + } catch (error) { + console.error("Failed to delete API key:", error) + return NextResponse.json( + { error: "删除 API Key 失败" }, + { status: 500 } + ) + } +} + +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const hasPermission = await checkPermission(PERMISSIONS.MANAGE_API_KEY) + if (!hasPermission) { + return NextResponse.json({ error: "权限不足" }, { status: 403 }) + } + + try { + const session = await auth() + const { id } = await params + + const { enabled } = await request.json() as { enabled: boolean } + const db = createDb() + + const result = await db.update(apiKeys) + .set({ enabled }) + .where( + and( + eq(apiKeys.id, id), + eq(apiKeys.userId, session!.user.id!) + ) + ) + .returning() + + if (!result.length) { + return NextResponse.json( + { error: "API Key 不存在或无权更新" }, + { status: 404 } + ) + } + + return NextResponse.json({ success: true }) + } catch (error) { + console.error("Failed to update API key:", error) + return NextResponse.json( + { error: "更新 API Key 失败" }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/api-keys/route.ts b/app/api/api-keys/route.ts new file mode 100644 index 0000000..3a74a33 --- /dev/null +++ b/app/api/api-keys/route.ts @@ -0,0 +1,75 @@ +import { auth } from "@/lib/auth" +import { createDb } from "@/lib/db" +import { apiKeys } from "@/lib/schema" +import { nanoid } from "nanoid" +import { NextResponse } from "next/server" +import { checkPermission } from "@/lib/auth" +import { PERMISSIONS } from "@/lib/permissions" +import { desc, eq } from "drizzle-orm" + +export const runtime = "edge" + +export async function GET() { + const hasPermission = await checkPermission(PERMISSIONS.MANAGE_API_KEY) + if (!hasPermission) { + return NextResponse.json({ error: "权限不足" }, { status: 403 }) + } + + const session = await auth() + try { + const db = createDb() + const keys = await db.query.apiKeys.findMany({ + where: eq(apiKeys.userId, session!.user.id!), + orderBy: desc(apiKeys.createdAt), + }) + + return NextResponse.json({ + apiKeys: keys.map(key => ({ + ...key, + key: undefined + })) + }) + } catch (error) { + console.error("Failed to fetch API keys:", error) + return NextResponse.json( + { error: "获取 API Keys 失败" }, + { status: 500 } + ) + } +} + +export async function POST(request: Request) { + const hasPermission = await checkPermission(PERMISSIONS.MANAGE_API_KEY) + if (!hasPermission) { + return NextResponse.json({ error: "权限不足" }, { status: 403 }) + } + + const session = await auth() + try { + const { name } = await request.json() as { name: string } + if (!name?.trim()) { + return NextResponse.json( + { error: "名称不能为空" }, + { status: 400 } + ) + } + + const key = `mk_${nanoid(32)}` + const db = createDb() + + await db.insert(apiKeys).values({ + name, + key, + userId: session!.user.id!, + expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year + }) + + return NextResponse.json({ key }) + } catch (error) { + console.error("Failed to create API key:", error) + return NextResponse.json( + { error: "创建 API Key 失败" }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/config/route.ts b/app/api/config/route.ts index afbb205..678d78b 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -5,31 +5,35 @@ export const runtime = "edge" export async function GET() { const env = getRequestContext().env - const [defaultRole, emailDomains] = await Promise.all([ + const [defaultRole, emailDomains, adminContact] = await Promise.all([ env.SITE_CONFIG.get("DEFAULT_ROLE"), - env.SITE_CONFIG.get("EMAIL_DOMAINS") + env.SITE_CONFIG.get("EMAIL_DOMAINS"), + env.SITE_CONFIG.get("ADMIN_CONTACT") ]) - return Response.json({ + return Response.json({ defaultRole: defaultRole || ROLES.CIVILIAN, - emailDomains: emailDomains || "" + emailDomains: emailDomains || "", + adminContact: adminContact || "" }) } export async function POST(request: Request) { - const { defaultRole, emailDomains } = await request.json() as { + const { defaultRole, emailDomains, adminContact } = await request.json() as { defaultRole: Exclude, - emailDomains: string + emailDomains: string, + adminContact: string } - if (![ROLES.KNIGHT, ROLES.CIVILIAN].includes(defaultRole)) { + if (![ROLES.DUKE, ROLES.KNIGHT, ROLES.CIVILIAN].includes(defaultRole)) { return Response.json({ error: "无效的角色" }, { status: 400 }) } const env = getRequestContext().env await Promise.all([ env.SITE_CONFIG.put("DEFAULT_ROLE", defaultRole), - env.SITE_CONFIG.put("EMAIL_DOMAINS", emailDomains) + env.SITE_CONFIG.put("EMAIL_DOMAINS", emailDomains), + env.SITE_CONFIG.put("ADMIN_CONTACT", adminContact) ]) return Response.json({ success: true }) diff --git a/app/api/emails/[id]/[messageId]/route.ts b/app/api/emails/[id]/[messageId]/route.ts index 52d6fea..891be33 100644 --- a/app/api/emails/[id]/[messageId]/route.ts +++ b/app/api/emails/[id]/[messageId]/route.ts @@ -1,14 +1,30 @@ import { NextResponse } from "next/server" import { createDb } from "@/lib/db" -import { messages } from "@/lib/schema" +import { messages, emails } from "@/lib/schema" import { and, eq } from "drizzle-orm" - +import { getUserId } from "@/lib/apiKey" export const runtime = "edge" -export async function GET(request: Request, { params }: { params: Promise<{ id: string; messageId: string }> }) { +export async function GET(_request: Request, { params }: { params: Promise<{ id: string; messageId: string }> }) { try { const { id, messageId } = await params const db = createDb() + const userId = await getUserId() + + const email = await db.query.emails.findFirst({ + where: and( + eq(emails.id, id), + eq(emails.userId, userId!) + ) + }) + + if (!email) { + return NextResponse.json( + { error: "无权限查看" }, + { status: 403 } + ) + } + const message = await db.query.messages.findFirst({ where: and( eq(messages.id, messageId), diff --git a/app/api/emails/[id]/route.ts b/app/api/emails/[id]/route.ts index bd93ab6..be1e706 100644 --- a/app/api/emails/[id]/route.ts +++ b/app/api/emails/[id]/route.ts @@ -1,17 +1,16 @@ import { NextResponse } from "next/server" -import { auth } from "@/lib/auth" import { createDb } from "@/lib/db" import { emails, messages } from "@/lib/schema" import { eq, and, lt, or, sql } from "drizzle-orm" import { encodeCursor, decodeCursor } from "@/lib/cursor" - +import { getUserId } from "@/lib/apiKey" export const runtime = "edge" export async function DELETE( - _request: Request, + request: Request, { params }: { params: Promise<{ id: string }> } ) { - const session = await auth() + const userId = await getUserId() try { const db = createDb() @@ -19,7 +18,7 @@ export async function DELETE( const email = await db.query.emails.findFirst({ where: and( eq(emails.id, id), - eq(emails.userId, session!.user!.id!) + eq(emails.userId, userId!) ) }) @@ -58,6 +57,22 @@ export async function GET( const db = createDb() const { id } = await params + const userId = await getUserId() + + const email = await db.query.emails.findFirst({ + where: and( + eq(emails.id, id), + eq(emails.userId, userId!) + ) + }) + + if (!email) { + return NextResponse.json( + { error: "无权限查看" }, + { status: 403 } + ) + } + const baseConditions = eq(messages.emailId, id) const totalResult = await db.select({ count: sql`count(*)` }) diff --git a/app/api/emails/generate/route.ts b/app/api/emails/generate/route.ts index 0d64cb8..558553e 100644 --- a/app/api/emails/generate/route.ts +++ b/app/api/emails/generate/route.ts @@ -1,18 +1,18 @@ import { NextResponse } from "next/server" import { nanoid } from "nanoid" -import { auth } from "@/lib/auth" import { createDb } from "@/lib/db" 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" - +import { getUserId } from "@/lib/apiKey" export const runtime = "edge" export async function POST(request: Request) { const db = createDb() - const session = await auth() + + const userId = await getUserId() try { const activeEmailsCount = await db @@ -20,7 +20,7 @@ export async function POST(request: Request) { .from(emails) .where( and( - eq(emails.userId, session!.user!.id!), + eq(emails.userId, userId!), gt(emails.expiresAt, new Date()) ) ) @@ -76,7 +76,7 @@ export async function POST(request: Request) { address, createdAt: now, expiresAt: expires, - userId: session!.user!.id + userId: userId! } const result = await db.insert(emails) diff --git a/app/api/emails/route.ts b/app/api/emails/route.ts index 950ac6b..6686dc3 100644 --- a/app/api/emails/route.ts +++ b/app/api/emails/route.ts @@ -1,16 +1,17 @@ -import { auth } from "@/lib/auth" import { createDb } from "@/lib/db" import { and, eq, gt, lt, or, sql } from "drizzle-orm" import { NextResponse } from "next/server" import { emails } from "@/lib/schema" import { encodeCursor, decodeCursor } from "@/lib/cursor" +import { getUserId } from "@/lib/apiKey" export const runtime = "edge" const PAGE_SIZE = 20 export async function GET(request: Request) { - const session = await auth() + const userId = await getUserId() + const { searchParams } = new URL(request.url) const cursor = searchParams.get('cursor') @@ -18,7 +19,7 @@ export async function GET(request: Request) { try { const baseConditions = and( - eq(emails.userId, session!.user!.id!), + eq(emails.userId, userId!), gt(emails.expiresAt, new Date()) ) diff --git a/app/api/roles/promote/route.ts b/app/api/roles/promote/route.ts index fe8219d..546db66 100644 --- a/app/api/roles/promote/route.ts +++ b/app/api/roles/promote/route.ts @@ -10,7 +10,7 @@ export async function POST(request: Request) { try { const { userId, roleName } = await request.json() as { userId: string, - roleName: typeof ROLES.KNIGHT | typeof ROLES.CIVILIAN + roleName: typeof ROLES.DUKE | typeof ROLES.KNIGHT | typeof ROLES.CIVILIAN }; if (!userId || !roleName) { return Response.json( @@ -19,7 +19,7 @@ export async function POST(request: Request) { ); } - if (![ROLES.KNIGHT, ROLES.CIVILIAN].includes(roleName)) { + if (![ROLES.DUKE, ROLES.KNIGHT, ROLES.CIVILIAN].includes(roleName)) { return Response.json( { error: "角色不合法" }, { status: 400 } @@ -47,9 +47,11 @@ export async function POST(request: Request) { }); if (!targetRole) { - const description = roleName === ROLES.KNIGHT - ? "高级用户" - : "普通用户"; + const description = { + [ROLES.DUKE]: "超级用户", + [ROLES.KNIGHT]: "高级用户", + [ROLES.CIVILIAN]: "普通用户", + }[roleName]; const [newRole] = await db.insert(roles) .values({ @@ -64,7 +66,6 @@ export async function POST(request: Request) { return Response.json({ success: true, - message: roleName === ROLES.KNIGHT ? "册封成功" : "贬为平民" }); } catch (error) { console.error("Failed to change user role:", error); diff --git a/app/components/no-permission-dialog.tsx b/app/components/no-permission-dialog.tsx index 09349bc..1dded67 100644 --- a/app/components/no-permission-dialog.tsx +++ b/app/components/no-permission-dialog.tsx @@ -2,9 +2,10 @@ import { Button } from "@/components/ui/button" import { useRouter } from "next/navigation" - +import { useAdminContact } from "@/hooks/use-admin-contact" export function NoPermissionDialog() { const router = useRouter() + const { adminContact } = useAdminContact() return (
@@ -12,7 +13,12 @@ export function NoPermissionDialog() {

权限不足

-

你没有权限访问此页面,请联系网站皇帝授权

+

你没有权限访问此页面,请联系网站管理员

+ { + adminContact && ( +

管理员联系方式:{adminContact}

+ ) + } + + + + + {newKey ? "API Key 创建成功" : "创建新的 API Key"} + + {newKey && ( + + 请立即保存此密钥,它只会显示一次且无法恢复 + + )} + + + {!newKey ? ( +
+
+ + setNewKeyName(e.target.value)} + placeholder="为你的 API Key 起个名字" + /> +
+
+ ) : ( +
+
+ +
+ + +
+
+
+ )} + + + + + + {!newKey && ( + + )} + +
+ + ) + } +
+ + { + !canManageApiKey ? ( +
+

需要公爵或更高权限才能管理 API Key

+

请联系网站管理员升级您的角色

+ { + adminContact && ( +

管理员联系方式:{adminContact}

+ ) + } +
+ ) : ( +
+ {isLoading ? ( +
+
+ +
+
+

加载中...

+
+
+ ) : apiKeys.length === 0 ? ( +
+
+ +
+
+

没有 API Keys

+

+ 点击上方的"创建 API Key"按钮来创建你的第一个 API Key +

+
+
+ ) : ( + <> + {apiKeys.map((key) => ( +
+
+
{key.name}
+
+ 创建于 {new Date(key.createdAt).toLocaleString()} +
+
+
+ toggleApiKey(key.id, checked)} + /> + +
+
+ ))} + +
+ + + {showExamples && ( +
+
+
+
生成临时邮箱
+ +
+
+                          {`curl -X POST https://${window.location.host}/api/emails/generate \\
+  -H "X-API-Key: YOUR_API_KEY" \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "name": "test",
+    "expiryTime": 3600000,
+    "domain": "moemail.app"
+  }'`}
+                        
+
+ +
+
+
获取邮箱列表
+ +
+
+                          {`curl https://${window.location.host}/api/emails?cursor=CURSOR \\
+  -H "X-API-Key: YOUR_API_KEY"`}
+                        
+
+ +
+
+
获取邮件列表
+ +
+
+                          {`curl https://${window.location.host}/api/emails/{emailId}/messages?cursor=CURSOR \\
+  -H "X-API-Key: YOUR_API_KEY"`}
+                        
+
+ +
+
+
获取单封邮件
+ +
+
+                          {`curl https://${window.location.host}/api/emails/{emailId}/{messageId} \\
+  -H "X-API-Key: YOUR_API_KEY"`}
+                        
+
+ +
+

注意:

+
    +
  • 请将 YOUR_API_KEY 替换为你的实际 API Key
  • +
  • emailId 是邮箱的唯一标识符
  • +
  • messageId 是邮件的唯一标识符
  • +
  • expiryTime 是邮箱的有效期(毫秒),可选值:3600000(1小时)、86400000(1天)、604800000(7天)、0(永久)
  • +
  • domain 是邮箱域名,可通过 /api/emails/domains 获取可用域名列表
  • +
  • cursor 用于分页,从上一次请求的响应中获取 nextCursor
  • +
  • 所有请求都需要包含 X-API-Key 请求头
  • +
+
+
+ )} +
+ + )} +
+ ) + } +
+ ) +} \ No newline at end of file diff --git a/app/components/profile/config-panel.tsx b/app/components/profile/config-panel.tsx index c718466..37b83ea 100644 --- a/app/components/profile/config-panel.tsx +++ b/app/components/profile/config-panel.tsx @@ -17,9 +17,11 @@ import { export function ConfigPanel() { const [defaultRole, setDefaultRole] = useState("") const [emailDomains, setEmailDomains] = useState("") + const [adminContact, setAdminContact] = useState("") const [loading, setLoading] = useState(false) const { toast } = useToast() + useEffect(() => { fetchConfig() }, []) @@ -42,7 +44,11 @@ export function ConfigPanel() { const res = await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ defaultRole, emailDomains }), + body: JSON.stringify({ + defaultRole, + emailDomains, + adminContact + }), }) if (!res.ok) throw new Error("保存失败") @@ -77,6 +83,7 @@ export function ConfigPanel() { + 公爵 骑士 平民 @@ -94,6 +101,17 @@ export function ConfigPanel() {
+
+ 管理员联系方式: +
+ setAdminContact(e.target.value)} + placeholder="如: 微信号、邮箱等" + /> +
+
+ - +
+
+
+ setSearchText(e.target.value)} + placeholder="输入用户名或邮箱" + /> +
+
+ +
) diff --git a/app/components/profile/webhook-config.tsx b/app/components/profile/webhook-config.tsx index 0e47595..fd39d72 100644 --- a/app/components/profile/webhook-config.tsx +++ b/app/components/profile/webhook-config.tsx @@ -37,8 +37,13 @@ export function WebhookConfig() { if (initialLoading) { return ( -
- +
+
+ +
+
+

加载中...

+
) } @@ -138,8 +143,8 @@ export function WebhookConfig() { -