import { createTRPCRouter, publicProcedure, protectedProcedure } from "../utils"; import { z } from "zod"; import { ConnectionFactory } from "~/server/utils"; import { TRPCError } from "@trpc/server"; import { env } from "~/env/server"; export const databaseRouter = createTRPCRouter({ // ============================================================ // Comment Reactions Routes // ============================================================ getCommentReactions: publicProcedure .input(z.object({ commentID: z.string() })) .query(async ({ input }) => { try { const conn = ConnectionFactory(); const query = "SELECT * FROM CommentReaction WHERE comment_id = ?"; const results = await conn.execute({ sql: query, args: [input.commentID] }); return { commentReactions: results.rows }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch comment reactions" }); } }), addCommentReaction: publicProcedure .input( z.object({ type: z.string(), comment_id: z.string(), user_id: z.string() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); const query = ` INSERT INTO CommentReaction (type, comment_id, user_id) VALUES (?, ?, ?) `; await conn.execute({ sql: query, args: [input.type, input.comment_id, input.user_id] }); const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`; const res = await conn.execute({ sql: followUpQuery, args: [input.comment_id] }); return { commentReactions: res.rows }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to add comment reaction" }); } }), removeCommentReaction: publicProcedure .input( z.object({ type: z.string(), comment_id: z.string(), user_id: z.string() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); const query = ` DELETE FROM CommentReaction WHERE type = ? AND comment_id = ? AND user_id = ? `; await conn.execute({ sql: query, args: [input.type, input.comment_id, input.user_id] }); const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`; const res = await conn.execute({ sql: followUpQuery, args: [input.comment_id] }); return { commentReactions: res.rows }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to remove comment reaction" }); } }), // ============================================================ // Comments Routes // ============================================================ getAllComments: publicProcedure.query(async () => { try { const conn = ConnectionFactory(); // Join with Post table to get post titles along with comments const query = ` SELECT c.*, p.title as post_title FROM Comment c JOIN Post p ON c.post_id = p.id ORDER BY c.created_at DESC `; const res = await conn.execute(query); return { comments: res.rows }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch comments" }); } }), deleteComment: protectedProcedure .input( z.object({ commentID: z.number(), commenterID: z.string(), deletionType: z.enum(["user", "admin", "database"]) }) ) .mutation(async ({ input, ctx }) => { try { const conn = ConnectionFactory(); console.log("[deleteComment] Starting deletion:", { commentID: input.commentID, deletionType: input.deletionType, userId: ctx.userId, privilegeLevel: ctx.privilegeLevel }); // Get the comment to check ownership const commentQuery = await conn.execute({ sql: "SELECT * FROM Comment WHERE id = ?", args: [input.commentID] }); const comment = commentQuery.rows[0] as any; if (!comment) { throw new TRPCError({ code: "NOT_FOUND", message: "Comment not found" }); } // Authorization checks const isOwner = comment.commenter_id === ctx.userId; const isAdmin = ctx.privilegeLevel === "admin"; console.log("[deleteComment] Authorization check:", { isOwner, isAdmin, commentOwner: comment.commenter_id, requestingUser: ctx.userId }); // User can only delete their own comments with "user" type if (input.deletionType === "user" && !isOwner && !isAdmin) { throw new TRPCError({ code: "FORBIDDEN", message: "You can only delete your own comments" }); } // Only admins can do admin or database deletion if ( (input.deletionType === "admin" || input.deletionType === "database") && !isAdmin ) { throw new TRPCError({ code: "FORBIDDEN", message: "Admin access required for this deletion type" }); } if (input.deletionType === "database") { console.log("[deleteComment] Performing database deletion"); // Full deletion - remove from database // First delete reactions await conn.execute({ sql: "DELETE FROM CommentReaction WHERE comment_id = ?", args: [input.commentID] }); // Then delete the comment await conn.execute({ sql: "DELETE FROM Comment WHERE id = ?", args: [input.commentID] }); console.log("[deleteComment] Database deletion successful"); return { success: true, deletionType: "database", commentBody: null }; } else if (input.deletionType === "admin") { console.log("[deleteComment] Performing admin deletion"); // Admin delete - replace body with admin message await conn.execute({ sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?", args: ["[deleted by admin]", "", input.commentID] }); console.log("[deleteComment] Admin deletion successful"); return { success: true, deletionType: "admin", commentBody: "[deleted by admin]" }; } else { console.log("[deleteComment] Performing user deletion"); // User delete - replace body with user message await conn.execute({ sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?", args: ["[deleted]", "", input.commentID] }); console.log("[deleteComment] User deletion successful"); return { success: true, deletionType: "user", commentBody: "[deleted]" }; } } catch (error) { console.error("[deleteComment] Failed to delete comment:", error); if (error instanceof TRPCError) { throw error; } throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to delete comment" }); } }), getCommentsByPostId: publicProcedure .input(z.object({ post_id: z.string() })) .query(async ({ input }) => { try { const conn = ConnectionFactory(); // Join with Post table to get post titles along with comments const query = ` SELECT c.*, p.title as post_title FROM Comment c JOIN Post p ON c.post_id = p.id WHERE c.post_id = ? ORDER BY c.created_at DESC `; const res = await conn.execute({ sql: query, args: [input.post_id] }); return { comments: res.rows }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch comments by post ID" }); } }), // ============================================================ // Post Routes // ============================================================ getPostById: publicProcedure .input( z.object({ category: z.literal("blog"), id: z.number() }) ) .query(async ({ input }) => { try { const conn = ConnectionFactory(); // Single query with JOIN to get post and tags in one go const query = ` SELECT p.*, t.value as tag_value FROM Post p LEFT JOIN Tag t ON p.id = t.post_id WHERE p.id = ? `; const results = await conn.execute({ sql: query, args: [input.id] }); if (results.rows[0]) { // Group tags by post ID const post = results.rows[0]; const tags = results.rows .filter((row) => row.tag_value) .map((row) => row.tag_value); return { post, tags }; } else { return { post: null, tags: [] }; } } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch post by ID" }); } }), getPostByTitle: publicProcedure .input( z.object({ category: z.literal("blog"), title: z.string() }) ) .query(async ({ input, ctx }) => { try { const conn = ConnectionFactory(); // Get post by title with JOINs to get all related data in one query const postQuery = ` SELECT p.*, COUNT(DISTINCT c.id) as comment_count, COUNT(DISTINCT pl.user_id) as like_count, GROUP_CONCAT(t.value) as tags FROM Post p LEFT JOIN Comment c ON p.id = c.post_id LEFT JOIN PostLike pl ON p.id = pl.post_id LEFT JOIN Tag t ON p.id = t.post_id WHERE p.title = ? AND p.category = ? AND p.published = ? GROUP BY p.id `; const postResults = await conn.execute({ sql: postQuery, args: [input.title, input.category, true] }); if (!postResults.rows[0]) { return null; } const postRow = postResults.rows[0]; // Return structured data with proper formatting return { post: postRow, comments: [], // Comments are not included in this optimized query - would need separate call if needed likes: [], // Likes are not included in this optimized query - would need separate call if needed tags: postRow.tags ? postRow.tags.split(",") : [] }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch post by title" }); } }), createPost: publicProcedure .input( z.object({ category: z.literal("blog"), title: z.string(), subtitle: z.string().nullable(), body: z.string().nullable(), banner_photo: z.string().nullable(), published: z.boolean(), tags: z.array(z.string()).nullable(), author_id: z.string() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); const fullURL = input.banner_photo ? env.VITE_AWS_BUCKET_STRING + input.banner_photo : null; const query = ` INSERT INTO Post (title, category, subtitle, body, banner_photo, published, author_id) VALUES (?, ?, ?, ?, ?, ?, ?) `; const params = [ input.title, input.category, input.subtitle, input.body, fullURL, input.published, input.author_id ]; const results = await conn.execute({ sql: query, args: params }); if (input.tags && input.tags.length > 0) { let tagQuery = "INSERT INTO Tag (value, post_id) VALUES "; let values = input.tags.map( (tag) => `("${tag}", ${results.lastInsertRowid})` ); tagQuery += values.join(", "); await conn.execute(tagQuery); } return { data: results.lastInsertRowid }; } catch (error) { console.error(error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create post" }); } }), updatePost: publicProcedure .input( z.object({ id: z.number(), title: z.string().nullable().optional(), subtitle: z.string().nullable().optional(), body: z.string().nullable().optional(), banner_photo: z.string().nullable().optional(), published: z.boolean().nullable().optional(), tags: z.array(z.string()).nullable().optional(), author_id: z.string() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); let query = "UPDATE Post SET "; let params: any[] = []; let first = true; if (input.title !== undefined && input.title !== null) { query += first ? "title = ?" : ", title = ?"; params.push(input.title); first = false; } if (input.subtitle !== undefined && input.subtitle !== null) { query += first ? "subtitle = ?" : ", subtitle = ?"; params.push(input.subtitle); first = false; } if (input.body !== undefined && input.body !== null) { query += first ? "body = ?" : ", body = ?"; params.push(input.body); first = false; } if (input.banner_photo !== undefined && input.banner_photo !== null) { query += first ? "banner_photo = ?" : ", banner_photo = ?"; if (input.banner_photo === "_DELETE_IMAGE_") { params.push(null); } else { params.push(env.VITE_AWS_BUCKET_STRING + input.banner_photo); } first = false; } if (input.published !== undefined && input.published !== null) { query += first ? "published = ?" : ", published = ?"; params.push(input.published); first = false; } query += first ? "author_id = ?" : ", author_id = ?"; params.push(input.author_id); query += " WHERE id = ?"; params.push(input.id); const results = await conn.execute({ sql: query, args: params }); // Handle tags const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`; await conn.execute({ sql: deleteTagsQuery, args: [input.id.toString()] }); if (input.tags && input.tags.length > 0) { let tagQuery = "INSERT INTO Tag (value, post_id) VALUES "; let values = input.tags.map((tag) => `("${tag}", ${input.id})`); tagQuery += values.join(", "); await conn.execute(tagQuery); } return { data: results.lastInsertRowid }; } catch (error) { console.error(error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update post" }); } }), deletePost: publicProcedure .input(z.object({ id: z.number() })) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); // Delete associated tags first await conn.execute({ sql: "DELETE FROM Tag WHERE post_id = ?", args: [input.id.toString()] }); // Delete associated likes await conn.execute({ sql: "DELETE FROM PostLike WHERE post_id = ?", args: [input.id.toString()] }); // Delete associated comments await conn.execute({ sql: "DELETE FROM Comment WHERE post_id = ?", args: [input.id] }); // Finally delete the post await conn.execute({ sql: "DELETE FROM Post WHERE id = ?", args: [input.id] }); 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() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); const query = `INSERT INTO PostLike (user_id, post_id) VALUES (?, ?)`; await conn.execute({ sql: query, args: [input.user_id, input.post_id] }); const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`; const res = await conn.execute({ sql: followUpQuery, args: [input.post_id] }); return { newLikes: res.rows }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to add post like" }); } }), removePostLike: publicProcedure .input( z.object({ user_id: z.string(), post_id: z.string() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); const query = ` DELETE FROM PostLike WHERE user_id = ? AND post_id = ? `; await conn.execute({ sql: query, args: [input.user_id, input.post_id] }); const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`; const res = await conn.execute({ sql: followUpQuery, args: [input.post_id] }); return { newLikes: res.rows }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to remove post like" }); } }), // ============================================================ // User Routes // ============================================================ getUserById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { try { const conn = ConnectionFactory(); const query = "SELECT * FROM User WHERE id = ?"; const res = await conn.execute({ sql: query, args: [input.id] }); if (res.rows[0]) { const user = res.rows[0] as any; if (user && user.display_name !== "user deleted") { return { id: user.id, email: user.email, emailVerified: user.email_verified, image: user.image, displayName: user.display_name, provider: user.provider, hasPassword: !!user.password_hash }; } } return null; } catch (error) { console.error(error); return null; } }), getUserPublicData: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { try { const conn = ConnectionFactory(); const query = "SELECT email, display_name, image FROM User WHERE id = ?"; const res = await conn.execute({ sql: query, args: [input.id] }); if (res.rows[0]) { const user = res.rows[0] as any; if (user && user.display_name !== "user deleted") { return { email: user.email, image: user.image, display_name: user.display_name }; } } return null; } catch (error) { console.error(error); return null; } }), getUserImage: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { try { const conn = ConnectionFactory(); const query = "SELECT * FROM User WHERE id = ?"; const results = await conn.execute({ sql: query, args: [input.id] }); return { user: results.rows[0] }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch user image" }); } }), updateUserImage: publicProcedure .input( z.object({ id: z.string(), imageURL: z.string() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); const fullURL = input.imageURL ? env.VITE_AWS_BUCKET_STRING + input.imageURL : null; const query = `UPDATE User SET image = ? WHERE id = ?`; await conn.execute({ sql: query, args: [fullURL, input.id] }); return { res: "success" }; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update user image" }); } }), updateUserEmail: publicProcedure .input( z.object({ id: z.string(), newEmail: z.string().email(), oldEmail: z.string().email() }) ) .mutation(async ({ input }) => { try { const conn = ConnectionFactory(); const query = `UPDATE User SET email = ? WHERE id = ? AND email = ?`; const res = await conn.execute({ sql: query, args: [input.newEmail, input.id, input.oldEmail] }); return { res }; } catch (error) { console.error(error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update user email" }); } }) });