+
-
+如二维码失效,请添加我的个人微信(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你没有权限访问此页面,请联系网站皇帝授权
+你没有权限访问此页面,请联系网站管理员
+ { + adminContact && ( +管理员联系方式:{adminContact}
+ ) + }加载中...
+请求体示例:
-{`{
+ {`{
"emailId": "email-uuid",
"messageId": "message-uuid",
"fromAddress": "sender@example.com",
diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx
index 585e811..3bc0271 100644
--- a/app/components/ui/dialog.tsx
+++ b/app/components/ui/dialog.tsx
@@ -2,6 +2,7 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
@@ -39,6 +40,10 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
+
+
+ 关闭
+
))
@@ -58,6 +63,20 @@ const DialogHeader = ({
)
DialogHeader.displayName = "DialogHeader"
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
@@ -73,11 +92,25 @@ const DialogTitle = React.forwardRef<
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
+ DialogFooter,
DialogTitle,
DialogClose,
+ DialogDescription,
}
\ No newline at end of file
diff --git a/app/hooks/use-admin-contact.ts b/app/hooks/use-admin-contact.ts
new file mode 100644
index 0000000..8ecd47d
--- /dev/null
+++ b/app/hooks/use-admin-contact.ts
@@ -0,0 +1,38 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useToast } from "@/components/ui/use-toast"
+
+export function useAdminContact() {
+ const [adminContact, setAdminContact] = useState("")
+ const [loading, setLoading] = useState(true)
+ const { toast } = useToast()
+
+ const fetchAdminContact = async () => {
+ try {
+ const res = await fetch("/api/admin-contact")
+ if (!res.ok) throw new Error("获取管理员联系方式失败")
+ const data = await res.json() as { adminContact: string }
+ setAdminContact(data.adminContact)
+ } catch (error) {
+ console.error(error)
+ toast({
+ title: "获取失败",
+ description: "获取管理员联系方式失败",
+ variant: "destructive"
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ fetchAdminContact()
+ }, [])
+
+ return {
+ adminContact,
+ loading,
+ refreshAdminContact: fetchAdminContact
+ }
+}
\ No newline at end of file
diff --git a/app/lib/apiKey.ts b/app/lib/apiKey.ts
new file mode 100644
index 0000000..28dce30
--- /dev/null
+++ b/app/lib/apiKey.ts
@@ -0,0 +1,57 @@
+import { createDb } from "./db"
+import { apiKeys } from "./schema"
+import { eq, and, gt } from "drizzle-orm"
+import { NextResponse } from "next/server"
+import type { User } from "next-auth"
+import { auth } from "./auth"
+import { headers } from "next/headers"
+
+async function getUserByApiKey(key: string): Promise {
+ const db = createDb()
+ const apiKey = await db.query.apiKeys.findFirst({
+ where: and(
+ eq(apiKeys.key, key),
+ eq(apiKeys.enabled, true),
+ gt(apiKeys.expiresAt, new Date())
+ ),
+ with: {
+ user: true
+ }
+ })
+
+ if (!apiKey) return null
+
+ return apiKey.user
+}
+
+export async function handleApiKeyAuth(apiKey: string, pathname: string) {
+ if (!pathname.startsWith('/api/emails')) {
+ return NextResponse.json(
+ { error: "无权限查看" },
+ { status: 403 }
+ )
+ }
+
+ const user = await getUserByApiKey(apiKey)
+ if (!user?.id) {
+ return NextResponse.json(
+ { error: "无效的 API Key" },
+ { status: 401 }
+ )
+ }
+
+ const response = NextResponse.next()
+ response.headers.set("X-User-Id", user.id)
+ return response
+}
+
+export const getUserId = async () => {
+ const headersList = await headers()
+ const userId = headersList.get("X-User-Id")
+
+ if (userId) return userId
+
+ const session = await auth()
+
+ return session?.user.id
+}
diff --git a/app/lib/auth.ts b/app/lib/auth.ts
index 1d0b9e2..b715fc7 100644
--- a/app/lib/auth.ts
+++ b/app/lib/auth.ts
@@ -13,6 +13,7 @@ import { generateAvatarUrl } from "./avatar"
const ROLE_DESCRIPTIONS: Record = {
[ROLES.EMPEROR]: "皇帝(网站所有者)",
+ [ROLES.DUKE]: "公爵(超级用户)",
[ROLES.KNIGHT]: "骑士(高级用户)",
[ROLES.CIVILIAN]: "平民(普通用户)",
}
diff --git a/app/lib/permissions.ts b/app/lib/permissions.ts
index 8f1d78b..3628cee 100644
--- a/app/lib/permissions.ts
+++ b/app/lib/permissions.ts
@@ -1,5 +1,6 @@
export const ROLES = {
EMPEROR: 'emperor',
+ DUKE: 'duke',
KNIGHT: 'knight',
CIVILIAN: 'civilian',
} as const;
@@ -11,12 +12,18 @@ export const PERMISSIONS = {
MANAGE_WEBHOOK: 'manage_webhook',
PROMOTE_USER: 'promote_user',
MANAGE_CONFIG: 'manage_config',
+ MANAGE_API_KEY: 'manage_api_key',
} as const;
export type Permission = typeof PERMISSIONS[keyof typeof PERMISSIONS];
export const ROLE_PERMISSIONS: Record = {
[ROLES.EMPEROR]: Object.values(PERMISSIONS),
+ [ROLES.DUKE]: [
+ PERMISSIONS.MANAGE_EMAIL,
+ PERMISSIONS.MANAGE_WEBHOOK,
+ PERMISSIONS.MANAGE_API_KEY,
+ ],
[ROLES.KNIGHT]: [
PERMISSIONS.MANAGE_EMAIL,
PERMISSIONS.MANAGE_WEBHOOK,
diff --git a/app/lib/schema.ts b/app/lib/schema.ts
index ed21e30..bb0cc3e 100644
--- a/app/lib/schema.ts
+++ b/app/lib/schema.ts
@@ -93,6 +93,23 @@ export const userRoles = sqliteTable("user_role", {
pk: primaryKey({ columns: [table.userId, table.roleId] }),
}));
+export const apiKeys = sqliteTable('api_keys', {
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
+ userId: text('user_id').notNull().references(() => users.id),
+ name: text('name').notNull().unique(),
+ key: text('key').notNull().unique(),
+ createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
+ expiresAt: integer('expires_at', { mode: 'timestamp' }),
+ enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
+});
+
+export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
+ user: one(users, {
+ fields: [apiKeys.userId],
+ references: [users.id],
+ }),
+}));
+
export const userRolesRelations = relations(userRoles, ({ one }) => ({
user: one(users, {
fields: [userRoles.userId],
@@ -106,7 +123,8 @@ export const userRolesRelations = relations(userRoles, ({ one }) => ({
export const usersRelations = relations(users, ({ many }) => ({
userRoles: many(userRoles),
-}));
+ apiKeys: many(apiKeys),
+}));
export const rolesRelations = relations(roles, ({ many }) => ({
userRoles: many(userRoles),
diff --git a/drizzle/0009_overconfident_gateway.sql b/drizzle/0009_overconfident_gateway.sql
new file mode 100644
index 0000000..d9d8ad3
--- /dev/null
+++ b/drizzle/0009_overconfident_gateway.sql
@@ -0,0 +1,13 @@
+CREATE TABLE `api_keys` (
+ `id` text PRIMARY KEY NOT NULL,
+ `user_id` text NOT NULL,
+ `name` text NOT NULL,
+ `key` text NOT NULL,
+ `created_at` integer,
+ `expires_at` integer,
+ `last_used_at` integer,
+ `enabled` integer DEFAULT true NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `api_keys_key_unique` ON `api_keys` (`key`);
\ No newline at end of file
diff --git a/drizzle/0010_brief_stellaris.sql b/drizzle/0010_brief_stellaris.sql
new file mode 100644
index 0000000..1507b5d
--- /dev/null
+++ b/drizzle/0010_brief_stellaris.sql
@@ -0,0 +1,2 @@
+CREATE UNIQUE INDEX `api_keys_name_unique` ON `api_keys` (`name`);--> statement-breakpoint
+ALTER TABLE `api_keys` DROP COLUMN `last_used_at`;
\ No newline at end of file
diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json
new file mode 100644
index 0000000..fef1780
--- /dev/null
+++ b/drizzle/meta/0009_snapshot.json
@@ -0,0 +1,608 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "30be4c36-0174-4fc0-ae3a-c1fa786a638d",
+ "prevId": "144530f0-42a9-450d-ab24-a419eb7b4f65",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_userId_user_id_fk": {
+ "name": "account_userId_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_providerAccountId_pk": {
+ "columns": [
+ "provider",
+ "providerAccountId"
+ ],
+ "name": "account_provider_providerAccountId_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "api_keys": {
+ "name": "api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {
+ "api_keys_key_unique": {
+ "name": "api_keys_key_unique",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "api_keys_user_id_user_id_fk": {
+ "name": "api_keys_user_id_user_id_fk",
+ "tableFrom": "api_keys",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "email": {
+ "name": "email",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "email_address_unique": {
+ "name": "email_address_unique",
+ "columns": [
+ "address"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "email_userId_user_id_fk": {
+ "name": "email_userId_user_id_fk",
+ "tableFrom": "email",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "message": {
+ "name": "message",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emailId": {
+ "name": "emailId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from_address": {
+ "name": "from_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "html": {
+ "name": "html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "message_emailId_email_id_fk": {
+ "name": "message_emailId_email_id_fk",
+ "tableFrom": "message",
+ "tableTo": "email",
+ "columnsFrom": [
+ "emailId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "role": {
+ "name": "role",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user_role": {
+ "name": "user_role",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_role_user_id_user_id_fk": {
+ "name": "user_role_user_id_user_id_fk",
+ "tableFrom": "user_role",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_role_role_id_role_id_fk": {
+ "name": "user_role_role_id_role_id_fk",
+ "tableFrom": "user_role",
+ "tableTo": "role",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_role_user_id_role_id_pk": {
+ "columns": [
+ "user_id",
+ "role_id"
+ ],
+ "name": "user_role_user_id_role_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ },
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "webhook": {
+ "name": "webhook",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "webhook_user_id_user_id_fk": {
+ "name": "webhook_user_id_user_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json
new file mode 100644
index 0000000..d8d6150
--- /dev/null
+++ b/drizzle/meta/0010_snapshot.json
@@ -0,0 +1,608 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "75ed0edc-e2f7-4782-b317-e16de23405f8",
+ "prevId": "30be4c36-0174-4fc0-ae3a-c1fa786a638d",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_userId_user_id_fk": {
+ "name": "account_userId_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_providerAccountId_pk": {
+ "columns": [
+ "provider",
+ "providerAccountId"
+ ],
+ "name": "account_provider_providerAccountId_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "api_keys": {
+ "name": "api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {
+ "api_keys_name_unique": {
+ "name": "api_keys_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ },
+ "api_keys_key_unique": {
+ "name": "api_keys_key_unique",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "api_keys_user_id_user_id_fk": {
+ "name": "api_keys_user_id_user_id_fk",
+ "tableFrom": "api_keys",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "email": {
+ "name": "email",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "email_address_unique": {
+ "name": "email_address_unique",
+ "columns": [
+ "address"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "email_userId_user_id_fk": {
+ "name": "email_userId_user_id_fk",
+ "tableFrom": "email",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "message": {
+ "name": "message",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emailId": {
+ "name": "emailId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "from_address": {
+ "name": "from_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "html": {
+ "name": "html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "message_emailId_email_id_fk": {
+ "name": "message_emailId_email_id_fk",
+ "tableFrom": "message",
+ "tableTo": "email",
+ "columnsFrom": [
+ "emailId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "role": {
+ "name": "role",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user_role": {
+ "name": "user_role",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_role_user_id_user_id_fk": {
+ "name": "user_role_user_id_user_id_fk",
+ "tableFrom": "user_role",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_role_role_id_role_id_fk": {
+ "name": "user_role_role_id_role_id_fk",
+ "tableFrom": "user_role",
+ "tableTo": "role",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_role_user_id_role_id_pk": {
+ "columns": [
+ "user_id",
+ "role_id"
+ ],
+ "name": "user_role_user_id_role_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ },
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "webhook": {
+ "name": "webhook",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "webhook_user_id_user_id_fk": {
+ "name": "webhook_user_id_user_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 5d38966..6c9bd31 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -64,6 +64,20 @@
"when": 1736922488093,
"tag": "0008_cute_dormammu",
"breakpoints": true
+ },
+ {
+ "idx": 9,
+ "version": "6",
+ "when": 1739079086122,
+ "tag": "0009_overconfident_gateway",
+ "breakpoints": true
+ },
+ {
+ "idx": 10,
+ "version": "6",
+ "when": 1739157879946,
+ "tag": "0010_brief_stellaris",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index 579f547..2753d6f 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -3,18 +3,28 @@ import { NextResponse } from "next/server"
import { PERMISSIONS } from "@/lib/permissions"
import { checkPermission } from "@/lib/auth"
import { Permission } from "@/lib/permissions"
+import { handleApiKeyAuth } from "@/lib/apiKey"
const API_PERMISSIONS: Record = {
'/api/emails': PERMISSIONS.MANAGE_EMAIL,
'/api/webhook': PERMISSIONS.MANAGE_WEBHOOK,
'/api/roles/promote': PERMISSIONS.PROMOTE_USER,
'/api/config': PERMISSIONS.MANAGE_CONFIG,
+ '/api/api-keys': PERMISSIONS.MANAGE_API_KEY,
}
export async function middleware(request: Request) {
- const session = await auth()
const pathname = new URL(request.url).pathname
+ // API Key 认证
+ request.headers.delete("X-User-Id")
+ const apiKey = request.headers.get("X-API-Key")
+ if (apiKey) {
+ return handleApiKeyAuth(apiKey, pathname)
+ }
+
+ // Session 认证
+ const session = await auth()
if (!session?.user) {
return NextResponse.json(
{ error: "未授权" },
@@ -25,7 +35,7 @@ export async function middleware(request: Request) {
for (const [route, permission] of Object.entries(API_PERMISSIONS)) {
if (pathname.startsWith(route)) {
const hasAccess = await checkPermission(permission)
-
+
if (!hasAccess) {
return NextResponse.json(
{ error: "权限不足" },
@@ -45,5 +55,7 @@ export const config = {
'/api/webhook/:path*',
'/api/roles/:path*',
'/api/config/:path*',
+ '/api/api-keys/:path*',
+ '/api/admin-contact',
]
}
\ No newline at end of file