validation

This commit is contained in:
Michael Freno
2025-12-26 15:04:18 -05:00
parent c18363c74f
commit b412db92e5
5 changed files with 191 additions and 126 deletions

View File

@@ -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<string | undefined>(
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);
}
});

View File

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

View File

@@ -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,9 +507,7 @@ export const databaseRouter = createTRPCRouter({
}
}),
deletePost: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
deletePost: publicProcedure.input(idSchema).mutation(async ({ input }) => {
try {
const conn = ConnectionFactory();
@@ -553,12 +548,7 @@ export const databaseRouter = createTRPCRouter({
// ============================================================
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();

View File

@@ -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<typeof createPostLikeSchema>;
export type CreateTagInput = z.infer<typeof createTagSchema>;
export type CreateConnectionInput = z.infer<typeof createConnectionSchema>;
export type PaginationInput = z.infer<typeof paginationSchema>;
export type GetPostByIdInput = z.infer<typeof getPostByIdSchema>;
export type GetPostByTitleInput = z.infer<typeof getPostByTitleSchema>;
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<typeof getUserByIdSchema>;
export type UpdateUserImageInput = z.infer<typeof updateUserImageSchema>;
export type UpdateUserEmailInput = z.infer<typeof updateUserEmailSchema>;

View File

@@ -1,3 +1,4 @@
// lineage User
export interface User {
id: string;
email: string | null;