From b412db92e5d3542160b786d0c513c8826e3f07cd Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 26 Dec 2025 15:04:18 -0500 Subject: [PATCH] validation --- src/components/blog/PostForm.tsx | 5 +- src/server/api/routers/auth.ts | 38 +++---- src/server/api/routers/database.ts | 174 ++++++++++++----------------- src/server/api/schemas/database.ts | 99 ++++++++++++++++ src/types/user.ts | 1 + 5 files changed, 191 insertions(+), 126 deletions(-) diff --git a/src/components/blog/PostForm.tsx b/src/components/blog/PostForm.tsx index d0737e0..5dd67ba 100644 --- a/src/components/blog/PostForm.tsx +++ b/src/components/blog/PostForm.tsx @@ -47,7 +47,7 @@ export default function PostForm(props: PostFormProps) { const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(""); const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false); - const [isInitialLoad, setIsInitialLoad] = createSignal(props.mode === "edit"); + const [isInitialLoad, setIsInitialLoad] = createSignal(true); const [initialBody, setInitialBody] = createSignal( props.initialData?.body ); @@ -57,9 +57,10 @@ export default function PostForm(props: PostFormProps) { ); // Mark initial load as complete after data is loaded (for edit mode) + // Use setTimeout to ensure this runs after all signals are initialized createEffect(() => { if (props.mode === "edit" && props.initialData) { - setIsInitialLoad(false); + setTimeout(() => setIsInitialLoad(false), 0); } }); diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 598ca65..3beeee1 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -6,7 +6,7 @@ import { env } from "~/env/server"; import { ConnectionFactory, hashPassword, checkPassword } from "~/server/utils"; import { SignJWT, jwtVerify } from "jose"; import { setCookie, getCookie } from "vinxi/http"; -import type { User } from "~/types/user"; +import type { User } from "~/db/types"; import { fetchWithTimeout, checkResponse, @@ -15,6 +15,12 @@ import { TimeoutError, APIError } from "~/server/fetch-utils"; +import { + registerUserSchema, + loginUserSchema, + resetPasswordSchema, + requestPasswordResetSchema +} from "../schemas/user"; async function createJWT( userId: string, @@ -501,16 +507,11 @@ export const authRouter = createTRPCRouter({ }), emailRegistration: publicProcedure - .input( - z.object({ - email: z.string().email(), - password: z.string().min(8), - passwordConfirmation: z.string().min(8) - }) - ) + .input(registerUserSchema) .mutation(async ({ input, ctx }) => { const { email, password, passwordConfirmation } = input; + // Schema already validates password match, but double check if (password !== passwordConfirmation) { throw new TRPCError({ code: "BAD_REQUEST", @@ -549,13 +550,7 @@ export const authRouter = createTRPCRouter({ }), emailPasswordLogin: publicProcedure - .input( - z.object({ - email: z.string().email(), - password: z.string(), - rememberMe: z.boolean().optional() - }) - ) + .input(loginUserSchema) .mutation(async ({ input, ctx }) => { const { email, password, rememberMe } = input; @@ -746,7 +741,7 @@ export const authRouter = createTRPCRouter({ }), requestPasswordReset: publicProcedure - .input(z.object({ email: z.string().email() })) + .input(requestPasswordResetSchema) .mutation(async ({ input, ctx }) => { const { email } = input; @@ -862,16 +857,11 @@ export const authRouter = createTRPCRouter({ }), resetPassword: publicProcedure - .input( - z.object({ - token: z.string(), - newPassword: z.string().min(8), - newPasswordConfirmation: z.string().min(8) - }) - ) + .input(resetPasswordSchema) .mutation(async ({ input, ctx }) => { const { token, newPassword, newPasswordConfirmation } = input; + // Schema already validates password match, but double check if (newPassword !== newPasswordConfirmation) { throw new TRPCError({ code: "BAD_REQUEST", @@ -945,7 +935,7 @@ export const authRouter = createTRPCRouter({ }), resendEmailVerification: publicProcedure - .input(z.object({ email: z.string().email() })) + .input(requestPasswordResetSchema) .mutation(async ({ input, ctx }) => { const { email } = input; diff --git a/src/server/api/routers/database.ts b/src/server/api/routers/database.ts index c613c97..efc639d 100644 --- a/src/server/api/routers/database.ts +++ b/src/server/api/routers/database.ts @@ -8,12 +8,35 @@ import { ConnectionFactory } from "~/server/utils"; import { TRPCError } from "@trpc/server"; import { env } from "~/env/server"; import { cache, withCacheAndStale } from "~/server/cache"; +import type { + Comment, + CommentReaction, + Post, + PostLike, + User, + Tag +} from "~/db/types"; +import { + getCommentReactionsQuerySchema, + toggleCommentReactionMutationSchema, + deleteCommentWithTypeSchema, + getCommentsByPostIdSchema, + getPostByIdSchema, + getPostByTitleSchema, + createPostSchema, + updatePostSchema, + idSchema, + togglePostLikeMutationSchema, + getUserByIdSchema, + updateUserImageSchema, + updateUserEmailSchema +} from "../schemas/database"; const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours export const databaseRouter = createTRPCRouter({ getCommentReactions: publicProcedure - .input(z.object({ commentID: z.string() })) + .input(getCommentReactionsQuerySchema) .query(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -22,7 +45,9 @@ export const databaseRouter = createTRPCRouter({ sql: query, args: [input.commentID] }); - return { commentReactions: results.rows }; + return { + commentReactions: results.rows as unknown as CommentReaction[] + }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -32,13 +57,7 @@ export const databaseRouter = createTRPCRouter({ }), addCommentReaction: publicProcedure - .input( - z.object({ - type: z.string(), - comment_id: z.string(), - user_id: z.string() - }) - ) + .input(toggleCommentReactionMutationSchema) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -57,7 +76,7 @@ export const databaseRouter = createTRPCRouter({ args: [input.comment_id] }); - return { commentReactions: res.rows }; + return { commentReactions: res.rows as unknown as CommentReaction[] }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -67,13 +86,7 @@ export const databaseRouter = createTRPCRouter({ }), removeCommentReaction: publicProcedure - .input( - z.object({ - type: z.string(), - comment_id: z.string(), - user_id: z.string() - }) - ) + .input(toggleCommentReactionMutationSchema) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -92,7 +105,7 @@ export const databaseRouter = createTRPCRouter({ args: [input.comment_id] }); - return { commentReactions: res.rows }; + return { commentReactions: res.rows as unknown as CommentReaction[] }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -121,13 +134,7 @@ export const databaseRouter = createTRPCRouter({ }), deleteComment: protectedProcedure - .input( - z.object({ - commentID: z.number(), - commenterID: z.string(), - deletionType: z.enum(["user", "admin", "database"]) - }) - ) + .input(deleteCommentWithTypeSchema) .mutation(async ({ input, ctx }) => { try { const conn = ConnectionFactory(); @@ -238,7 +245,7 @@ export const databaseRouter = createTRPCRouter({ }), getCommentsByPostId: publicProcedure - .input(z.object({ post_id: z.string() })) + .input(getCommentsByPostIdSchema) .query(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -253,7 +260,7 @@ export const databaseRouter = createTRPCRouter({ sql: query, args: [input.post_id] }); - return { comments: res.rows }; + return { comments: res.rows as unknown as Comment[] }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -263,12 +270,7 @@ export const databaseRouter = createTRPCRouter({ }), getPostById: publicProcedure - .input( - z.object({ - category: z.literal("blog"), - id: z.number() - }) - ) + .input(getPostByIdSchema) .query(async ({ input }) => { return withCacheAndStale( `blog-post-id-${input.id}`, @@ -311,12 +313,7 @@ export const databaseRouter = createTRPCRouter({ }), getPostByTitle: publicProcedure - .input( - z.object({ - category: z.literal("blog"), - title: z.string() - }) - ) + .input(getPostByTitleSchema) .query(async ({ input, ctx }) => { return withCacheAndStale( `blog-post-title-${input.title}`, @@ -510,55 +507,48 @@ export const databaseRouter = createTRPCRouter({ } }), - deletePost: publicProcedure - .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { - try { - const conn = ConnectionFactory(); + deletePost: publicProcedure.input(idSchema).mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); - await conn.execute({ - sql: "DELETE FROM Tag WHERE post_id = ?", - args: [input.id.toString()] - }); + await conn.execute({ + sql: "DELETE FROM Tag WHERE post_id = ?", + args: [input.id.toString()] + }); - await conn.execute({ - sql: "DELETE FROM PostLike WHERE post_id = ?", - args: [input.id.toString()] - }); + await conn.execute({ + sql: "DELETE FROM PostLike WHERE post_id = ?", + args: [input.id.toString()] + }); - await conn.execute({ - sql: "DELETE FROM Comment WHERE post_id = ?", - args: [input.id] - }); + await conn.execute({ + sql: "DELETE FROM Comment WHERE post_id = ?", + args: [input.id] + }); - await conn.execute({ - sql: "DELETE FROM Post WHERE id = ?", - args: [input.id] - }); + await conn.execute({ + sql: "DELETE FROM Post WHERE id = ?", + args: [input.id] + }); - cache.deleteByPrefix("blog-"); + cache.deleteByPrefix("blog-"); - return { success: true }; - } catch (error) { - console.error(error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to delete post" - }); - } - }), + return { success: true }; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to delete post" + }); + } + }), // ============================================================ // Post Likes Routes // ============================================================ addPostLike: publicProcedure - .input( - z.object({ - user_id: z.string(), - post_id: z.string() - }) - ) + .input(togglePostLikeMutationSchema) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -574,7 +564,7 @@ export const databaseRouter = createTRPCRouter({ args: [input.post_id] }); - return { newLikes: res.rows }; + return { newLikes: res.rows as unknown as PostLike[] }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -584,12 +574,7 @@ export const databaseRouter = createTRPCRouter({ }), removePostLike: publicProcedure - .input( - z.object({ - user_id: z.string(), - post_id: z.string() - }) - ) + .input(togglePostLikeMutationSchema) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -608,7 +593,7 @@ export const databaseRouter = createTRPCRouter({ args: [input.post_id] }); - return { newLikes: res.rows }; + return { newLikes: res.rows as unknown as PostLike[] }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -622,7 +607,7 @@ export const databaseRouter = createTRPCRouter({ // ============================================================ getUserById: publicProcedure - .input(z.object({ id: z.string() })) + .input(getUserByIdSchema) .query(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -654,7 +639,7 @@ export const databaseRouter = createTRPCRouter({ }), getUserPublicData: publicProcedure - .input(z.object({ id: z.string() })) + .input(getUserByIdSchema) .query(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -683,7 +668,7 @@ export const databaseRouter = createTRPCRouter({ }), getUserImage: publicProcedure - .input(z.object({ id: z.string() })) + .input(getUserByIdSchema) .query(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -702,12 +687,7 @@ export const databaseRouter = createTRPCRouter({ }), updateUserImage: publicProcedure - .input( - z.object({ - id: z.string(), - imageURL: z.string() - }) - ) + .input(updateUserImageSchema) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); @@ -729,13 +709,7 @@ export const databaseRouter = createTRPCRouter({ }), updateUserEmail: publicProcedure - .input( - z.object({ - id: z.string(), - newEmail: z.string().email(), - oldEmail: z.string().email() - }) - ) + .input(updateUserEmailSchema) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); diff --git a/src/server/api/schemas/database.ts b/src/server/api/schemas/database.ts index 29c72c5..7d2f5b3 100644 --- a/src/server/api/schemas/database.ts +++ b/src/server/api/schemas/database.ts @@ -277,6 +277,85 @@ export const paginationSchema = z.object({ offset: z.number().min(0).default(0) }); +// ============================================================================ +// Additional Database Router Schemas +// ============================================================================ + +/** + * Get post by ID or title + */ +export const getPostByIdSchema = z.object({ + category: z.literal("blog"), + id: z.number() +}); + +export const getPostByTitleSchema = z.object({ + category: z.literal("blog"), + title: z.string() +}); + +/** + * Get comments by post ID + */ +export const getCommentsByPostIdSchema = z.object({ + post_id: z.number() +}); + +/** + * Toggle post like (add/remove) + */ +export const togglePostLikeMutationSchema = z.object({ + user_id: z.string(), + post_id: z.number() +}); + +/** + * Toggle comment reaction (add/remove) + */ +export const toggleCommentReactionMutationSchema = z.object({ + type: reactionTypeSchema, + comment_id: z.number(), + user_id: z.string() +}); + +/** + * Get comment reactions + */ +export const getCommentReactionsQuerySchema = z.object({ + commentID: z.number() +}); + +/** + * Delete comment with deletion type + */ +export const deleteCommentWithTypeSchema = z.object({ + commentID: z.number(), + commenterID: z.string(), + deletionType: z.enum(["user", "admin", "database"]) +}); + +/** + * User query schemas + */ +export const getUserByIdSchema = z.object({ + id: z.string() +}); + +export const getUserPublicDataSchema = z.object({ + id: z.string() +}); + +export const updateUserImageSchema = z.object({ + id: z.string(), + imageURL: z.string() +}); + +export const updateUserEmailSchema = z.object({ + id: z.string(), + newEmail: z.string().email(), + oldEmail: z.string().email() +}); + // ============================================================================ // Type Exports // ============================================================================ @@ -293,3 +372,23 @@ export type CreatePostLikeInput = z.infer; export type CreateTagInput = z.infer; export type CreateConnectionInput = z.infer; export type PaginationInput = z.infer; +export type GetPostByIdInput = z.infer; +export type GetPostByTitleInput = z.infer; +export type GetCommentsByPostIdInput = z.infer< + typeof getCommentsByPostIdSchema +>; +export type TogglePostLikeMutationInput = z.infer< + typeof togglePostLikeMutationSchema +>; +export type ToggleCommentReactionMutationInput = z.infer< + typeof toggleCommentReactionMutationSchema +>; +export type GetCommentReactionsQueryInput = z.infer< + typeof getCommentReactionsQuerySchema +>; +export type DeleteCommentWithTypeInput = z.infer< + typeof deleteCommentWithTypeSchema +>; +export type GetUserByIdInput = z.infer; +export type UpdateUserImageInput = z.infer; +export type UpdateUserEmailInput = z.infer; diff --git a/src/types/user.ts b/src/types/user.ts index f42c96e..43d9be8 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,3 +1,4 @@ +// lineage User export interface User { id: string; email: string | null;