diff --git a/messages/en-US.json b/messages/en-US.json index 83ae7eb..faf1cf8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -44,7 +44,15 @@ "language2": "Locale 2", "enterLanguageName": "Please enter language name", "edit": "Edit", - "delete": "Delete" + "delete": "Delete", + "permissionDenied": "You do not have permission to perform this action", + "error": { + "update": "You do not have permission to update this item.", + "delete": "You do not have permission to delete this item.", + "add": "You do not have permission to add items to this folder.", + "rename": "You do not have permission to rename this folder.", + "deleteFolder": "You do not have permission to delete this folder." + } }, "home": { "title": "Learn Languages", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 68172f2..427f1e7 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -44,7 +44,15 @@ "language2": "语言2", "enterLanguageName": "请输入语言名称", "edit": "编辑", - "delete": "删除" + "delete": "删除", + "permissionDenied": "您没有权限执行此操作", + "error": { + "update": "您没有权限更新此项目", + "delete": "您没有权限删除此项目", + "add": "您没有权限向此文件夹添加项目", + "rename": "您没有权限重命名此文件夹", + "deleteFolder": "您没有权限删除此文件夹" + } }, "home": { "title": "学语言", diff --git a/src/app/folders/[folder_id]/InFolder.tsx b/src/app/folders/[folder_id]/InFolder.tsx index fff6276..cb045cf 100644 --- a/src/app/folders/[folder_id]/InFolder.tsx +++ b/src/app/folders/[folder_id]/InFolder.tsx @@ -15,7 +15,7 @@ import { TSharedPair } from "@/shared/folder-type"; import { toast } from "sonner"; -export function InFolder({ folderId }: { folderId: number; }) { +export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnly: boolean; }) { const [textPairs, setTextPairs] = useState([]); const [loading, setLoading] = useState(true); const [openAddModal, setAddModal] = useState(false); @@ -81,12 +81,14 @@ export function InFolder({ folderId }: { folderId: number; }) { > {t("memorize")} - { - setAddModal(true); - }} - icon={} - /> + {!isReadOnly && ( + { + setAddModal(true); + }} + icon={} + /> + )} @@ -113,6 +115,7 @@ export function InFolder({ folderId }: { folderId: number; }) { { actionDeletePairById(textPair.id) .then(result => { diff --git a/src/app/folders/[folder_id]/TextPairCard.tsx b/src/app/folders/[folder_id]/TextPairCard.tsx index b9c699c..ea2cf77 100644 --- a/src/app/folders/[folder_id]/TextPairCard.tsx +++ b/src/app/folders/[folder_id]/TextPairCard.tsx @@ -9,12 +9,14 @@ import { toast } from "sonner"; interface TextPairCardProps { textPair: TSharedPair; + isReadOnly: boolean; onDel: () => void; refreshTextPairs: () => void; } export function TextPairCard({ textPair, + isReadOnly, onDel, refreshTextPairs, }: TextPairCardProps) { @@ -35,20 +37,24 @@ export function TextPairCard({
- - + {!isReadOnly && ( + <> + + + + )}
diff --git a/src/app/folders/[folder_id]/page.tsx b/src/app/folders/[folder_id]/page.tsx index d8b4a16..271c4d9 100644 --- a/src/app/folders/[folder_id]/page.tsx +++ b/src/app/folders/[folder_id]/page.tsx @@ -4,6 +4,7 @@ import { InFolder } from "./InFolder"; import { auth } from "@/auth"; import { headers } from "next/headers"; import { actionGetUserIdByFolderId } from "@/modules/folder/folder-aciton"; + export default async function FoldersPage({ params, }: { @@ -16,9 +17,11 @@ export default async function FoldersPage({ if (!folder_id) { redirect("/folders"); } - if (!session) redirect(`/auth?redirect=/folders/${folder_id}`); - if ((await actionGetUserIdByFolderId(Number(folder_id))).data !== session.user.id) { - return

{t("unauthorized")}

; - } - return ; + + // Allow non-authenticated users to view folders (read-only mode) + const folderUserId = (await actionGetUserIdByFolderId(Number(folder_id))).data; + const isOwner = session?.user?.id === folderUserId; + const isReadOnly = !isOwner; + + return ; } diff --git a/src/modules/folder/folder-aciton.ts b/src/modules/folder/folder-aciton.ts index 5b001a0..ec633d8 100644 --- a/src/modules/folder/folder-aciton.ts +++ b/src/modules/folder/folder-aciton.ts @@ -1,12 +1,39 @@ "use server"; +import { auth } from "@/auth"; +import { headers } from "next/headers"; import { ValidateError } from "@/lib/errors"; import { ActionInputCreatePair, ActionInputUpdatePairById, ActionOutputGetFoldersWithTotalPairsByUserId, validateActionInputCreatePair, validateActionInputUpdatePairById } from "./folder-action-dto"; -import { repoCreateFolder, repoCreatePair, repoDeleteFolderById, repoDeletePairById, repoGetFoldersByUserId, repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetUserIdByFolderId, repoRenameFolderById, repoUpdatePairById } from "./folder-repository"; +import { repoCreateFolder, repoCreatePair, repoDeleteFolderById, repoDeletePairById, repoGetFolderIdByPairId, repoGetFoldersByUserId, repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetUserIdByFolderId, repoRenameFolderById, repoUpdatePairById } from "./folder-repository"; import { validate } from "@/utils/validate"; import z from "zod"; import { LENGTH_MAX_FOLDER_NAME, LENGTH_MIN_FOLDER_NAME } from "@/shared/constant"; +/** + * Helper function to check if the current user is the owner of a folder + */ +async function checkFolderOwnership(folderId: number): Promise { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session?.user?.id) return false; + + const folderOwnerId = await repoGetUserIdByFolderId(folderId); + return folderOwnerId === session.user.id; +} + +/** + * Helper function to check if the current user is the owner of a pair's folder + */ +async function checkPairOwnership(pairId: number): Promise { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session?.user?.id) return false; + + const folderId = await repoGetFolderIdByPairId(pairId); + if (!folderId) return false; + + const folderOwnerId = await repoGetUserIdByFolderId(folderId); + return folderOwnerId === session.user.id; +} + export async function actionGetPairsByFolderId(folderId: number) { try { return { @@ -25,6 +52,15 @@ export async function actionGetPairsByFolderId(folderId: number) { export async function actionUpdatePairById(id: number, dto: ActionInputUpdatePairById) { try { + // Check ownership + const isOwner = await checkPairOwnership(id); + if (!isOwner) { + return { + success: false, + message: 'You do not have permission to update this item.', + }; + } + const validatedDto = validateActionInputUpdatePairById(dto); await repoUpdatePairById(id, validatedDto); return { @@ -58,6 +94,15 @@ export async function actionGetUserIdByFolderId(folderId: number) { export async function actionDeleteFolderById(folderId: number) { try { + // Check ownership + const isOwner = await checkFolderOwnership(folderId); + if (!isOwner) { + return { + success: false, + message: 'You do not have permission to delete this folder.', + }; + } + await repoDeleteFolderById(folderId); return { success: true, @@ -74,6 +119,15 @@ export async function actionDeleteFolderById(folderId: number) { export async function actionDeletePairById(id: number) { try { + // Check ownership + const isOwner = await checkPairOwnership(id); + if (!isOwner) { + return { + success: false, + message: 'You do not have permission to delete this item.', + }; + } + await repoDeletePairById(id); return { success: true, @@ -122,6 +176,15 @@ export async function actionGetFoldersByUserId(userId: string) { export async function actionCreatePair(dto: ActionInputCreatePair) { try { + // Check ownership + const isOwner = await checkFolderOwnership(dto.folderId); + if (!isOwner) { + return { + success: false, + message: 'You do not have permission to add items to this folder.', + }; + } + const validatedDto = validateActionInputCreatePair(dto); await repoCreatePair(validatedDto); return { @@ -175,6 +238,15 @@ export async function actionCreateFolder(userId: string, folderName: string) { export async function actionRenameFolderById(id: number, newName: string) { try { + // Check ownership + const isOwner = await checkFolderOwnership(id); + if (!isOwner) { + return { + success: false, + message: 'You do not have permission to rename this folder.', + }; + } + const validatedNewName = validate( newName, z.string() diff --git a/src/modules/folder/folder-repository.ts b/src/modules/folder/folder-repository.ts index 97b3ac9..6d8b857 100644 --- a/src/modules/folder/folder-repository.ts +++ b/src/modules/folder/folder-repository.ts @@ -122,3 +122,15 @@ export async function repoGetUserIdByFolderId(id: number) { }); return folder?.userId; } + +export async function repoGetFolderIdByPairId(pairId: number) { + const pair = await prisma.pair.findUnique({ + where: { + id: pairId, + }, + select: { + folderId: true, + }, + }); + return pair?.folderId; +}