This commit is contained in:
2026-02-06 03:28:53 +08:00
parent 2537b9fe75
commit 8ed9b011f4
7 changed files with 141 additions and 29 deletions

View File

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

View File

@@ -44,7 +44,15 @@
"language2": "语言2",
"enterLanguageName": "请输入语言名称",
"edit": "编辑",
"delete": "删除"
"delete": "删除",
"permissionDenied": "您没有权限执行此操作",
"error": {
"update": "您没有权限更新此项目",
"delete": "您没有权限删除此项目",
"add": "您没有权限向此文件夹添加项目",
"rename": "您没有权限重命名此文件夹",
"deleteFolder": "您没有权限删除此文件夹"
}
},
"home": {
"title": "学语言",

View File

@@ -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<TSharedPair[]>([]);
const [loading, setLoading] = useState(true);
const [openAddModal, setAddModal] = useState(false);
@@ -81,12 +81,14 @@ export function InFolder({ folderId }: { folderId: number; }) {
>
{t("memorize")}
</GreenButton>
<IconButton
onClick={() => {
setAddModal(true);
}}
icon={<Plus size={18} className="text-gray-700" />}
/>
{!isReadOnly && (
<IconButton
onClick={() => {
setAddModal(true);
}}
icon={<Plus size={18} className="text-gray-700" />}
/>
)}
</div>
</div>
</div>
@@ -113,6 +115,7 @@ export function InFolder({ folderId }: { folderId: number; }) {
<TextPairCard
key={textPair.id}
textPair={textPair}
isReadOnly={isReadOnly}
onDel={() => {
actionDeletePairById(textPair.id)
.then(result => {

View File

@@ -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({
</div>
<div className="flex items-center gap-1 opacity-50 group-hover:opacity-100 transition-opacity">
<button
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
onClick={() => setOpenUpdateModal(true)}
title={t("edit")}
>
<Edit size={14} />
</button>
<button
className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
onClick={onDel}
title={t("delete")}
>
<Trash2 size={14} />
</button>
{!isReadOnly && (
<>
<button
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
onClick={() => setOpenUpdateModal(true)}
title={t("edit")}
>
<Edit size={14} />
</button>
<button
className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
onClick={onDel}
title={t("delete")}
>
<Trash2 size={14} />
</button>
</>
)}
</div>
</div>
<div className="text-gray-900 grid grid-cols-2 gap-4 w-3/4">

View File

@@ -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 <p>{t("unauthorized")}</p>;
}
return <InFolder folderId={Number(folder_id)} />;
// 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 <InFolder folderId={Number(folder_id)} isReadOnly={isReadOnly} />;
}

View File

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

View File

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