fix: improve action security

This commit is contained in:
Kirill Siukhin 2025-07-16 01:00:29 +05:00
parent ed28943b54
commit b3d7eb99eb
4 changed files with 69 additions and 5 deletions

View File

@ -4,9 +4,28 @@ import { revalidatePath } from "next/cache";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { blocksTable, IBlock } from "@/lib/db/schema"; import { blocksTable, IBlock } from "@/lib/db/schema";
import { requireAuth } from "./auth";
import { assertNoteOwner } from "./notes";
export async function assertBlockOwner(blockId: string): Promise<boolean> {
const user = await requireAuth();
const block = await db.query.blocksTable.findFirst({
where: eq(blocksTable.id, blockId),
with: { note: true },
});
if (!block) {
return false;
}
return block.note.authorId === user.id;
}
export async function createBlock(formData: FormData) { export async function createBlock(formData: FormData) {
const noteId = formData.get("noteId") as string; const noteId = formData.get("noteId") as string;
const isAllowed = await assertNoteOwner(noteId);
if (!isAllowed) return;
const blocks = await getBlocks(noteId); const blocks = await getBlocks(noteId);
const lastBlock = blocks.pop(); const lastBlock = blocks.pop();
const order = lastBlock === undefined ? 1 : lastBlock.order + 1; const order = lastBlock === undefined ? 1 : lastBlock.order + 1;
@ -17,6 +36,9 @@ export async function createBlock(formData: FormData) {
} }
async function getBlock(blockId: string): Promise<IBlock | null> { async function getBlock(blockId: string): Promise<IBlock | null> {
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return null;
const blocks = await db const blocks = await db
.select() .select()
.from(blocksTable) .from(blocksTable)
@ -29,6 +51,8 @@ async function getBlock(blockId: string): Promise<IBlock | null> {
} }
export async function getBlocks(noteId: string): Promise<IBlock[]> { export async function getBlocks(noteId: string): Promise<IBlock[]> {
const isAllowed = await assertNoteOwner(noteId);
if (!isAllowed) return [];
return db return db
.select() .select()
.from(blocksTable) .from(blocksTable)
@ -38,6 +62,8 @@ export async function getBlocks(noteId: string): Promise<IBlock[]> {
export async function deleteBlock(formData: FormData) { export async function deleteBlock(formData: FormData) {
const blockId = formData.get("blockId") as string; const blockId = formData.get("blockId") as string;
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
await db await db
.delete(blocksTable) .delete(blocksTable)
.where(eq(blocksTable.id, blockId)); .where(eq(blocksTable.id, blockId));
@ -47,6 +73,8 @@ export async function deleteBlock(formData: FormData) {
export async function changeLock(formData: FormData) { export async function changeLock(formData: FormData) {
const blockId = formData.get("blockId") as string; const blockId = formData.get("blockId") as string;
const isLocked = formData.get("isLocked") === null ? false : true; const isLocked = formData.get("isLocked") === null ? false : true;
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
await db await db
.update(blocksTable) .update(blocksTable)
.set({ isLocked }) .set({ isLocked })
@ -56,6 +84,8 @@ export async function changeLock(formData: FormData) {
export async function addLine(formData: FormData) { export async function addLine(formData: FormData) {
const blockId = formData.get("blockId") as string; const blockId = formData.get("blockId") as string;
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
const blocks = await db const blocks = await db
.select() .select()
.from(blocksTable) .from(blocksTable)
@ -74,6 +104,8 @@ export async function addLine(formData: FormData) {
export async function deleteLine(formData: FormData) { export async function deleteLine(formData: FormData) {
const blockId = formData.get("blockId") as string; const blockId = formData.get("blockId") as string;
const block = await getBlock(blockId); const block = await getBlock(blockId);
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
if (!block) { if (!block) {
return; return;
} }
@ -86,6 +118,8 @@ export async function deleteLine(formData: FormData) {
export async function moveUp(formData: FormData) { export async function moveUp(formData: FormData) {
const blockId = formData.get("blockId") as string; const blockId = formData.get("blockId") as string;
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
const block = await getBlock(blockId); const block = await getBlock(blockId);
if (!block) return; if (!block) return;
@ -112,6 +146,8 @@ export async function moveUp(formData: FormData) {
export async function moveDown(formData: FormData) { export async function moveDown(formData: FormData) {
const blockId = formData.get("blockId") as string; const blockId = formData.get("blockId") as string;
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
const block = await getBlock(blockId); const block = await getBlock(blockId);
if (!block) return; if (!block) return;
@ -137,7 +173,8 @@ export async function moveDown(formData: FormData) {
} }
export async function setLines(blockId: string, lines: string[]) { export async function setLines(blockId: string, lines: string[]) {
console.log(lines) const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
await db await db
.update(blocksTable) .update(blocksTable)
.set({ lines }) .set({ lines })
@ -145,6 +182,8 @@ export async function setLines(blockId: string, lines: string[]) {
} }
export async function setTag(blockId: string, tag: string) { export async function setTag(blockId: string, tag: string) {
const isAllowed = await assertBlockOwner(blockId);
if (!isAllowed) return;
await db await db
.update(blocksTable) .update(blocksTable)
.set({ tag }) .set({ tag })

View File

@ -7,6 +7,19 @@ import { blocksTable, INote, notesTable, usersTable } from "@/lib/db/schema";
import { requireAuth } from "./auth"; import { requireAuth } from "./auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
export async function assertNoteOwner(noteId: string): Promise<boolean> {
const user = await requireAuth();
const note = await db.query.notesTable.findFirst({
where: eq(notesTable.id, noteId),
});
if (!note) {
return false;
}
return note.authorId === user.id;
}
export async function createNote() { export async function createNote() {
const user = await requireAuth(); const user = await requireAuth();
const result = await db const result = await db
@ -47,6 +60,7 @@ export async function deleteNote(formData: FormData) {
} }
export async function setTitle(noteId: string, title: string) { export async function setTitle(noteId: string, title: string) {
if (title === "") return;
await db await db
.update(notesTable) .update(notesTable)
.set({ title }) .set({ title })

View File

@ -1,6 +1,6 @@
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import { ArrowDown, ArrowUp, LockOpen, Lock, Minus, Plus, X } from "lucide-react"; import { ArrowDown, ArrowUp, LockOpen, Lock, Minus, Plus, X } from "lucide-react";
import { addLine, changeLock, deleteBlock, deleteLine, moveDown, moveUp, setLines } from "@/app/actions/blocks"; import { addLine, changeLock, deleteBlock, deleteLine, moveDown, moveUp, setLines, setTag } from "@/app/actions/blocks";
import { useDebounce } from "@/lib/hooks/useDebounce"; import { useDebounce } from "@/lib/hooks/useDebounce";
import { IBlock } from "@/lib/db/schema"; import { IBlock } from "@/lib/db/schema";
import IconOnlyButton from "../ui/IconOnlyButton"; import IconOnlyButton from "../ui/IconOnlyButton";
@ -8,11 +8,16 @@ import LineInput from "./LineInput";
export default function Block({ block }: { block: IBlock }) { export default function Block({ block }: { block: IBlock }) {
const [lines, setLinesState] = useState(block.lines); const [lines, setLinesState] = useState(block.lines);
const [tag, setTagState] = useState(block.tag);
useDebounce(() => { useDebounce(() => {
setLines(block.id, lines); setLines(block.id, lines);
}, [lines]); }, [lines]);
useDebounce(() => {
setTag(block.id, tag);
}, [tag]);
const lineChangeHandler = (i: number, e: ChangeEvent<HTMLInputElement>) => { const lineChangeHandler = (i: number, e: ChangeEvent<HTMLInputElement>) => {
const newLines = [...lines]; const newLines = [...lines];
newLines[i] = e.target.value; newLines[i] = e.target.value;
@ -26,11 +31,17 @@ export default function Block({ block }: { block: IBlock }) {
type="text" type="text"
placeholder="enter tag..." placeholder="enter tag..."
className="w-full focus:outline-none" className="w-full focus:outline-none"
defaultValue={block.tag} onChange={(e) => setTagState(e.target.value)}
value={tag}
disabled={block.isLocked} disabled={block.isLocked}
/> />
{block.lines.map((line, i) => ( {block.lines.map((line, i) => (
<LineInput key={i} defaultValue={line} disabled={block.isLocked} onChange={(e) => lineChangeHandler(i, e)} /> <LineInput
key={i}
defaultValue={line}
disabled={block.isLocked}
onChange={(e) => lineChangeHandler(i, e)}
/>
))} ))}
<div className="flex items-center mx-2 mt-2"> <div className="flex items-center mx-2 mt-2">
<div className="flex gap-1 mr-4"> <div className="flex gap-1 mr-4">

View File

@ -1,4 +1,4 @@
import { drizzle } from 'drizzle-orm/node-postgres'; import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from "./schema"; import * as schema from "./schema";
export const db = drizzle<typeof schema>(process.env.DATABASE_URL!); export const db = drizzle(process.env.DATABASE_URL!, { schema });