diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6c445088..5ac5cb26 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Install Dependencies diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 4e7f800a..1bc5811a 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -20,7 +20,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' registry-url: 'https://registry.npmjs.org' cache: 'pnpm' diff --git a/app/api/emails/[id]/[messageId]/route.ts b/app/api/emails/[id]/[messageId]/route.ts index 8a949c2a..b23dc502 100644 --- a/app/api/emails/[id]/[messageId]/route.ts +++ b/app/api/emails/[id]/[messageId]/route.ts @@ -3,6 +3,7 @@ import { createDb } from "@/lib/db" import { messages, emails } from "@/lib/schema" import { and, eq } from "drizzle-orm" import { getUserId } from "@/lib/apiKey" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" export async function DELETE( @@ -10,15 +11,21 @@ export async function DELETE( { params }: { params: Promise<{ id: string; messageId: string }> } ) { const userId = await getUserId() + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } try { const db = createDb() const { id, messageId } = await params + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and( - eq(emails.id, id), - eq(emails.userId, userId!) - ) + where: canAccessAll + ? eq(emails.id, id) + : and( + eq(emails.id, id), + eq(emails.userId, userId) + ) }) if (!email) { @@ -60,12 +67,18 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: const { id, messageId } = await params const db = createDb() const userId = await getUserId() + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and( - eq(emails.id, id), - eq(emails.userId, userId!) - ) + where: canAccessAll + ? eq(emails.id, id) + : and( + eq(emails.id, id), + eq(emails.userId, userId) + ) }) if (!email) { @@ -109,4 +122,4 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: { status: 500 } ) } -} \ No newline at end of file +} diff --git a/app/api/emails/[id]/messages/[messageId]/share/[shareId]/route.ts b/app/api/emails/[id]/messages/[messageId]/share/[shareId]/route.ts index ab0e0cd1..2d8c30eb 100644 --- a/app/api/emails/[id]/messages/[messageId]/share/[shareId]/route.ts +++ b/app/api/emails/[id]/messages/[messageId]/share/[shareId]/route.ts @@ -3,6 +3,7 @@ import { messageShares, messages, emails } from "@/lib/schema" import { eq, and } from "drizzle-orm" import { NextResponse } from "next/server" import { getUserId } from "@/lib/apiKey" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" @@ -20,9 +21,11 @@ export async function DELETE( const db = createDb() try { - // 验证邮箱所有权 + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and(eq(emails.id, emailId), eq(emails.userId, userId)) + where: canAccessAll + ? eq(emails.id, emailId) + : and(eq(emails.id, emailId), eq(emails.userId, userId)) }) if (!email) { @@ -52,4 +55,3 @@ export async function DELETE( ) } } - diff --git a/app/api/emails/[id]/messages/[messageId]/share/route.ts b/app/api/emails/[id]/messages/[messageId]/share/route.ts index f0c8d06d..0361a524 100644 --- a/app/api/emails/[id]/messages/[messageId]/share/route.ts +++ b/app/api/emails/[id]/messages/[messageId]/share/route.ts @@ -4,6 +4,7 @@ import { eq, and } from "drizzle-orm" import { NextResponse } from "next/server" import { getUserId } from "@/lib/apiKey" import { nanoid } from "nanoid" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" @@ -21,9 +22,11 @@ export async function GET( const db = createDb() try { - // 验证邮箱所有权 + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and(eq(emails.id, emailId), eq(emails.userId, userId)) + where: canAccessAll + ? eq(emails.id, emailId) + : and(eq(emails.id, emailId), eq(emails.userId, userId)) }) if (!email) { @@ -69,9 +72,11 @@ export async function POST( const db = createDb() try { - // 验证邮箱所有权 + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and(eq(emails.id, emailId), eq(emails.userId, userId)) + where: canAccessAll + ? eq(emails.id, emailId) + : and(eq(emails.id, emailId), eq(emails.userId, userId)) }) if (!email) { @@ -116,4 +121,3 @@ export async function POST( ) } } - diff --git a/app/api/emails/[id]/route.ts b/app/api/emails/[id]/route.ts index 0725745c..54fe5cd6 100644 --- a/app/api/emails/[id]/route.ts +++ b/app/api/emails/[id]/route.ts @@ -5,6 +5,7 @@ import { eq, and, lt, or, sql, ne, isNull } from "drizzle-orm" import { encodeCursor, decodeCursor } from "@/lib/cursor" import { getUserId } from "@/lib/apiKey" import { checkBasicSendPermission } from "@/lib/send-permissions" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" @@ -13,15 +14,21 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { const userId = await getUserId() + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } try { const db = createDb() const { id } = await params + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and( - eq(emails.id, id), - eq(emails.userId, userId!) - ) + where: canAccessAll + ? eq(emails.id, id) + : and( + eq(emails.id, id), + eq(emails.userId, userId) + ) }) if (!email) { @@ -61,8 +68,12 @@ export async function GET( const { id } = await params const userId = await getUserId() + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } + if (messageType === 'sent') { - const permissionResult = await checkBasicSendPermission(userId!) + const permissionResult = await checkBasicSendPermission(userId) if (!permissionResult.canSend) { return NextResponse.json( { error: permissionResult.error || "您没有查看发送邮件的权限" }, @@ -71,11 +82,14 @@ export async function GET( } } + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and( - eq(emails.id, id), - eq(emails.userId, userId!) - ) + where: canAccessAll + ? eq(emails.id, id) + : and( + eq(emails.id, id), + eq(emails.userId, userId) + ) }) if (!email) { @@ -159,4 +173,4 @@ export async function GET( { status: 500 } ) } -} \ No newline at end of file +} diff --git a/app/api/emails/[id]/send/route.ts b/app/api/emails/[id]/send/route.ts index 388d5420..baf2f7bf 100644 --- a/app/api/emails/[id]/send/route.ts +++ b/app/api/emails/[id]/send/route.ts @@ -5,6 +5,7 @@ import { emails, messages } from "@/lib/schema" import { eq } from "drizzle-orm" import { getRequestContext } from "@cloudflare/next-on-pages" import { checkSendPermission } from "@/lib/send-permissions" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" @@ -90,7 +91,8 @@ export async function POST( ) } - if (email.userId !== userId) { + const canAccessAll = await canUserAccessAllEmails(userId) + if (!canAccessAll && email.userId !== userId) { return NextResponse.json( { error: "无权访问此邮箱" }, { status: 403 } @@ -131,4 +133,4 @@ export async function POST( { status: 500 } ) } -} \ No newline at end of file +} diff --git a/app/api/emails/[id]/share/[shareId]/route.ts b/app/api/emails/[id]/share/[shareId]/route.ts index 52078dd1..17bd9953 100644 --- a/app/api/emails/[id]/share/[shareId]/route.ts +++ b/app/api/emails/[id]/share/[shareId]/route.ts @@ -3,6 +3,7 @@ import { emailShares, emails } from "@/lib/schema" import { eq, and } from "drizzle-orm" import { NextResponse } from "next/server" import { getUserId } from "@/lib/apiKey" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" @@ -20,9 +21,11 @@ export async function DELETE( const db = createDb() try { - // 验证邮箱所有权 + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and(eq(emails.id, emailId), eq(emails.userId, userId)) + where: canAccessAll + ? eq(emails.id, emailId) + : and(eq(emails.id, emailId), eq(emails.userId, userId)) }) if (!email) { @@ -43,4 +46,3 @@ export async function DELETE( ) } } - diff --git a/app/api/emails/[id]/share/route.ts b/app/api/emails/[id]/share/route.ts index 74c48cf1..d575bc33 100644 --- a/app/api/emails/[id]/share/route.ts +++ b/app/api/emails/[id]/share/route.ts @@ -4,6 +4,7 @@ import { eq, and } from "drizzle-orm" import { NextResponse } from "next/server" import { getUserId } from "@/lib/apiKey" import { nanoid } from "nanoid" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" @@ -21,9 +22,11 @@ export async function GET( const db = createDb() try { - // 验证邮箱所有权 + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and(eq(emails.id, emailId), eq(emails.userId, userId)) + where: canAccessAll + ? eq(emails.id, emailId) + : and(eq(emails.id, emailId), eq(emails.userId, userId)) }) if (!email) { @@ -60,9 +63,11 @@ export async function POST( const db = createDb() try { - // 验证邮箱所有权 + const canAccessAll = await canUserAccessAllEmails(userId) const email = await db.query.emails.findFirst({ - where: and(eq(emails.id, emailId), eq(emails.userId, userId)) + where: canAccessAll + ? eq(emails.id, emailId) + : and(eq(emails.id, emailId), eq(emails.userId, userId)) }) if (!email) { @@ -98,4 +103,3 @@ export async function POST( ) } } - diff --git a/app/api/emails/route.ts b/app/api/emails/route.ts index 6686dc3e..d1bf4eae 100644 --- a/app/api/emails/route.ts +++ b/app/api/emails/route.ts @@ -4,6 +4,7 @@ import { NextResponse } from "next/server" import { emails } from "@/lib/schema" import { encodeCursor, decodeCursor } from "@/lib/cursor" import { getUserId } from "@/lib/apiKey" +import { canUserAccessAllEmails } from "@/lib/email-access" export const runtime = "edge" @@ -11,6 +12,9 @@ const PAGE_SIZE = 20 export async function GET(request: Request) { const userId = await getUserId() + if (!userId) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } const { searchParams } = new URL(request.url) const cursor = searchParams.get('cursor') @@ -18,10 +22,13 @@ export async function GET(request: Request) { const db = createDb() try { - const baseConditions = and( - eq(emails.userId, userId!), - gt(emails.expiresAt, new Date()) - ) + const canAccessAll = await canUserAccessAllEmails(userId) + const baseConditions = canAccessAll + ? gt(emails.expiresAt, new Date()) + : and( + eq(emails.userId, userId), + gt(emails.expiresAt, new Date()) + ) const totalResult = await db.select({ count: sql`count(*)` }) .from(emails) @@ -73,4 +80,4 @@ export async function GET(request: Request) { { status: 500 } ) } -} \ No newline at end of file +} diff --git a/app/lib/email-access.ts b/app/lib/email-access.ts new file mode 100644 index 00000000..de579962 --- /dev/null +++ b/app/lib/email-access.ts @@ -0,0 +1,15 @@ +import { eq } from "drizzle-orm" +import { createDb } from "./db" +import { roles, userRoles } from "./schema" +import { canAccessAllEmails, type Role } from "./permissions" + +export async function canUserAccessAllEmails(userId: string) { + const db = createDb() + const currentUserRole = await db.select({ roleName: roles.name }) + .from(userRoles) + .innerJoin(roles, eq(userRoles.roleId, roles.id)) + .where(eq(userRoles.userId, userId)) + .get() + + return canAccessAllEmails(currentUserRole?.roleName as Role | null | undefined) +} diff --git a/app/lib/permissions.ts b/app/lib/permissions.ts index 3628cee7..69d8b049 100644 --- a/app/lib/permissions.ts +++ b/app/lib/permissions.ts @@ -33,4 +33,8 @@ export const ROLE_PERMISSIONS: Record = { export function hasPermission(userRoles: Role[], permission: Permission): boolean { return userRoles.some(role => ROLE_PERMISSIONS[role]?.includes(permission)); -} \ No newline at end of file +} + +export function canAccessAllEmails(role: Role | null | undefined): boolean { + return role === ROLES.EMPEROR; +} diff --git a/tests/email-access.test.ts b/tests/email-access.test.ts new file mode 100644 index 00000000..ae6f447e --- /dev/null +++ b/tests/email-access.test.ts @@ -0,0 +1,11 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { canAccessAllEmails, ROLES } from "../app/lib/permissions" + +test("only emperor can access all emails", () => { + assert.equal(canAccessAllEmails(ROLES.EMPEROR), true) + assert.equal(canAccessAllEmails(ROLES.DUKE), false) + assert.equal(canAccessAllEmails(ROLES.KNIGHT), false) + assert.equal(canAccessAllEmails(ROLES.CIVILIAN), false) + assert.equal(canAccessAllEmails(null), false) +})