Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/schemas/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const createFileFormSchema = z.object({
});

export const createFileSchema = z.object({
id: noteIdSchema.optional(),
name: z
.string()
.min(3, 'Name must be at least 3 characters')
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/routers/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const fileRouter = createTRPCRouter({
year,
profFirst,
profLast,
id,
...fileData
} = input;

Expand Down Expand Up @@ -144,6 +145,7 @@ export const fileRouter = createTRPCRouter({
.insert(files)
.values({
...fileData,
...(id ? { id } : {}), // Providing id should prevent publicURL from having '' placeholder
authorId: userId,
sectionId: section.id,
publicUrl: '', // This must be filled in with an update call right after the create call
Expand Down
41 changes: 26 additions & 15 deletions src/server/api/routers/storage.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since id originates in a "use client" component and the create procedure accepts it directly, the database's nanoid(20) default is bypassed whenever an ID is provided. That means a client can submit an arbitrary ID instead of the NanoID generated by the UI. This ID should be generated server-side rather than accepted from the client.

Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,32 @@ const createUploadSchema = z.object({
mime: z.literal(NOTE_MIME_TYPE),
});

const ownedFileProcedure = protectedProcedure
.input(getDeleteSchema)
.use(async ({ ctx, input, next }) => {
const file = await ctx.db.query.file.findFirst({
where: (file, { eq }) => eq(file.id, input.objectId),
columns: { authorId: true },
});
if (!file || file.authorId !== ctx.session.user.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You do not own this note.',
function createOwnedFileProcedure(options: { allowMissing: boolean }) {
return protectedProcedure
.input(getDeleteSchema)
.use(async ({ ctx, input, next }) => {
const file = await ctx.db.query.file.findFirst({
where: (file, { eq }) => eq(file.id, input.objectId),
columns: { authorId: true },
});
}
return next();
});

const missing = !file;
const notYours = file && file.authorId !== ctx.session.user.id;

if (notYours || (missing && !options.allowMissing)) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You do not own this note.',
});
}
return next();
});
}

const ownedFileProcedure = createOwnedFileProcedure({ allowMissing: false });
const ownedOrNewFileProcedure = createOwnedFileProcedure({
allowMissing: true,
});

export const storageRouter = createTRPCRouter({
get: publicProcedure.input(getDeleteSchema).query(async ({ input }) => {
Expand All @@ -52,7 +63,7 @@ export const storageRouter = createTRPCRouter({
}
return data;
}),
createUpload: ownedFileProcedure
createUpload: ownedOrNewFileProcedure
.input(createUploadSchema)
.mutation(async ({ input }) => {
const data = await getUploadURL(input.objectId, input.mime);
Expand Down
36 changes: 14 additions & 22 deletions src/systems/notes/forms/CreateNoteForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useMutation } from '@tanstack/react-query';
import { nanoid } from 'nanoid';
import { useRouter, useSearchParams } from 'next/navigation';
import Panel, { PanelSkeleton } from '@nebula-library/components/Panel';
import { useAppForm } from '@src/lib/components/form/form';
Expand Down Expand Up @@ -42,7 +43,6 @@ const defaultValues: FileDetails = {
export default function CreateNoteForm() {
const api = useTRPC();
const createMutation = useMutation(api.file.create.mutationOptions());
const updateMutation = useMutation(api.file.update.mutationOptions());
const uploadFile = useUploadToUploadURL();
const router = useRouter();
const searchParams = useSearchParams();
Expand All @@ -52,10 +52,22 @@ export default function CreateNoteForm() {
defaultValues,
onSubmit: async ({ value, formApi }) => {
const selectedFile = value.file ?? null;
const isFileDirty = !formApi.getFieldMeta('file')?.isDefaultValue;

let fileId: string | undefined;

if (isFileDirty) {
fileId = nanoid(20);
await uploadFile.mutateAsync({
file: selectedFile,
fileName: fileId,
});
}

// Create
return createMutation.mutateAsync(
{
id: fileId,
name: value.name,
description: value.description,
handwritten: value.handwritten,
Expand All @@ -69,27 +81,7 @@ export default function CreateNoteForm() {
},
{
onSuccess: async (newId) => {
const isFileDirty = !formApi.getFieldMeta('file')?.isDefaultValue;
if (!isFileDirty) {
router.push(`/notes/${newId}`);
return;
}

await uploadFile.mutateAsync({
file: selectedFile,
fileName: newId,
});
updateMutation.mutate(
{
id: newId,
name: value.name,
description: value.description,
handwritten: value.handwritten,
},
{
onSuccess: () => router.push(`/notes/${newId}`),
},
);
router.push(`/notes/${newId}`);
},
},
);
Expand Down
Loading