From d079f3f9a870c7d09f1dbd97294a5a058ca2d3fd Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 15:03:26 +0800 Subject: [PATCH] Allow users to view admin-owned websites --- src/app/api/me/websites/route.ts | 7 ++- src/app/api/users/[userId]/websites/route.ts | 7 ++- src/app/api/websites/route.ts | 7 ++- src/permissions/website.ts | 8 ++- src/queries/prisma/website.ts | 54 ++++++++++++++++++++ 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/app/api/me/websites/route.ts b/src/app/api/me/websites/route.ts index 9ec39c78d0..97f2ee646e 100644 --- a/src/app/api/me/websites/route.ts +++ b/src/app/api/me/websites/route.ts @@ -2,7 +2,10 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json } from '@/lib/response'; import { pagingParams } from '@/lib/schema'; -import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma'; +import { + getAllUserWebsitesIncludingTeamOwner, + getUserWebsitesIncludingAdminOwned, +} from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ @@ -22,5 +25,5 @@ export async function GET(request: Request) { return json(await getAllUserWebsitesIncludingTeamOwner(auth.user.id, filters)); } - return json(await getUserWebsites(auth.user.id, filters)); + return json(await getUserWebsitesIncludingAdminOwned(auth.user.id, filters)); } diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts index 1107d8e16c..e65dd5cee3 100644 --- a/src/app/api/users/[userId]/websites/route.ts +++ b/src/app/api/users/[userId]/websites/route.ts @@ -2,7 +2,10 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { pagingParams, searchParams } from '@/lib/schema'; -import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website'; +import { + getAllUserWebsitesIncludingTeamOwner, + getUserWebsitesIncludingAdminOwned, +} from '@/queries/prisma/website'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ @@ -29,5 +32,5 @@ export async function GET(request: Request, { params }: { params: Promise<{ user return json(await getAllUserWebsitesIncludingTeamOwner(userId, filters)); } - return json(await getUserWebsites(userId, filters)); + return json(await getUserWebsitesIncludingAdminOwned(userId, filters)); } diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index d54aeac628..2cd6819ec1 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -7,7 +7,10 @@ import { json, unauthorized } from '@/lib/response'; import { pagingParams, searchParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createShare, createWebsite, getWebsiteCount } from '@/queries/prisma'; -import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website'; +import { + getAllUserWebsitesIncludingTeamOwner, + getUserWebsitesIncludingAdminOwned, +} from '@/queries/prisma/website'; const CLOUD_WEBSITE_LIMIT = 3; @@ -32,7 +35,7 @@ export async function GET(request: Request) { return json(await getAllUserWebsitesIncludingTeamOwner(userId, filters)); } - return json(await getUserWebsites(userId, filters)); + return json(await getUserWebsitesIncludingAdminOwned(userId, filters)); } export async function POST(request: Request) { diff --git a/src/permissions/website.ts b/src/permissions/website.ts index cf8e8d82dd..cc45b2267a 100644 --- a/src/permissions/website.ts +++ b/src/permissions/website.ts @@ -2,7 +2,7 @@ import { hasPermission } from '@/lib/auth'; import { PERMISSIONS } from '@/lib/constants'; import { getEntity } from '@/lib/entity'; import type { Auth } from '@/lib/types'; -import { getTeamUser, getWebsite } from '@/queries/prisma'; +import { getTeamUser, getWebsite, isAdminOwnedWebsite } from '@/queries/prisma'; export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) { if (user?.isAdmin) { @@ -27,7 +27,11 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri } if (entity.userId) { - return user.id === entity.userId; + if (user.id === entity.userId) { + return true; + } + + return isAdminOwnedWebsite(websiteId); } if (entity.teamId) { diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index 6c69431e7b..b82463c01b 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -22,6 +22,24 @@ export async function getWebsite(websiteId: string) { return attachShareIdToWebsite(website); } +export async function isAdminOwnedWebsite(websiteId: string) { + const website = await prisma.client.website.findFirst({ + where: { + id: websiteId, + deletedAt: null, + user: { + role: ROLES.admin, + deletedAt: null, + }, + }, + select: { + id: true, + }, + }); + + return !!website; +} + export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: QueryFilters) { const { search } = filters; const { getSearchParameters, pagedQuery } = prisma; @@ -48,6 +66,12 @@ export async function getAllUserWebsitesIncludingTeamOwner(userId: string, filte where: { OR: [ { userId }, + { + user: { + role: ROLES.admin, + deletedAt: null, + }, + }, { team: { deletedAt: null, @@ -91,6 +115,36 @@ export async function getUserWebsites(userId: string, filters?: QueryFilters) { ); } +export async function getUserWebsitesIncludingAdminOwned(userId: string, filters?: QueryFilters) { + return getWebsites( + { + where: { + OR: [ + { userId }, + { + user: { + role: ROLES.admin, + deletedAt: null, + }, + }, + ], + }, + include: { + user: { + select: { + username: true, + id: true, + }, + }, + }, + }, + { + orderBy: 'name', + ...filters, + }, + ); +} + export async function getTeamWebsites(teamId: string, filters?: QueryFilters) { return getWebsites( {