diff --git a/apps/web/components/post/post.tsx b/apps/web/components/post/post.tsx index 1dc93d5..1e203f0 100644 --- a/apps/web/components/post/post.tsx +++ b/apps/web/components/post/post.tsx @@ -23,6 +23,7 @@ import { notifyError } from "../core/toast.component"; import ConfirmDeleteDialog from "../dialogs/confirm-delete-dialog.component"; import PostOptions from "./post-options"; import { PostStatusIcon } from "./post-status"; +import { createAuditLog } from "../../utils/auditLog"; const ReactionsCounter = ({ aggregate }: { aggregate: IReactions }) => { return ( @@ -171,7 +172,7 @@ export function Post({ try { await supabase.from("posts").delete().eq("id", post.id); - await supabase.from("page_audit_logs").insert({ + await createAuditLog(supabase, { page_id: page.id, actor_id: user.id, action: "Deleted Post", diff --git a/apps/web/components/roadmap/RoadmapBoard.tsx b/apps/web/components/roadmap/RoadmapBoard.tsx index c4b1b38..46ae0c8 100644 --- a/apps/web/components/roadmap/RoadmapBoard.tsx +++ b/apps/web/components/roadmap/RoadmapBoard.tsx @@ -36,6 +36,7 @@ export default function RoadmapBoard({ const dragDropHandlers = useRoadmapDragDrop({ itemsByColumn, setBoardItems, + board, }); const itemHandlers = useRoadmapItems({ diff --git a/apps/web/components/roadmap/hooks/useRoadmapDragDrop.ts b/apps/web/components/roadmap/hooks/useRoadmapDragDrop.ts index fc623d6..6054a2e 100644 --- a/apps/web/components/roadmap/hooks/useRoadmapDragDrop.ts +++ b/apps/web/components/roadmap/hooks/useRoadmapDragDrop.ts @@ -1,19 +1,23 @@ import { Dispatch, SetStateAction, useState } from "react"; import { useUserData } from "../../../utils/useUser"; +import { createAuditLog } from "../../../utils/auditLog"; import { DragOverPosition, ItemsByColumn, RoadmapItemWithRelations, } from "../types"; +import { IRoadmapBoard } from "@changes-page/supabase/types/page"; export function useRoadmapDragDrop({ itemsByColumn, setBoardItems, + board, }: { itemsByColumn: ItemsByColumn; setBoardItems: Dispatch>; + board: IRoadmapBoard; }) { - const { supabase } = useUserData(); + const { supabase, user } = useUserData(); const [draggedItem, setDraggedItem] = useState(null); const [dragOverColumn, setDragOverColumn] = useState(null); @@ -90,6 +94,26 @@ export function useRoadmapDragDrop({ currentDragOverPosition ); } + + // Create audit log for item move + if (user && draggedItem) { + const action = sourceColumnId === targetColumnId ? "Reordered" : "Moved"; + const description = sourceColumnId === targetColumnId + ? `${action} item within column` + : `${action} item from column to column`; + + await createAuditLog(supabase, { + page_id: board.page_id, + actor_id: user.id, + action: `Updated Roadmap Item: ${draggedItem.title}`, + changes: { + action: description, + from_column: sourceColumnId, + to_column: targetColumnId, + item: draggedItem + }, + }); + } } catch (error) { console.error("Error moving item:", error); alert("Failed to move item"); diff --git a/apps/web/components/roadmap/hooks/useRoadmapItems.ts b/apps/web/components/roadmap/hooks/useRoadmapItems.ts index 114f434..0ff57ce 100644 --- a/apps/web/components/roadmap/hooks/useRoadmapItems.ts +++ b/apps/web/components/roadmap/hooks/useRoadmapItems.ts @@ -3,6 +3,7 @@ import { IRoadmapCategory, } from "@changes-page/supabase/types/page"; import { useState } from "react"; +import { createAuditLog } from "../../../utils/auditLog"; import { useUserData } from "../../../utils/useUser"; import { FormErrors, @@ -20,7 +21,7 @@ export function useRoadmapItems({ categories: IRoadmapCategory[]; itemsByColumn: ItemsByColumn; }) { - const { supabase } = useUserData(); + const { supabase, user } = useUserData(); const [showItemModal, setShowItemModal] = useState(false); const [selectedColumnId, setSelectedColumnId] = useState(null); const [editingItem, setEditingItem] = @@ -66,6 +67,10 @@ export function useRoadmapItems({ if (!confirm("Are you sure you want to delete this item?")) return; try { + const itemToDelete = Object.values(itemsByColumn) + .flat() + .find((it) => it.id === itemId); + const { error } = await supabase .from("roadmap_items") .delete() @@ -74,6 +79,15 @@ export function useRoadmapItems({ if (error) throw error; setBoardItems((prev) => prev.filter((item) => item.id !== itemId)); + + if (itemToDelete && user) { + await createAuditLog(supabase, { + page_id: board.page_id, + actor_id: user.id, + action: `Deleted Roadmap Item: ${itemToDelete.title}`, + changes: { item_id: itemToDelete.id, item_title: itemToDelete.title }, + }); + } } catch (error) { console.error("Error deleting item:", error); alert("Failed to delete item"); @@ -127,6 +141,19 @@ export function useRoadmapItems({ setBoardItems((prev) => prev.map((item) => (item.id === editingItem.id ? data : item)) ); + + // Create audit log for update + if (user) { + await createAuditLog(supabase, { + page_id: board.page_id, + actor_id: user.id, + action: `Updated Roadmap Item: ${data.title}`, + changes: { + old: editingItem, + new: data, + }, + }); + } } else { if (!selectedColumnId) return; @@ -162,6 +189,16 @@ export function useRoadmapItems({ if (error) throw error; setBoardItems((prev) => [...prev, data]); + + // Create audit log for creation + if (user) { + await createAuditLog(supabase, { + page_id: board.page_id, + actor_id: user.id, + action: `Created Roadmap Item: ${data.title}`, + changes: { item: data }, + }); + } } setShowItemModal(false); diff --git a/apps/web/pages/api/posts/index.ts b/apps/web/pages/api/posts/index.ts index c8e9f1c..5061045 100644 --- a/apps/web/pages/api/posts/index.ts +++ b/apps/web/pages/api/posts/index.ts @@ -3,6 +3,7 @@ import { NewPostSchema } from "../../../data/schema"; import { apiRateLimiter } from "../../../utils/rate-limit"; import { createPost } from "../../../utils/useDatabase"; import { withAuth } from "../../../utils/withAuth"; +import { createAuditLog } from "../../../utils/auditLog"; const createNewPost = withAuth(async (req, res, { user, supabase }) => { if (req.method === "POST") { @@ -73,7 +74,7 @@ const createNewPost = withAuth(async (req, res, { user, supabase }) => { const post = await createPost(postPayload); - await supabase.from("page_audit_logs").insert({ + await createAuditLog(supabase, { page_id, actor_id: user.id, action: `Created Post: ${post.title}`, diff --git a/apps/web/pages/pages/[page_id]/[post_id].tsx b/apps/web/pages/pages/[page_id]/[post_id].tsx index 30bf136..589fff8 100644 --- a/apps/web/pages/pages/[page_id]/[post_id].tsx +++ b/apps/web/pages/pages/[page_id]/[post_id].tsx @@ -14,6 +14,7 @@ import { NewPostSchema } from "../../../data/schema"; import { withSupabase } from "../../../utils/supabase/withSupabase"; import { createOrRetrievePageSettings } from "../../../utils/useDatabase"; import { useUserData } from "../../../utils/useUser"; +import { createAuditLog } from "../../../utils/auditLog"; export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const { page_id, post_id } = ctx.params; @@ -67,7 +68,7 @@ export default function EditPost({ await supabase.from("posts").update(newPost).match({ id: post_id }); - await supabase.from("page_audit_logs").insert({ + await createAuditLog(supabase, { page_id: String(page_id), actor_id: user.id, action: `Updated Post: ${newPost.title}`, diff --git a/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx b/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx index 4caf2bb..1edd960 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/[board_id]/settings.tsx @@ -16,6 +16,7 @@ import { withSupabase } from "../../../../../utils/supabase/withSupabase"; import { createOrRetrievePageSettings } from "../../../../../utils/useDatabase"; import { getPage } from "../../../../../utils/useSSR"; import { useUserData } from "../../../../../utils/useUser"; +import { createAuditLog } from "../../../../../utils/auditLog"; export const getServerSideProps = withSupabase(async (ctx, { supabase }) => { const { page_id } = ctx.params; @@ -235,6 +236,27 @@ export default function BoardSettings({ throw error; } + // Create audit log for board update + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Updated Roadmap Board: ${boardForm.title}`, + changes: { + old: { + title: board.title, + description: board.description, + slug: board.slug, + is_public: board.is_public + }, + new: { + title: boardForm.title, + description: boardForm.description, + slug: boardForm.slug, + is_public: boardForm.is_public + } + }, + }); + // Refresh the page to show updated settings window.location.reload(); } catch (error) { @@ -266,6 +288,14 @@ export default function BoardSettings({ setBoardCategories([...boardCategories, data]); setNewCategory(""); setNewCategoryColor("blue"); + + // Create audit log for category creation + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Created Roadmap Category: ${data.name}`, + changes: { category: data }, + }); } catch (error) { console.error("Error adding category:", error); alert("Failed to add category"); @@ -286,13 +316,18 @@ export default function BoardSettings({ if (error) throw error; + const oldCategory = boardCategories.find(cat => cat.id === categoryId); + const newCategoryData = { + name: categoryToEdit.trim(), + color: categoryColorToEdit, + }; + setBoardCategories((prev) => prev.map((cat) => cat.id === categoryId ? { ...cat, - name: categoryToEdit.trim(), - color: categoryColorToEdit, + ...newCategoryData, } : cat ) @@ -300,6 +335,19 @@ export default function BoardSettings({ setEditingCategory(null); setCategoryToEdit(""); setCategoryColorToEdit("blue"); + + // Create audit log for category update + if (oldCategory) { + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Updated Roadmap Category: ${newCategoryData.name}`, + changes: { + old: oldCategory, + new: { ...oldCategory, ...newCategoryData } + }, + }); + } } catch (error) { console.error("Error updating category:", error); alert("Failed to update category"); @@ -332,7 +380,18 @@ export default function BoardSettings({ if (error) throw error; + const deletedCategory = boardCategories.find(cat => cat.id === categoryId); setBoardCategories((prev) => prev.filter((cat) => cat.id !== categoryId)); + + // Create audit log for category deletion + if (deletedCategory) { + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Deleted Roadmap Category: ${deletedCategory.name}`, + changes: { category: deletedCategory }, + }); + } } catch (error) { console.error("Error deleting category:", error); alert("Failed to delete category"); @@ -362,6 +421,14 @@ export default function BoardSettings({ setBoardColumns([...boardColumns, data]); setNewColumn(""); + + // Create audit log for column creation + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Created Roadmap Column: ${data.name}`, + changes: { column: data }, + }); } catch (error) { console.error("Error adding column:", error); alert("Failed to add column"); @@ -379,13 +446,29 @@ export default function BoardSettings({ if (error) throw error; + const oldColumn = boardColumns.find(col => col.id === columnId); + const newColumnName = columnToEdit.trim(); + setBoardColumns((prev) => prev.map((col) => - col.id === columnId ? { ...col, name: columnToEdit.trim() } : col + col.id === columnId ? { ...col, name: newColumnName } : col ) ); setEditingColumn(null); setColumnToEdit(""); + + // Create audit log for column update + if (oldColumn) { + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Updated Roadmap Column: ${newColumnName}`, + changes: { + old: oldColumn, + new: { ...oldColumn, name: newColumnName } + }, + }); + } } catch (error) { console.error("Error updating column:", error); alert("Failed to update column"); @@ -420,7 +503,18 @@ export default function BoardSettings({ if (error) throw error; + const deletedColumn = boardColumns.find(col => col.id === columnId); setBoardColumns((prev) => prev.filter((col) => col.id !== columnId)); + + // Create audit log for column deletion + if (deletedColumn) { + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Deleted Roadmap Column: ${deletedColumn.name}`, + changes: { column: deletedColumn }, + }); + } } catch (error) { console.error("Error deleting column:", error); alert("Failed to delete column"); @@ -471,6 +565,17 @@ export default function BoardSettings({ await Promise.all(updatePromises); + // Create audit log for column reordering + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: "Reordered Roadmap Columns", + changes: { + old_order: boardColumns.map(col => ({ id: col.id, name: col.name, position: col.position })), + new_order: newColumns.map((col, index) => ({ id: col.id, name: col.name, position: index + 1 })) + }, + }); + // Update local state setBoardColumns( newColumns.map((column, index) => ({ diff --git a/apps/web/pages/pages/[page_id]/roadmap/new.tsx b/apps/web/pages/pages/[page_id]/roadmap/new.tsx index 59bc26f..5e2ef22 100644 --- a/apps/web/pages/pages/[page_id]/roadmap/new.tsx +++ b/apps/web/pages/pages/[page_id]/roadmap/new.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { useState, type JSX } from "react"; import AuthLayout from "../../../../components/layout/auth-layout.component"; import Page from "../../../../components/layout/page.component"; +import { createAuditLog } from "../../../../utils/auditLog"; import { withSupabase } from "../../../../utils/supabase/withSupabase"; import { getPage } from "../../../../utils/useSSR"; import { useUserData } from "../../../../utils/useUser"; @@ -36,7 +37,7 @@ export default function NewRoadmapBoard({ page_id, }: InferGetServerSidePropsType) { const router = useRouter(); - const { supabase } = useUserData(); + const { supabase, user } = useUserData(); const [formData, setFormData] = useState({ title: "", @@ -136,6 +137,15 @@ export default function NewRoadmapBoard({ board_id: board.id, }); + if (user) { + await createAuditLog(supabase, { + page_id: page_id, + actor_id: user.id, + action: `Created Roadmap Board: ${board.title}`, + changes: { board }, + }); + } + // Redirect to the roadmap page await router.push(`/pages/${page_id}/roadmap`); } catch (error) { diff --git a/apps/web/utils/auditLog.ts b/apps/web/utils/auditLog.ts new file mode 100644 index 0000000..0a87748 --- /dev/null +++ b/apps/web/utils/auditLog.ts @@ -0,0 +1,24 @@ +import { SupabaseClient } from "@supabase/supabase-js"; + +interface AuditLogEntry { + page_id: string; + actor_id: string; + action: string; + changes?: Record; +} + +export async function createAuditLog( + supabase: SupabaseClient, + entry: AuditLogEntry +) { + try { + const { error } = await supabase.from("page_audit_logs").insert(entry); + + if (error) { + console.error("Failed to create audit log:", error); + throw error; + } + } catch (error) { + console.error("Error creating audit log:", error); + } +} \ No newline at end of file diff --git a/apps/web/utils/hooks/usePageSettings.ts b/apps/web/utils/hooks/usePageSettings.ts index cd1a96d..55870eb 100644 --- a/apps/web/utils/hooks/usePageSettings.ts +++ b/apps/web/utils/hooks/usePageSettings.ts @@ -6,6 +6,7 @@ import { } from "../../components/core/toast.component"; import { httpGet } from "../http"; import { useUserData } from "../useUser"; +import { createAuditLog } from "../auditLog"; export default function usePageSettings(pageId: string, prefetch = true) { const { supabase, user } = useUserData(); @@ -21,7 +22,7 @@ export default function usePageSettings(pageId: string, prefetch = true) { .match({ page_id: pageId }) .select(); - await supabase.from("page_audit_logs").insert({ + await createAuditLog(supabase, { page_id: pageId, actor_id: user.id, action: "Updated Page Settings",