feat: Implement OpenAPI with API Key authentication and role-based access control

This commit is contained in:
beilunyang
2025-02-10 11:25:25 +08:00
parent 1bc0369b83
commit 9ad3115833
28 changed files with 2339 additions and 144 deletions

View File

@@ -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 || ""
})
}

View File

@@ -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 }
)
}
}

75
app/api/api-keys/route.ts Normal file
View File

@@ -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 }
)
}
}

View File

@@ -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<Role, typeof ROLES.EMPEROR>,
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 })

View File

@@ -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),

View File

@@ -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<number>`count(*)` })

View File

@@ -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)

View File

@@ -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())
)

View File

@@ -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);