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 [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal(""); const [error, setError] = createSignal("");
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false); const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
const [isInitialLoad, setIsInitialLoad] = createSignal(props.mode === "edit"); const [isInitialLoad, setIsInitialLoad] = createSignal(true);
const [initialBody, setInitialBody] = createSignal<string | undefined>( const [initialBody, setInitialBody] = createSignal<string | undefined>(
props.initialData?.body 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) // Mark initial load as complete after data is loaded (for edit mode)
// Use setTimeout to ensure this runs after all signals are initialized
createEffect(() => { createEffect(() => {
if (props.mode === "edit" && props.initialData) { 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 { ConnectionFactory, hashPassword, checkPassword } from "~/server/utils";
import { SignJWT, jwtVerify } from "jose"; import { SignJWT, jwtVerify } from "jose";
import { setCookie, getCookie } from "vinxi/http"; import { setCookie, getCookie } from "vinxi/http";
import type { User } from "~/types/user"; import type { User } from "~/db/types";
import { import {
fetchWithTimeout, fetchWithTimeout,
checkResponse, checkResponse,
@@ -15,6 +15,12 @@ import {
TimeoutError, TimeoutError,
APIError APIError
} from "~/server/fetch-utils"; } from "~/server/fetch-utils";
import {
registerUserSchema,
loginUserSchema,
resetPasswordSchema,
requestPasswordResetSchema
} from "../schemas/user";
async function createJWT( async function createJWT(
userId: string, userId: string,
@@ -501,16 +507,11 @@ export const authRouter = createTRPCRouter({
}), }),
emailRegistration: publicProcedure emailRegistration: publicProcedure
.input( .input(registerUserSchema)
z.object({
email: z.string().email(),
password: z.string().min(8),
passwordConfirmation: z.string().min(8)
})
)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email, password, passwordConfirmation } = input; const { email, password, passwordConfirmation } = input;
// Schema already validates password match, but double check
if (password !== passwordConfirmation) { if (password !== passwordConfirmation) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -549,13 +550,7 @@ export const authRouter = createTRPCRouter({
}), }),
emailPasswordLogin: publicProcedure emailPasswordLogin: publicProcedure
.input( .input(loginUserSchema)
z.object({
email: z.string().email(),
password: z.string(),
rememberMe: z.boolean().optional()
})
)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email, password, rememberMe } = input; const { email, password, rememberMe } = input;
@@ -746,7 +741,7 @@ export const authRouter = createTRPCRouter({
}), }),
requestPasswordReset: publicProcedure requestPasswordReset: publicProcedure
.input(z.object({ email: z.string().email() })) .input(requestPasswordResetSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email } = input; const { email } = input;
@@ -862,16 +857,11 @@ export const authRouter = createTRPCRouter({
}), }),
resetPassword: publicProcedure resetPassword: publicProcedure
.input( .input(resetPasswordSchema)
z.object({
token: z.string(),
newPassword: z.string().min(8),
newPasswordConfirmation: z.string().min(8)
})
)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { token, newPassword, newPasswordConfirmation } = input; const { token, newPassword, newPasswordConfirmation } = input;
// Schema already validates password match, but double check
if (newPassword !== newPasswordConfirmation) { if (newPassword !== newPasswordConfirmation) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -945,7 +935,7 @@ export const authRouter = createTRPCRouter({
}), }),
resendEmailVerification: publicProcedure resendEmailVerification: publicProcedure
.input(z.object({ email: z.string().email() })) .input(requestPasswordResetSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { email } = input; const { email } = input;

View File

@@ -8,12 +8,35 @@ import { ConnectionFactory } from "~/server/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { env } from "~/env/server"; import { env } from "~/env/server";
import { cache, withCacheAndStale } from "~/server/cache"; 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 const BLOG_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
export const databaseRouter = createTRPCRouter({ export const databaseRouter = createTRPCRouter({
getCommentReactions: publicProcedure getCommentReactions: publicProcedure
.input(z.object({ commentID: z.string() })) .input(getCommentReactionsQuerySchema)
.query(async ({ input }) => { .query(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -22,7 +45,9 @@ export const databaseRouter = createTRPCRouter({
sql: query, sql: query,
args: [input.commentID] args: [input.commentID]
}); });
return { commentReactions: results.rows }; return {
commentReactions: results.rows as unknown as CommentReaction[]
};
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@@ -32,13 +57,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
addCommentReaction: publicProcedure addCommentReaction: publicProcedure
.input( .input(toggleCommentReactionMutationSchema)
z.object({
type: z.string(),
comment_id: z.string(),
user_id: z.string()
})
)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -57,7 +76,7 @@ export const databaseRouter = createTRPCRouter({
args: [input.comment_id] args: [input.comment_id]
}); });
return { commentReactions: res.rows }; return { commentReactions: res.rows as unknown as CommentReaction[] };
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@@ -67,13 +86,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
removeCommentReaction: publicProcedure removeCommentReaction: publicProcedure
.input( .input(toggleCommentReactionMutationSchema)
z.object({
type: z.string(),
comment_id: z.string(),
user_id: z.string()
})
)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -92,7 +105,7 @@ export const databaseRouter = createTRPCRouter({
args: [input.comment_id] args: [input.comment_id]
}); });
return { commentReactions: res.rows }; return { commentReactions: res.rows as unknown as CommentReaction[] };
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@@ -121,13 +134,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
deleteComment: protectedProcedure deleteComment: protectedProcedure
.input( .input(deleteCommentWithTypeSchema)
z.object({
commentID: z.number(),
commenterID: z.string(),
deletionType: z.enum(["user", "admin", "database"])
})
)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -238,7 +245,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
getCommentsByPostId: publicProcedure getCommentsByPostId: publicProcedure
.input(z.object({ post_id: z.string() })) .input(getCommentsByPostIdSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -253,7 +260,7 @@ export const databaseRouter = createTRPCRouter({
sql: query, sql: query,
args: [input.post_id] args: [input.post_id]
}); });
return { comments: res.rows }; return { comments: res.rows as unknown as Comment[] };
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@@ -263,12 +270,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
getPostById: publicProcedure getPostById: publicProcedure
.input( .input(getPostByIdSchema)
z.object({
category: z.literal("blog"),
id: z.number()
})
)
.query(async ({ input }) => { .query(async ({ input }) => {
return withCacheAndStale( return withCacheAndStale(
`blog-post-id-${input.id}`, `blog-post-id-${input.id}`,
@@ -311,12 +313,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
getPostByTitle: publicProcedure getPostByTitle: publicProcedure
.input( .input(getPostByTitleSchema)
z.object({
category: z.literal("blog"),
title: z.string()
})
)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
return withCacheAndStale( return withCacheAndStale(
`blog-post-title-${input.title}`, `blog-post-title-${input.title}`,
@@ -510,55 +507,48 @@ export const databaseRouter = createTRPCRouter({
} }
}), }),
deletePost: publicProcedure deletePost: publicProcedure.input(idSchema).mutation(async ({ input }) => {
.input(z.object({ id: z.number() })) try {
.mutation(async ({ input }) => { const conn = ConnectionFactory();
try {
const conn = ConnectionFactory();
await conn.execute({ await conn.execute({
sql: "DELETE FROM Tag WHERE post_id = ?", sql: "DELETE FROM Tag WHERE post_id = ?",
args: [input.id.toString()] args: [input.id.toString()]
}); });
await conn.execute({ await conn.execute({
sql: "DELETE FROM PostLike WHERE post_id = ?", sql: "DELETE FROM PostLike WHERE post_id = ?",
args: [input.id.toString()] args: [input.id.toString()]
}); });
await conn.execute({ await conn.execute({
sql: "DELETE FROM Comment WHERE post_id = ?", sql: "DELETE FROM Comment WHERE post_id = ?",
args: [input.id] args: [input.id]
}); });
await conn.execute({ await conn.execute({
sql: "DELETE FROM Post WHERE id = ?", sql: "DELETE FROM Post WHERE id = ?",
args: [input.id] args: [input.id]
}); });
cache.deleteByPrefix("blog-"); cache.deleteByPrefix("blog-");
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete post" message: "Failed to delete post"
}); });
} }
}), }),
// ============================================================ // ============================================================
// Post Likes Routes // Post Likes Routes
// ============================================================ // ============================================================
addPostLike: publicProcedure addPostLike: publicProcedure
.input( .input(togglePostLikeMutationSchema)
z.object({
user_id: z.string(),
post_id: z.string()
})
)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -574,7 +564,7 @@ export const databaseRouter = createTRPCRouter({
args: [input.post_id] args: [input.post_id]
}); });
return { newLikes: res.rows }; return { newLikes: res.rows as unknown as PostLike[] };
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@@ -584,12 +574,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
removePostLike: publicProcedure removePostLike: publicProcedure
.input( .input(togglePostLikeMutationSchema)
z.object({
user_id: z.string(),
post_id: z.string()
})
)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -608,7 +593,7 @@ export const databaseRouter = createTRPCRouter({
args: [input.post_id] args: [input.post_id]
}); });
return { newLikes: res.rows }; return { newLikes: res.rows as unknown as PostLike[] };
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@@ -622,7 +607,7 @@ export const databaseRouter = createTRPCRouter({
// ============================================================ // ============================================================
getUserById: publicProcedure getUserById: publicProcedure
.input(z.object({ id: z.string() })) .input(getUserByIdSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -654,7 +639,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
getUserPublicData: publicProcedure getUserPublicData: publicProcedure
.input(z.object({ id: z.string() })) .input(getUserByIdSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -683,7 +668,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
getUserImage: publicProcedure getUserImage: publicProcedure
.input(z.object({ id: z.string() })) .input(getUserByIdSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -702,12 +687,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
updateUserImage: publicProcedure updateUserImage: publicProcedure
.input( .input(updateUserImageSchema)
z.object({
id: z.string(),
imageURL: z.string()
})
)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();
@@ -729,13 +709,7 @@ export const databaseRouter = createTRPCRouter({
}), }),
updateUserEmail: publicProcedure updateUserEmail: publicProcedure
.input( .input(updateUserEmailSchema)
z.object({
id: z.string(),
newEmail: z.string().email(),
oldEmail: z.string().email()
})
)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const conn = ConnectionFactory(); const conn = ConnectionFactory();

View File

@@ -277,6 +277,85 @@ export const paginationSchema = z.object({
offset: z.number().min(0).default(0) 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 // Type Exports
// ============================================================================ // ============================================================================
@@ -293,3 +372,23 @@ export type CreatePostLikeInput = z.infer<typeof createPostLikeSchema>;
export type CreateTagInput = z.infer<typeof createTagSchema>; export type CreateTagInput = z.infer<typeof createTagSchema>;
export type CreateConnectionInput = z.infer<typeof createConnectionSchema>; export type CreateConnectionInput = z.infer<typeof createConnectionSchema>;
export type PaginationInput = z.infer<typeof paginationSchema>; 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 { export interface User {
id: string; id: string;
email: string | null; email: string | null;