@@ -424,40 +481,61 @@ export function WikiFolderTree({
>
);
+ // Common class string including selection highlight
+ const nodeClasses = `group flex cursor-pointer items-center rounded-md px-2 py-1.5 text-sm hover:bg-background-level2 ${
+ // Adjusted py padding
+ isSelected
+ ? "bg-primary/10 text-text-primary font-medium" // Highlight selected
+ : "text-text-secondary"
+ }`;
+
return (
- {mode === "navigation" ? (
-
- {childElements}
-
- ) : (
-
handleSelect(node, e)}
- style={{ paddingLeft: `${depth * 16 + 8}px` }}
- title={fullPath}
- >
- {childElements}
-
- )}
+ {/* Use a div with onClick instead of Link or the selection mode div */}
+
handleNodeClick(node, e)}
+ style={{ paddingLeft: `${depth * 16 + 8}px` }}
+ title={fullPath}
+ role="button" // Accessibility
+ tabIndex={0} // Accessibility
+ onKeyDown={(e) => {
+ // Allow activation with Enter/Space
+ if (e.key === "Enter" || e.key === " ") {
+ handleNodeClick(node, e as unknown as React.MouseEvent); // Cast needed for event type mismatch
+ }
+ }}
+ >
+ {/* Expansion chevron button */}
+ {hasChildren && !isWithinOpenDepth ? (
+ {
+ e.stopPropagation(); // Prevent node click
+ toggleFolder(node.path);
+ }}
+ className="hover:bg-background-level2 mr-1 rounded p-0.5 focus:outline-none"
+ title={isExpanded ? "Collapse folder" : "Expand folder"}
+ aria-expanded={isExpanded}
+ >
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ ) : (
+ /* Spacer if no chevron needed or forced open */
+
+ )}
+ {childElements}
+
{isExpanded && hasChildren && (
+ {" "}
+ {/* Indent children slightly more */}
{node.children.map((child) => renderNode(child, depth + 1))}
)}
@@ -481,7 +559,7 @@ export function WikiFolderTree({
<>
{currentNode.children.map((child) => renderNode(child))}
{currentNode.children.length === 0 && (
-
+
No subpages found
)}
@@ -501,172 +579,149 @@ export function WikiFolderTree({
};
if (isLoading) {
+ // Basic loading state
return (
-
-
Loading...
+
+ {!hideHeader && (
+
+ {title}
+
+ )}
+
);
}
return (
-
- {!hideHeader && (
-
-
{title}
-
- {showActions && (
- handleNewFolder("", e)}
- className="hover:bg-background-level2 rounded-md p-1"
- title="Create new root folder"
- >
-
-
- )}
-
-
- )}
-
- {renderFolderStructure()}
- {(!folderStructure ||
- (folderStructure.children.length === 0 && !showOnlyChildren)) && (
-
- No content found
-
+
+
-
- {/* Legend */}
- {showLegend && (
-
-
-
-
- Real folder
-
-
-
- Virtual folder
-
-
-
- 1p
-
-
Page count
+ >
+
+ {!hideHeader && (
+
+
+ {title}
+
+ {showActions && (
+
+ handleNewFolder("", e)} // Create at root
+ className="text-primary hover:text-primary/80 text-xs"
+ title="Create new root folder/page"
+ >
+ + New Root Item
+
+
+ )}
-
-
- 1f
-
-
Subfolder count
+ )}
+ {renderFolderStructure()}
+
+ {showLegend && !hideHeader && (
+
+
+ = Folder
+ with Content
+
+
+ =
+ Virtual Folder
+
+
+ = Page
+
+ {mode === "navigation" && (
+ <>
+
+
+ Double click a folder to navigate
+
+
+
+
+ Shift
+
+ + Click a folder to navigate immediately
+
+ >
+ )}
-
-
+ )}
+
+
+
+ {/* Modals */}
+ {showPageLocationEditor && (
+
setShowPageLocationEditor(false)}
+ />
)}
- {/* New Folder/Page Editor using PageLocationEditor */}
- {
- setShowPageLocationEditor(false);
- // Make sure to expand the folder where the new content is created
- if (newFolderPath) {
- setExpandedFolders((prev) => ({
- ...prev,
- [newFolderPath]: true,
- }));
- }
- // Refresh the folder structure after creation
- queryClient.invalidateQueries({
- queryKey: folderStructureQueryKey,
- });
- }}
- initialPath={newFolderPath}
- />
-
- {/* Rename Modal */}
{showRenameModal && renamingNode && (
- setShowRenameModal(false)}
- size="sm"
- closeOnEscape={true}
- showCloseButton={true}
- >
+ setShowRenameModal(false)}>
-
- Rename {renamingNode.type === "folder" ? "Folder" : "Page"}
-
-
-
- Current Name
-
-
- {renamingNode.title || renamingNode.name}
-
-
-
-
-
- New Name
-
- {renameConflict && (
-
- Name already exists
-
- )}
-
-
{
- setNewName(e.target.value);
- setRenameConflict(false);
- }}
- className={`w-full rounded-md border px-3 py-2 focus:outline-none focus:ring-2 ${
- renameConflict
- ? "border-red-500 focus:ring-red-200"
- : "focus:ring-primary"
- }`}
- placeholder="new-name"
- />
-
-
-
+ New name for "{renamingNode.title || renamingNode.name}
+ ":
+
+ {
+ setNewName(e.target.value);
+ setRenameConflict(false); // Reset conflict on change
+ }}
+ className={`border-border-default w-full rounded border p-2 ${
+ renameConflict ? "border-error" : ""
+ }`}
+ />
+ {renameConflict && (
+
+ A page or folder with this name already exists here.
+
+ )}
+
+ setShowRenameModal(false)}
- className="text-text-secondary hover:bg-background-level2 border-border-default rounded-md border px-3 py-1.5 text-sm font-medium transition-colors"
+ variant="destructive"
>
Cancel
-
-
+
Rename
-
+
)}
- {/* Move Modal using PageLocationEditor */}
- {movingNode && (
+ {showMoveModal && movingNode && (
{
- setShowMoveModal(false);
- setMovingNode(null);
- }}
- initialPath={movingNode.path}
+ onClose={() => setShowMoveModal(false)}
pageId={movingNode.id}
+ initialPath={movingNode.path}
pageTitle={movingNode.title || movingNode.name}
initialName={movingNode.name}
/>
diff --git a/apps/web/src/components/wiki/WikiLockInfo.tsx b/apps/web/src/components/wiki/WikiLockInfo.tsx
index 7721975..54458ef 100644
--- a/apps/web/src/components/wiki/WikiLockInfo.tsx
+++ b/apps/web/src/components/wiki/WikiLockInfo.tsx
@@ -5,7 +5,14 @@ import { useTRPC } from "~/server/client";
import { useMutation } from "@tanstack/react-query";
import { useNotification } from "~/lib/hooks/useNotification";
import { formatDistanceToNow } from "date-fns";
-import { Button } from "@repo/ui";
+import {
+ Button,
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@repo/ui";
+import { LockIcon, UnlockIcon, PencilIcon, XCircleIcon } from "lucide-react";
interface WikiLockInfoProps {
pageId: number;
@@ -14,6 +21,7 @@ interface WikiLockInfoProps {
lockedUntil?: string | null;
isCurrentUserLockOwner: boolean;
editPath: string;
+ displayMode?: "full" | "header";
}
export function WikiLockInfo({
@@ -23,6 +31,7 @@ export function WikiLockInfo({
lockedUntil,
isCurrentUserLockOwner,
editPath,
+ displayMode = "full",
}: WikiLockInfoProps) {
const router = useRouter();
const notification = useNotification();
@@ -51,44 +60,76 @@ export function WikiLockInfo({
releaseLockMutation.mutate({ id: pageId });
};
+ const lockTooltipContent = `Locked by ${lockedByName || "another user"}${lockedUntil ? ` (expires ${formatDistanceToNow(new Date(lockedUntil), { addSuffix: true })})` : ""}`;
+
+ // Header display mode
+ if (displayMode === "header") {
+ if (!isLocked) {
+ return null;
+ }
+
+ const lockText = isCurrentUserLockOwner ? "Editing" : "Locked";
+ const iconColor = isCurrentUserLockOwner
+ ? "text-blue-500"
+ : "text-orange-500";
+
+ return (
+
+
+
+
+
+
+ {lockText}
+
+
+
+ {lockTooltipContent}
+
+
+
+
+ {isCurrentUserLockOwner && (
+
+
+
+
+
+ )}
+
+ );
+ }
+
+ // Full display mode (original logic)
if (!isLocked) {
return (
-
-
-
+
Unlocked
-
- Edit
-
);
}
- // If locked, render the lock status and relevant actions
+ // If locked, render the full lock status and relevant actions
return (
-
+
+
{isCurrentUserLockOwner
? "You are currently editing this page"
: `This page is being edited by ${lockedByName || "another user"}`}
{lockedUntil && new Date(lockedUntil) > new Date() && (
-
+
Lock expires{" "}
{formatDistanceToNow(new Date(lockedUntil), { addSuffix: true })}
@@ -98,14 +139,6 @@ export function WikiLockInfo({
{isCurrentUserLockOwner ? (
<>
-
- Continue Editing
-
{
- // utils.wiki.getFolderStructure.invalidate();
router.refresh();
},
})
@@ -168,57 +153,9 @@ export function WikiPage({
{/* Breadcrumbs */}
-
-
-
-
{title}
-
- {/* Combined Actions and Lock Info Area */}
-
- {" "}
- {/* Rename/Move Icons */}
-
- {hasPageUpdatePermission && (
-
- {" "}
- {/* Container for icons */}
-
-
-
-
-
-
-
- )}
-
- {/* Lock status - Moved here */}
-
- {hasPageUpdatePermission && (
-
- )}
-
-
-
+
+ {/* Page metadata - simpler now */}
+ {/* Tags shown only on mobile */}
+ {/* Tags display moved below metadata */}
{tags.length > 0 && (
-
+
+
+ Tags:
+
{tags.map((tag) => (
{tag.name}
diff --git a/apps/web/src/lib/hooks/useLocalStorage.ts b/apps/web/src/lib/hooks/useLocalStorage.ts
index 24c3ecd..44f41b0 100644
--- a/apps/web/src/lib/hooks/useLocalStorage.ts
+++ b/apps/web/src/lib/hooks/useLocalStorage.ts
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from "react";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
export function useLocalStorage
(
key: string,
diff --git a/apps/web/src/lib/markdown/core/plugins.ts b/apps/web/src/lib/markdown/core/plugins.ts
index 1ac903f..9d33f50 100644
--- a/apps/web/src/lib/markdown/core/plugins.ts
+++ b/apps/web/src/lib/markdown/core/plugins.ts
@@ -10,7 +10,7 @@ import remarkDirective from "remark-directive";
import remarkDirectiveRehype from "remark-directive-rehype";
import rehypeHighlight from "rehype-highlight";
import type { PluggableList } from "unified";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
// Import custom plugins
import { customPlugins } from "../plugins";
diff --git a/apps/web/src/lib/markdown/plugins/server-only/loggerPlugin.ts b/apps/web/src/lib/markdown/plugins/server-only/loggerPlugin.ts
index ce6591d..8110de4 100644
--- a/apps/web/src/lib/markdown/plugins/server-only/loggerPlugin.ts
+++ b/apps/web/src/lib/markdown/plugins/server-only/loggerPlugin.ts
@@ -5,7 +5,7 @@
import { visit } from "unist-util-visit";
import type { Plugin } from "unified";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
interface LoggerPluginOptions {
/**
diff --git a/apps/web/src/lib/markdown/plugins/server-only/rehypeWikiLinks.ts b/apps/web/src/lib/markdown/plugins/server-only/rehypeWikiLinks.ts
index 5422281..04ee97f 100644
--- a/apps/web/src/lib/markdown/plugins/server-only/rehypeWikiLinks.ts
+++ b/apps/web/src/lib/markdown/plugins/server-only/rehypeWikiLinks.ts
@@ -12,7 +12,7 @@ import type { Element, Root } from "hast";
import { db } from "@repo/db";
import { wikiPages } from "@repo/db";
import { inArray } from "drizzle-orm";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
// Cache configuration
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minute cache lifetime
diff --git a/apps/web/src/lib/markdown/server-factory.ts b/apps/web/src/lib/markdown/server-factory.ts
index d368b03..75f6937 100644
--- a/apps/web/src/lib/markdown/server-factory.ts
+++ b/apps/web/src/lib/markdown/server-factory.ts
@@ -12,7 +12,7 @@ import remarkDirectiveRehype from "remark-directive-rehype";
import rehypeHighlight from "rehype-highlight";
import { customPlugins } from "./plugins";
import { markdownOptions } from "./core/config";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Remark plugins for server-side use
diff --git a/apps/web/src/lib/markdown/server.ts b/apps/web/src/lib/markdown/server.ts
index f389c7e..b3d0b1d 100644
--- a/apps/web/src/lib/markdown/server.ts
+++ b/apps/web/src/lib/markdown/server.ts
@@ -8,7 +8,7 @@ import remarkParse from "remark-parse";
import rehypeStringify from "rehype-stringify";
import remarkRehype from "remark-rehype";
import { createServerMarkdownProcessor } from "./server-factory";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Renders Markdown content to HTML string for server-side rendering
diff --git a/apps/web/src/lib/markdown/utils/testing.ts b/apps/web/src/lib/markdown/utils/testing.ts
index a0c9b52..43d63ab 100644
--- a/apps/web/src/lib/markdown/utils/testing.ts
+++ b/apps/web/src/lib/markdown/utils/testing.ts
@@ -4,7 +4,7 @@
import { createClientMarkdownProcessor } from "../client-factory";
import { renderMarkdownToHtml } from "../server";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Normalizes HTML structure to account for differences between server-rendered HTML
diff --git a/apps/web/src/lib/permissions/client.ts b/apps/web/src/lib/permissions/client.ts
index 7503dde..a98be36 100644
--- a/apps/web/src/lib/permissions/client.ts
+++ b/apps/web/src/lib/permissions/client.ts
@@ -9,7 +9,7 @@ import {
validatePermissionId,
getAllPermissionIds,
} from "@repo/db/client";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Client-side function to check if a permission identifier is valid
diff --git a/apps/web/src/lib/permissions/validation.ts b/apps/web/src/lib/permissions/validation.ts
index 9882814..b887278 100644
--- a/apps/web/src/lib/permissions/validation.ts
+++ b/apps/web/src/lib/permissions/validation.ts
@@ -7,7 +7,7 @@ import { db } from "@repo/db";
import { permissions } from "@repo/db";
import { eq } from "drizzle-orm";
import { getAllPermissions, createPermissionId } from "@repo/db";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Validates permissions in the database against the registry
diff --git a/apps/web/src/lib/services/assets.ts b/apps/web/src/lib/services/assets.ts
index 9c867f9..6c6c984 100644
--- a/apps/web/src/lib/services/assets.ts
+++ b/apps/web/src/lib/services/assets.ts
@@ -1,5 +1,5 @@
import { db } from "@repo/db";
-import { assets, assetsToPages } from "@repo/db";
+import { assets, assetsToPages, users } from "@repo/db";
import { eq, like, ilike, and, sql, SQL } from "drizzle-orm";
import {
PaginationInput,
@@ -20,6 +20,12 @@ export const assetService = {
async getById(id: string) {
return db.query.assets.findFirst({
where: eq(assets.id, id),
+ with: {
+ uploadedBy: true,
+ },
+ columns: {
+ data: false,
+ },
});
},
@@ -29,9 +35,13 @@ export const assetService = {
async getAll() {
return db.query.assets.findMany({
orderBy: (assets, { desc }) => [desc(assets.createdAt)],
+ limit: 1000,
with: {
uploadedBy: true,
},
+ columns: {
+ data: false,
+ },
});
},
@@ -81,6 +91,9 @@ export const assetService = {
// Get paginated assets
const items = await db.query.assets.findMany({
+ columns: {
+ data: false,
+ },
where: whereClause,
orderBy: (assets, { desc }) => [desc(assets.createdAt)],
limit: take,
@@ -99,14 +112,29 @@ export const assetService = {
async getByPageId(pageId: number) {
// Using the junction table to get assets for a page
const assetsForPage = await db
- .select()
+ .select({
+ id: assets.id,
+ fileName: assets.fileName,
+ fileType: assets.fileType,
+ fileSize: assets.fileSize,
+ name: assets.name,
+ description: assets.description,
+ uploadedById: assets.uploadedById,
+ createdAt: assets.createdAt,
+ uploadedBy: {
+ id: users.id,
+ name: users.name,
+ image: users.image,
+ },
+ })
.from(assets)
.innerJoin(assetsToPages, eq(assets.id, assetsToPages.assetId))
+ .innerJoin(users, eq(assets.uploadedById, users.id))
.where(eq(assetsToPages.pageId, pageId))
.orderBy(assets.createdAt);
// Map the results to return just the assets
- return assetsForPage.map((row) => row.assets);
+ return assetsForPage;
},
/**
diff --git a/apps/web/src/lib/services/authorization.ts b/apps/web/src/lib/services/authorization.ts
index 2431051..7685f43 100644
--- a/apps/web/src/lib/services/authorization.ts
+++ b/apps/web/src/lib/services/authorization.ts
@@ -8,7 +8,7 @@ import {
} from "@repo/db";
import { eq, and, inArray, or, isNull } from "drizzle-orm";
import { PermissionIdentifier, validatePermissionId } from "@repo/db";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Authorization Service
diff --git a/apps/web/src/lib/services/locks.ts b/apps/web/src/lib/services/locks.ts
index 6f38200..4c9b2b4 100644
--- a/apps/web/src/lib/services/locks.ts
+++ b/apps/web/src/lib/services/locks.ts
@@ -1,7 +1,7 @@
import { db } from "@repo/db";
import { wikiPages } from "@repo/db";
import { eq, sql } from "drizzle-orm";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
// Software lock timeout in minutes
const LOCK_TIMEOUT_MINUTES = 5;
diff --git a/apps/web/src/lib/services/markdown.ts b/apps/web/src/lib/services/markdown.ts
index c732a45..64d11b3 100644
--- a/apps/web/src/lib/services/markdown.ts
+++ b/apps/web/src/lib/services/markdown.ts
@@ -7,7 +7,7 @@ import { db } from "@repo/db";
import { wikiPages } from "@repo/db";
import { eq } from "drizzle-orm";
import { invalidatePageExistenceCache } from "~/lib/markdown/plugins/server-only/rehypeWikiLinks";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Renders markdown content to HTML with enhanced wiki features
diff --git a/apps/web/src/lib/services/search.ts b/apps/web/src/lib/services/search.ts
index e4e77c2..cfa6d86 100644
--- a/apps/web/src/lib/services/search.ts
+++ b/apps/web/src/lib/services/search.ts
@@ -2,7 +2,7 @@ import { db } from "@repo/db";
import { wikiPages } from "@repo/db";
import { sql, count as drizzleCount } from "drizzle-orm";
import { PaginationInput, getPaginationParams } from "~/lib/utils/pagination";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
// Define the structure of a search result item
export interface SearchResultItem {
diff --git a/apps/web/src/lib/services/tags.ts b/apps/web/src/lib/services/tags.ts
index e7529d1..3e75483 100644
--- a/apps/web/src/lib/services/tags.ts
+++ b/apps/web/src/lib/services/tags.ts
@@ -1,5 +1,6 @@
import { db } from "@repo/db";
import { wikiTags, wikiPageToTag } from "@repo/db";
+import { logger } from "@repo/logger";
import { eq, ilike } from "drizzle-orm";
/**
@@ -8,8 +9,10 @@ import { eq, ilike } from "drizzle-orm";
export const tagService = {
/**
* Get all tags
+ * @deprecated Implement pagination
*/
async getAll() {
+ logger.warn("Using deprecated tagService.getAll()");
return db.query.wikiTags.findMany({
orderBy: (tags, { asc }) => [asc(tags.name)],
});
diff --git a/apps/web/src/lib/services/wiki.ts b/apps/web/src/lib/services/wiki.ts
index 239c30a..5a05d3b 100644
--- a/apps/web/src/lib/services/wiki.ts
+++ b/apps/web/src/lib/services/wiki.ts
@@ -8,7 +8,7 @@ import {
import { desc, eq, sql } from "drizzle-orm";
import { lockService } from "~/lib/services";
import { Transaction } from "~/types/db";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Wiki service - handles all wiki-related database operations
diff --git a/apps/web/src/lib/utils/server-auth-helpers.ts b/apps/web/src/lib/utils/server-auth-helpers.ts
index e71453e..da9d0e8 100644
--- a/apps/web/src/lib/utils/server-auth-helpers.ts
+++ b/apps/web/src/lib/utils/server-auth-helpers.ts
@@ -4,7 +4,7 @@ import { authOptions } from "~/lib/auth";
import { authorizationService } from "~/lib/services";
import { db } from "@repo/db";
import type { PermissionIdentifier } from "@repo/db";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
interface PermissionCheckResult {
authorized: boolean;
diff --git a/apps/web/src/server/context.ts b/apps/web/src/server/context.ts
index 4490401..cdd4c44 100644
--- a/apps/web/src/server/context.ts
+++ b/apps/web/src/server/context.ts
@@ -7,7 +7,7 @@ import { getServerSession, Session } from "next-auth";
// Socket type might be needed later for WS handling, keep for now
// import type { Socket } from "net";
import { authOptions, getServerAuthSession } from "~/lib/auth";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* Creates context for an incoming request
@@ -31,12 +31,12 @@ export const createContext = async (
opts.req !== null &&
"query" in opts.req
) {
- logger.log("Creating context for Next.js Pages API route");
+ logger.debug("Creating context for Next.js Pages API route");
// Type assertion needed as we've confirmed the structure
session = await getServerAuthSession();
// Check for Fetch API context (has req but not res, used in App Router)
} else if ("req" in opts && !("res" in opts)) {
- logger.log("Creating context for Fetch API (App Router)");
+ logger.debug("Creating context for Fetch API (App Router)");
// In App Router Route Handlers, getServerSession(authOptions) usually works
session = await getServerSession(authOptions);
// If session is null, you might need to manually extract/verify cookies/headers
@@ -54,7 +54,7 @@ export const createContext = async (
// throw new Error("Could not determine context type");
}
- logger.log(
+ logger.debug(
"createContext created for",
session?.user?.name ?? "unknown user"
);
diff --git a/apps/web/src/server/index.ts b/apps/web/src/server/index.ts
index d2f8dbb..b88aadf 100644
--- a/apps/web/src/server/index.ts
+++ b/apps/web/src/server/index.ts
@@ -4,7 +4,7 @@ import { authorizationService } from "~/lib/services/authorization";
import { PermissionIdentifier, validatePermissionId } from "@repo/db";
import type { TRPCPanelMeta } from "trpc-ui";
import { Context } from "./context";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
// Initialize tRPC server instance
const t = initTRPC.context().meta().create();
@@ -54,6 +54,10 @@ const allowGuests = middleware(async ({ ctx, next }) => {
});
});
+/**
+ * Procedure that allows guest access
+ * @abstract Better than publicProcedure because it adds context if user is logged in
+ */
export const guestProcedure = t.procedure.use(allowGuests);
// Create middleware that checks for a specific permission
diff --git a/apps/web/src/server/routers/groups.ts b/apps/web/src/server/routers/admin/groups.ts
similarity index 83%
rename from apps/web/src/server/routers/groups.ts
rename to apps/web/src/server/routers/admin/groups.ts
index 475bb3d..37e4be0 100644
--- a/apps/web/src/server/routers/groups.ts
+++ b/apps/web/src/server/routers/admin/groups.ts
@@ -2,16 +2,22 @@ import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { router, permissionProtectedProcedure } from "~/server";
import { dbService } from "~/lib/services";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
export const groupsRouter = router({
- // Get all groups
+ /**
+ * Get all groups
+ * @requires system:groups:read
+ */
getAll: permissionProtectedProcedure("system:groups:read").query(async () => {
const groups = await dbService.groups.getAll();
return groups;
}),
- // Get a single group by ID
+ /**
+ * Get a single group by ID
+ * @requires system:groups:read
+ */
getById: permissionProtectedProcedure("system:groups:read")
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
@@ -25,7 +31,10 @@ export const groupsRouter = router({
return group;
}),
- // Get all users in a group
+ /**
+ * Get all users in a group
+ * @requires system:groups:read
+ */
getGroupUsers: permissionProtectedProcedure("system:groups:read")
.input(z.object({ groupId: z.number() }))
.query(async ({ input }) => {
@@ -33,7 +42,10 @@ export const groupsRouter = router({
return users;
}),
- // Get all permissions for a group
+ /**
+ * Get all permissions for a group
+ * @requires system:groups:read
+ */
getGroupPermissions: permissionProtectedProcedure("system:groups:read")
.input(z.object({ groupId: z.number() }))
.query(async ({ input }) => {
@@ -43,7 +55,10 @@ export const groupsRouter = router({
return permissions;
}),
- // Create a new group
+ /**
+ * Create a new group
+ * @requires system:groups:create
+ */
create: permissionProtectedProcedure("system:groups:create")
.input(
z.object({
@@ -56,7 +71,10 @@ export const groupsRouter = router({
return newGroup;
}),
- // Update a group
+ /**
+ * Update a group
+ * @requires system:groups:update
+ */
update: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -77,7 +95,10 @@ export const groupsRouter = router({
return group;
}),
- // Delete a group
+ /**
+ * Delete a group
+ * @requires system:groups:delete
+ */
delete: permissionProtectedProcedure("system:groups:delete")
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
@@ -105,7 +126,10 @@ export const groupsRouter = router({
return { success: true };
}),
- // Add users to a group
+ /**
+ * Add users to a group
+ * @requires system:groups:update
+ */
addUsers: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -121,7 +145,10 @@ export const groupsRouter = router({
return result;
}),
- // Remove users from a group
+ /**
+ * Remove users from a group
+ * @requires system:groups:update
+ */
removeUsers: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -146,7 +173,10 @@ export const groupsRouter = router({
return result;
}),
- // Add permissions to a group
+ /**
+ * Add permissions to a group
+ * @requires system:groups:update
+ */
addPermissions: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -162,7 +192,10 @@ export const groupsRouter = router({
return result;
}),
- // Remove permissions from a group
+ /**
+ * Remove permissions from a group
+ * @requires system:groups:update
+ */
removePermissions: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -178,7 +211,10 @@ export const groupsRouter = router({
return result;
}),
- // Add module permissions to a group
+ /**
+ * Add module permissions to a group
+ * @requires system:groups:update
+ */
addModulePermissions: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -199,7 +235,10 @@ export const groupsRouter = router({
return result;
}),
- // Add action permissions to a group
+ /**
+ * Add action permissions to a group
+ * @requires system:groups:update
+ */
addActionPermissions: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -220,7 +259,10 @@ export const groupsRouter = router({
return result;
}),
- // Get module permissions for a group
+ /**
+ * Get module permissions for a group
+ * @requires system:groups:read
+ */
getModulePermissions: permissionProtectedProcedure("system:groups:read")
.input(z.object({ groupId: z.number() }))
.query(async ({ input }) => {
@@ -230,7 +272,10 @@ export const groupsRouter = router({
return permissions;
}),
- // Get action permissions for a group
+ /**
+ * Get action permissions for a group
+ * @requires system:groups:read
+ */
getActionPermissions: permissionProtectedProcedure("system:groups:read")
.input(z.object({ groupId: z.number() }))
.query(async ({ input }) => {
@@ -240,7 +285,10 @@ export const groupsRouter = router({
return permissions;
}),
- // Remove module permissions from a group
+ /**
+ * Remove module permissions from a group
+ * @requires system:groups:update
+ */
removeModulePermissions: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
@@ -256,7 +304,10 @@ export const groupsRouter = router({
return result;
}),
- // Remove action permissions from a group
+ /**
+ * Remove action permissions from a group
+ * @requires system:groups:update
+ */
removeActionPermissions: permissionProtectedProcedure("system:groups:update")
.input(
z.object({
diff --git a/apps/web/src/server/routers/admin/index.ts b/apps/web/src/server/routers/admin/index.ts
new file mode 100644
index 0000000..3a9f573
--- /dev/null
+++ b/apps/web/src/server/routers/admin/index.ts
@@ -0,0 +1,16 @@
+import { router } from "~/server";
+import { groupsRouter } from "./groups";
+import { permissionsRouter } from "./permissions";
+import { usersRouter } from "./users";
+
+/**
+ * Main router for admin-specific procedures.
+ * Sub-routers for different admin areas (e.g., users, groups) should be merged here.
+ */
+export const adminRouter = router({
+ groups: groupsRouter,
+ permissions: permissionsRouter,
+ users: usersRouter,
+});
+
+export type AdminRouter = typeof adminRouter;
diff --git a/apps/web/src/server/routers/permissions.ts b/apps/web/src/server/routers/admin/permissions.ts
similarity index 90%
rename from apps/web/src/server/routers/permissions.ts
rename to apps/web/src/server/routers/admin/permissions.ts
index 8d2d7d9..68d7e23 100644
--- a/apps/web/src/server/routers/permissions.ts
+++ b/apps/web/src/server/routers/admin/permissions.ts
@@ -4,7 +4,9 @@ import { router, permissionProtectedProcedure } from "~/server";
import { dbService } from "~/lib/services";
export const permissionsRouter = router({
- // Get all permissions
+ /**
+ * Get all permissions
+ */
getAll: permissionProtectedProcedure("system:permissions:read").query(
async () => {
const permissions = await dbService.permissions.getAll();
@@ -12,7 +14,9 @@ export const permissionsRouter = router({
}
),
- // Get all unique modules
+ /**
+ * Get all unique modules
+ */
getModules: permissionProtectedProcedure("system:permissions:read").query(
async () => {
const permissions = await dbService.permissions.getAll();
@@ -21,7 +25,9 @@ export const permissionsRouter = router({
}
),
- // Get all unique actions
+ /**
+ * Get all unique actions
+ */
getActions: permissionProtectedProcedure("system:permissions:read").query(
async () => {
const permissions = await dbService.permissions.getAll();
@@ -30,7 +36,9 @@ export const permissionsRouter = router({
}
),
- // Get a permission by ID
+ /**
+ * Get a permission by ID
+ */
getById: permissionProtectedProcedure("system:settings:read")
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
@@ -44,7 +52,9 @@ export const permissionsRouter = router({
return permission;
}),
- // Create a new permission (very restricted operation)
+ /**
+ * Create a new permission (very restricted operation)
+ */
create: permissionProtectedProcedure("system:settings:update")
.input(
z.object({
@@ -62,7 +72,9 @@ export const permissionsRouter = router({
return newPermission;
}),
- // Update a permission
+ /**
+ * Update a permission
+ */
update: permissionProtectedProcedure("system:settings:update")
.input(
z.object({
@@ -85,7 +97,9 @@ export const permissionsRouter = router({
return permission;
}),
- // Delete a permission
+ /**
+ * Delete a permission
+ */
delete: permissionProtectedProcedure("system:settings:update")
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
diff --git a/apps/web/src/server/routers/admin/users.ts b/apps/web/src/server/routers/admin/users.ts
new file mode 100644
index 0000000..c2715c4
--- /dev/null
+++ b/apps/web/src/server/routers/admin/users.ts
@@ -0,0 +1,51 @@
+import { z } from "zod";
+import { TRPCError } from "@trpc/server";
+import { router, permissionProtectedProcedure } from "~/server";
+import { dbService } from "~/lib/services";
+
+export const usersRouter = router({
+ /**
+ * Get all users
+ * @requires system:users:read
+ */
+ getAll: permissionProtectedProcedure("system:users:read").query(async () => {
+ const users = await dbService.users.getAll();
+ return users;
+ }),
+
+ /**
+ * Get the total count of users
+ * @requires system:users:read
+ */
+ count: permissionProtectedProcedure("system:users:read").query(async () => {
+ return await dbService.users.count();
+ }),
+
+ /**
+ * Get a single user by ID
+ * @requires system:users:read
+ */
+ getById: permissionProtectedProcedure("system:users:read")
+ .input(z.object({ id: z.number() }))
+ .query(async ({ input }) => {
+ const user = await dbService.users.getById(input.id);
+ if (!user) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+ return user;
+ }),
+
+ /**
+ * Get user groups by ID
+ * @requires system:users:read
+ */
+ getUserGroups: permissionProtectedProcedure("system:users:read")
+ .input(z.object({ id: z.number() }))
+ .query(async ({ input }) => {
+ const userGroups = await dbService.users.getUserGroups(input.id);
+ return userGroups;
+ }),
+});
diff --git a/apps/web/src/server/routers/assets.ts b/apps/web/src/server/routers/assets.ts
index 4e26eb2..22a2db3 100644
--- a/apps/web/src/server/routers/assets.ts
+++ b/apps/web/src/server/routers/assets.ts
@@ -3,15 +3,35 @@ import { assetService } from "~/lib/services";
import { permissionProtectedProcedure, router } from "~/server";
import { TRPCError } from "@trpc/server";
import { paginationSchema } from "~/lib/utils/pagination";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
const FILE_SIZE_LIMIT_MB = 100; // 100MB
+const AssetDTO = z.object({
+ id: z.string().uuid(),
+ fileName: z.string(),
+ fileType: z.string(),
+ fileSize: z.number(),
+ name: z.string().nullable(),
+ description: z.string().nullable(),
+ uploadedById: z.number(),
+ createdAt: z.date(),
+});
+
export const assetsRouter = router({
+ /**
+ * Get all assets
+ * @deprecated Use getPaginated instead - this endpoint will be removed in a future version
+ */
getAll: permissionProtectedProcedure("assets:asset:read")
.input(z.object({}).optional())
+ .output(z.array(AssetDTO))
.query(async () => {
- return assetService.getAll();
+ logger.warn(
+ "Deprecated endpoint 'getAll' called - migrate to 'getPaginated'"
+ );
+ const assets = await assetService.getAll();
+ return assets.map((asset) => AssetDTO.parse(asset));
}),
getPaginated: permissionProtectedProcedure("assets:asset:read")
@@ -40,8 +60,10 @@ export const assetsRouter = router({
getById: permissionProtectedProcedure("assets:asset:read")
.input(z.object({ id: z.string().uuid() }))
+ .output(AssetDTO.nullable())
.query(async ({ input }) => {
- return assetService.getById(input.id);
+ const asset = await assetService.getById(input.id);
+ return asset ? AssetDTO.parse(asset) : null;
}),
upload: permissionProtectedProcedure("assets:asset:create")
diff --git a/apps/web/src/server/routers/auth.ts b/apps/web/src/server/routers/auth.ts
index f1b244e..dcda8ec 100644
--- a/apps/web/src/server/routers/auth.ts
+++ b/apps/web/src/server/routers/auth.ts
@@ -2,12 +2,7 @@
* Auth router - Server only
*/
import { z } from "zod";
-import {
- router,
- protectedProcedure,
- guestProcedure,
- publicProcedure,
-} from "~/server";
+import { router, guestProcedure, publicProcedure } from "~/server";
import { authorizationService } from "~/lib/services";
import { PermissionIdentifier, validatePermissionId } from "@repo/db";
@@ -55,7 +50,7 @@ export const authRouter = router({
}),
// Check if the current user has a specific permission
- hasPermission: protectedProcedure
+ hasPermission: guestProcedure
.input(z.object({ permission: permissionIdentifierSchema }))
.query(async ({ ctx, input }) => {
const userId = parseInt(ctx.session.user.id);
@@ -67,7 +62,7 @@ export const authRouter = router({
}),
// Check if the current user has any of the specified permissions
- hasAnyPermission: protectedProcedure
+ hasAnyPermission: guestProcedure
.input(z.object({ permissions: z.array(permissionIdentifierSchema) }))
.query(async ({ ctx, input }) => {
const userId = parseInt(ctx.session.user.id);
@@ -79,7 +74,7 @@ export const authRouter = router({
}),
// Check if the current user has access to a specific page
- hasPagePermission: protectedProcedure
+ hasPagePermission: guestProcedure
.input(
z.object({
pageId: z.number(),
@@ -96,7 +91,10 @@ export const authRouter = router({
return hasPermission;
}),
- // Create a procedure to get guest permissions
+ /*
+ * Get permissions for guest users
+ * @deprecated Use getMyPermissions instead
+ */
getGuestPermissions: guestProcedure
.meta({
description: "Get permissions for guest users",
diff --git a/apps/web/src/server/routers/index.ts b/apps/web/src/server/routers/index.ts
index 07adb6a..f0e9b61 100644
--- a/apps/web/src/server/routers/index.ts
+++ b/apps/web/src/server/routers/index.ts
@@ -1,22 +1,18 @@
import { router } from "..";
import { wikiRouter } from "./wiki";
-import { userRouter } from "./user";
+import { usersRouter } from "./users";
import { searchRouter } from "./search";
import { assetsRouter } from "./assets";
-import { permissionsRouter } from "./permissions";
-import { groupsRouter } from "./groups";
-import { usersRouter } from "./users";
import { authRouter } from "./auth";
import { tagsRouter } from "./tags";
+import { adminRouter } from "./admin";
export const appRouter = router({
+ admin: adminRouter,
wiki: wikiRouter,
- user: userRouter,
users: usersRouter,
search: searchRouter,
assets: assetsRouter,
- permissions: permissionsRouter,
- groups: groupsRouter,
auth: authRouter,
tags: tagsRouter,
});
diff --git a/apps/web/src/server/routers/tags.ts b/apps/web/src/server/routers/tags.ts
index b369b2a..ff243b6 100644
--- a/apps/web/src/server/routers/tags.ts
+++ b/apps/web/src/server/routers/tags.ts
@@ -1,8 +1,8 @@
import { z } from "zod";
import { tagService } from "~/lib/services";
-import { protectedProcedure, router } from "~/server";
+import { permissionProtectedProcedure, router } from "~/server";
import { TRPCError } from "@trpc/server";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
/**
* tRPC router for tag-related operations.
@@ -12,7 +12,7 @@ export const tagsRouter = router({
* Search for tags by name.
* Requires authentication.
*/
- search: protectedProcedure
+ search: permissionProtectedProcedure("wiki:page:read")
.input(
z.object({
query: z.string(),
diff --git a/apps/web/src/server/routers/user.ts b/apps/web/src/server/routers/user.ts
deleted file mode 100644
index b2601c9..0000000
--- a/apps/web/src/server/routers/user.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import {
- publicProcedure,
- protectedProcedure,
- permissionProtectedProcedure,
- router,
-} from "..";
-import { dbService } from "~/lib/services";
-import { hash } from "bcryptjs";
-import { z } from "zod";
-import { TRPCError } from "@trpc/server";
-import { logger } from "~/lib/utils/logger";
-
-// TODO: Move to users router
-
-export const saltRounds = 10;
-
-// Define validation schema for registration
-const registerSchema = z.object({
- name: z.string().min(1, "Name is required"),
- email: z.string().email("Invalid email address"),
- password: z.string().min(8, "Password must be at least 8 characters"),
-});
-
-export const userRouter = router({
- // Get the total count of users
- count: permissionProtectedProcedure("system:users:read").query(async () => {
- return await dbService.users.count();
- }),
-
- // Get current user profile (using session)
- me: protectedProcedure.query(async ({ ctx }) => {
- const userIdString = ctx.session.user.id;
- const userId = parseInt(userIdString, 10);
-
- if (isNaN(userId)) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "Invalid user ID format.",
- });
- }
-
- const user = await dbService.users.getById(userId);
-
- if (!user) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "User not found",
- });
- }
-
- const groups = await dbService.groups.getUserGroups(user.id);
-
- return {
- id: user.id,
- name: user.name,
- email: user.email,
- image: user.image,
- createdAt: user.createdAt,
- updatedAt: user.updatedAt,
- groups: groups.map((g) => ({ id: g.id, name: g.name })),
- };
- }),
-
- // Register a new user
- register: publicProcedure
- .input(registerSchema)
- .mutation(async ({ input }) => {
- try {
- // 1. Check if it's the first user using dbService
- const userCount = await dbService.users.count();
- const isFirstUser = userCount === 0;
-
- // 2. Check if email already exists using dbService
- const existingUser = await dbService.users.findByEmail(input.email);
-
- if (existingUser) {
- throw new TRPCError({
- code: "CONFLICT",
- message: "User with this email already exists",
- });
- }
-
- // 3. Hash the password
- const hashedPassword = await hash(input.password, saltRounds);
-
- // 4. Create the user using dbService
- const newUser = await dbService.users.create({
- name: input.name,
- email: input.email,
- password: hashedPassword,
- });
-
- if (!newUser) {
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: "Failed to create user.",
- });
- }
-
- // 5. If it's the first user, assign to Administrators group
- if (isFirstUser) {
- const adminGroup =
- await dbService.groups.findByName("Administrators");
- if (adminGroup) {
- const added = await dbService.groups.addUserToGroup(
- newUser.id,
- adminGroup.id
- );
- if (added) {
- logger.log(
- `First user ${newUser.email} automatically assigned to Administrators group.`
- );
- } else {
- logger.error(
- `Failed to assign first user ${newUser.email} to Administrators group.`
- );
- }
- } else {
- logger.error(
- "CRITICAL: Administrators group not found during first user registration! Seeding might have failed."
- );
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message:
- "Failed to assign administrative privileges. Administrator group not found.",
- });
- }
- } else {
- // For non-first users, add them to the Viewers group
- const viewerGroup = await dbService.groups.findByName("Viewers");
- if (viewerGroup) {
- const added = await dbService.groups.addUserToGroup(
- newUser.id,
- viewerGroup.id
- );
- if (added) {
- logger.log(
- `New user ${newUser.email} automatically assigned to Viewers group.`
- );
- } else {
- logger.error(
- `Failed to assign new user ${newUser.email} to Viewers group.`
- );
- }
- } else {
- logger.error(
- "CRITICAL: Viewers group not found during user registration! Seeding might have failed."
- );
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message:
- "Failed to assign user to Viewers group. Viewers group not found.",
- });
- }
- }
-
- // Return relevant user info (excluding password)
- return {
- id: newUser.id,
- name: newUser.name,
- email: newUser.email,
- isFirstUser: isFirstUser,
- };
- } catch (error) {
- if (error instanceof TRPCError) {
- throw error;
- }
- logger.error("Registration error:", error);
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: "An error occurred during registration",
- });
- }
- }),
-});
diff --git a/apps/web/src/server/routers/users.ts b/apps/web/src/server/routers/users.ts
index 307e42f..4db198f 100644
--- a/apps/web/src/server/routers/users.ts
+++ b/apps/web/src/server/routers/users.ts
@@ -1,34 +1,171 @@
+import { publicProcedure, protectedProcedure, router } from "..";
+import { dbService } from "~/lib/services";
+import { hash } from "bcryptjs";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
-import { router, permissionProtectedProcedure } from "~/server";
-import { dbService } from "~/lib/services";
+import { logger } from "@repo/logger";
+
+// TODO: Move to users router
+
+export const saltRounds = 10;
+
+// Define validation schema for registration
+const registerSchema = z.object({
+ name: z.string().min(1, "Name is required"),
+ email: z.string().email("Invalid email address"),
+ password: z.string().min(8, "Password must be at least 8 characters"),
+});
export const usersRouter = router({
- // Get all users
- getAll: permissionProtectedProcedure("system:users:read").query(async () => {
- const users = await dbService.users.getAll();
- return users;
+ /**
+ * Get current user profile (using session)
+ * @requires system:users:read
+ */
+ me: protectedProcedure.query(async ({ ctx }) => {
+ const userIdString = ctx.session.user.id;
+ const userId = parseInt(userIdString, 10);
+
+ if (isNaN(userId)) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Invalid user ID format.",
+ });
+ }
+
+ const user = await dbService.users.getById(userId);
+
+ if (!user) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+
+ const groups = await dbService.groups.getUserGroups(user.id);
+
+ return {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ image: user.image,
+ createdAt: user.createdAt,
+ updatedAt: user.updatedAt,
+ groups: groups.map((g) => ({ id: g.id, name: g.name })),
+ };
}),
- // Get a single user by ID
- getById: permissionProtectedProcedure("system:users:read")
- .input(z.object({ id: z.number() }))
- .query(async ({ input }) => {
- const user = await dbService.users.getById(input.id);
- if (!user) {
+ /**
+ * Register a new user
+ * @requires system:users:create
+ */
+ register: publicProcedure
+ .input(registerSchema)
+ .mutation(async ({ input }) => {
+ try {
+ // 1. Check if it's the first user using dbService
+ const userCount = await dbService.users.count();
+ const isFirstUser = userCount === 0;
+
+ // 2. Check if email already exists using dbService
+ const existingUser = await dbService.users.findByEmail(input.email);
+
+ if (existingUser) {
+ throw new TRPCError({
+ code: "CONFLICT",
+ message: "User with this email already exists",
+ });
+ }
+
+ // 3. Hash the password
+ const hashedPassword = await hash(input.password, saltRounds);
+
+ // 4. Create the user using dbService
+ const newUser = await dbService.users.create({
+ name: input.name,
+ email: input.email,
+ password: hashedPassword,
+ });
+
+ if (!newUser) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Failed to create user.",
+ });
+ }
+
+ // 5. If it's the first user, assign to Administrators group
+ if (isFirstUser) {
+ const adminGroup =
+ await dbService.groups.findByName("Administrators");
+ if (adminGroup) {
+ const added = await dbService.groups.addUserToGroup(
+ newUser.id,
+ adminGroup.id
+ );
+ if (added) {
+ logger.log(
+ `First user ${newUser.email} automatically assigned to Administrators group.`
+ );
+ } else {
+ logger.error(
+ `Failed to assign first user ${newUser.email} to Administrators group.`
+ );
+ }
+ } else {
+ logger.error(
+ "CRITICAL: Administrators group not found during first user registration! Seeding might have failed."
+ );
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message:
+ "Failed to assign administrative privileges. Administrator group not found.",
+ });
+ }
+ } else {
+ // For non-first users, add them to the Viewers group
+ const viewerGroup = await dbService.groups.findByName("Viewers");
+ if (viewerGroup) {
+ const added = await dbService.groups.addUserToGroup(
+ newUser.id,
+ viewerGroup.id
+ );
+ if (added) {
+ logger.log(
+ `New user ${newUser.email} automatically assigned to Viewers group.`
+ );
+ } else {
+ logger.error(
+ `Failed to assign new user ${newUser.email} to Viewers group.`
+ );
+ }
+ } else {
+ logger.error(
+ "CRITICAL: Viewers group not found during user registration! Seeding might have failed."
+ );
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message:
+ "Failed to assign user to Viewers group. Viewers group not found.",
+ });
+ }
+ }
+
+ // Return relevant user info (excluding password)
+ return {
+ id: newUser.id,
+ name: newUser.name,
+ email: newUser.email,
+ isFirstUser: isFirstUser,
+ };
+ } catch (error) {
+ if (error instanceof TRPCError) {
+ throw error;
+ }
+ logger.error("Registration error:", error);
throw new TRPCError({
- code: "NOT_FOUND",
- message: "User not found",
+ code: "INTERNAL_SERVER_ERROR",
+ message: "An error occurred during registration",
});
}
- return user;
- }),
-
- // Get user groups by ID
- getUserGroups: permissionProtectedProcedure("system:users:read")
- .input(z.object({ id: z.number() }))
- .query(async ({ input }) => {
- const userGroups = await dbService.users.getUserGroups(input.id);
- return userGroups;
}),
});
diff --git a/apps/web/src/server/routers/wiki.ts b/apps/web/src/server/routers/wiki.ts
index ad2db64..19eaa9a 100644
--- a/apps/web/src/server/routers/wiki.ts
+++ b/apps/web/src/server/routers/wiki.ts
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
-import { desc, eq, like, gt, and, sql } from "drizzle-orm";
+import { desc, eq, gt, and, sql, ilike, or } from "drizzle-orm";
import { db, wikiPages } from "@repo/db";
import {
permissionGuestProcedure,
@@ -9,7 +9,7 @@ import {
router,
} from "..";
import { dbService, wikiService } from "~/lib/services";
-import { logger } from "~/lib/utils/logger";
+import { logger } from "@repo/logger";
// Wiki page input validation schema
const pageInputSchema = z.object({
@@ -346,7 +346,11 @@ export const wikiRouter = router({
// Handle search
if (search) {
- const searchCondition = like(wikiPages.title, `%${search}%`);
+ const searchCondition = or(
+ ilike(wikiPages.title, `%${search}%`),
+ ilike(wikiPages.title, `%${search}`),
+ ilike(wikiPages.title, `${search}%`)
+ );
whereConditions = whereConditions
? and(whereConditions, searchCondition)
: searchCondition;
diff --git a/package.json b/package.json
index bb1b5d2..085a935 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"gen:package": "turbo gen package",
"db:seed": "turbo run db:seed",
"db:setup": "turbo run db:setup",
+ "db:create": "SKIP_DEVELOPER_SEEDS=true turbo run db:setup",
"clean": "turbo run clean",
"fullclean": "turbo run fullclean && rm -rf node_modules .turbo && rm -f pnpm-lock.yaml"
},
diff --git a/packages/db/drizzle/0000_light_sumo.sql b/packages/db/drizzle/0000_light_sumo.sql
new file mode 100644
index 0000000..d3d56d8
--- /dev/null
+++ b/packages/db/drizzle/0000_light_sumo.sql
@@ -0,0 +1,271 @@
+CREATE TYPE "public"."editor_type" AS ENUM('markdown', 'html');
+--> statement-breakpoint
+CREATE TABLE "accounts" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "user_id" integer NOT NULL,
+ "type" varchar(255) NOT NULL,
+ "provider" varchar(255) NOT NULL,
+ "provider_account_id" varchar(255) NOT NULL,
+ "refresh_token" text,
+ "access_token" text,
+ "expires_at" integer,
+ "token_type" varchar(255),
+ "scope" varchar(255),
+ "id_token" text,
+ "session_state" varchar(255),
+ "created_at" timestamp DEFAULT now(),
+ "updated_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "assets" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "name" varchar(255),
+ "description" text,
+ "file_name" varchar(255) NOT NULL,
+ "file_type" varchar(100) NOT NULL,
+ "file_size" integer NOT NULL,
+ "data" text NOT NULL,
+ "uploaded_by_id" integer NOT NULL,
+ "created_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "assets_to_pages" (
+ "asset_id" uuid NOT NULL,
+ "page_id" integer NOT NULL,
+ CONSTRAINT "assets_to_pages_asset_id_page_id_pk" PRIMARY KEY("asset_id", "page_id")
+);
+--> statement-breakpoint
+CREATE TABLE "group_action_permissions" (
+ "group_id" integer NOT NULL,
+ "action" varchar(50) NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "group_action_permissions_group_id_action_pk" PRIMARY KEY("group_id", "action")
+);
+--> statement-breakpoint
+CREATE TABLE "group_module_permissions" (
+ "group_id" integer NOT NULL,
+ "module" varchar(50) NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "group_module_permissions_group_id_module_pk" PRIMARY KEY("group_id", "module")
+);
+--> statement-breakpoint
+CREATE TABLE "group_permissions" (
+ "group_id" integer NOT NULL,
+ "permission_id" integer NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "group_permissions_group_id_permission_id_pk" PRIMARY KEY("group_id", "permission_id")
+);
+--> statement-breakpoint
+CREATE TABLE "groups" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar(100) NOT NULL,
+ "description" text,
+ "created_at" timestamp DEFAULT now(),
+ "updated_at" timestamp DEFAULT now(),
+ "is_system" boolean DEFAULT false,
+ "is_editable" boolean DEFAULT true,
+ "allow_user_assignment" boolean DEFAULT true,
+ CONSTRAINT "groups_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+CREATE TABLE "page_permissions" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "page_id" integer NOT NULL,
+ "group_id" integer,
+ "permission_id" integer NOT NULL,
+ "permission_type" varchar(10) DEFAULT 'allow' NOT NULL,
+ "created_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "permissions" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "module" varchar(50) NOT NULL,
+ "resource" varchar(50) NOT NULL,
+ "action" varchar(50) NOT NULL,
+ "name" varchar(100) GENERATED ALWAYS AS ("module" || ':' || "resource" || ':' || "action") STORED NOT NULL,
+ "description" text,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "permissions_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+CREATE TABLE "sessions" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "session_token" varchar(255) NOT NULL,
+ "user_id" integer NOT NULL,
+ "expires" timestamp NOT NULL,
+ CONSTRAINT "sessions_session_token_unique" UNIQUE("session_token")
+);
+--> statement-breakpoint
+CREATE TABLE "user_groups" (
+ "user_id" integer NOT NULL,
+ "group_id" integer NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "user_groups_user_id_group_id_pk" PRIMARY KEY("user_id", "group_id")
+);
+--> statement-breakpoint
+CREATE TABLE "users" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar(255),
+ "email" varchar(255) NOT NULL,
+ "password" varchar(255),
+ "email_verified" timestamp,
+ "image" text,
+ "created_at" timestamp DEFAULT now(),
+ "updated_at" timestamp DEFAULT now(),
+ CONSTRAINT "users_email_unique" UNIQUE("email")
+);
+--> statement-breakpoint
+CREATE TABLE "verification_tokens" (
+ "identifier" varchar(255) NOT NULL,
+ "token" varchar(255) NOT NULL,
+ "expires" timestamp NOT NULL,
+ CONSTRAINT "verification_tokens_identifier_token_pk" PRIMARY KEY("identifier", "token")
+);
+--> statement-breakpoint
+CREATE TABLE "wiki_page_revisions" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "page_id" integer NOT NULL,
+ "content" text NOT NULL,
+ "created_by_id" integer,
+ "created_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "wiki_page_to_tag" (
+ "page_id" integer NOT NULL,
+ "tag_id" integer NOT NULL,
+ CONSTRAINT "wiki_page_to_tag_page_id_tag_id_pk" PRIMARY KEY("page_id", "tag_id")
+);
+--> statement-breakpoint
+CREATE TABLE "wiki_pages" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "path" varchar(1000) NOT NULL,
+ "title" varchar(255) NOT NULL,
+ "content" text,
+ "rendered_html" text,
+ "editor_type" "editor_type",
+ "is_published" boolean DEFAULT false,
+ "created_by_id" integer NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ "updated_by_id" integer NOT NULL,
+ "updated_at" timestamp DEFAULT now(),
+ "rendered_html_updated_at" timestamp,
+ "locked_by_id" integer,
+ "locked_at" timestamp,
+ "lock_expires_at" timestamp,
+ "search" "tsvector" GENERATED ALWAYS AS (
+ setweight(
+ to_tsvector('english', "wiki_pages"."title"),
+ 'A'
+ ) || setweight(
+ to_tsvector('english', "wiki_pages"."content"),
+ 'B'
+ )
+ ) STORED NOT NULL,
+ CONSTRAINT "wiki_pages_path_unique" UNIQUE("path")
+);
+--> statement-breakpoint
+CREATE TABLE "wiki_tags" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar(100) NOT NULL,
+ "description" text,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "wiki_tags_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+ALTER TABLE "accounts"
+ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "assets"
+ADD CONSTRAINT "assets_uploaded_by_id_users_id_fk" FOREIGN KEY ("uploaded_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "assets_to_pages"
+ADD CONSTRAINT "assets_to_pages_asset_id_assets_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "assets_to_pages"
+ADD CONSTRAINT "assets_to_pages_page_id_wiki_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."wiki_pages"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "group_action_permissions"
+ADD CONSTRAINT "group_action_permissions_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "group_module_permissions"
+ADD CONSTRAINT "group_module_permissions_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "group_permissions"
+ADD CONSTRAINT "group_permissions_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "group_permissions"
+ADD CONSTRAINT "group_permissions_permission_id_permissions_id_fk" FOREIGN KEY ("permission_id") REFERENCES "public"."permissions"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "page_permissions"
+ADD CONSTRAINT "page_permissions_page_id_wiki_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."wiki_pages"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "page_permissions"
+ADD CONSTRAINT "page_permissions_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "page_permissions"
+ADD CONSTRAINT "page_permissions_permission_id_permissions_id_fk" FOREIGN KEY ("permission_id") REFERENCES "public"."permissions"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "sessions"
+ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "user_groups"
+ADD CONSTRAINT "user_groups_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "user_groups"
+ADD CONSTRAINT "user_groups_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "wiki_page_revisions"
+ADD CONSTRAINT "wiki_page_revisions_page_id_wiki_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."wiki_pages"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "wiki_page_revisions"
+ADD CONSTRAINT "wiki_page_revisions_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "wiki_page_to_tag"
+ADD CONSTRAINT "wiki_page_to_tag_page_id_wiki_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."wiki_pages"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "wiki_page_to_tag"
+ADD CONSTRAINT "wiki_page_to_tag_tag_id_wiki_tags_id_fk" FOREIGN KEY ("tag_id") REFERENCES "public"."wiki_tags"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "wiki_pages"
+ADD CONSTRAINT "wiki_pages_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "wiki_pages"
+ADD CONSTRAINT "wiki_pages_updated_by_id_users_id_fk" FOREIGN KEY ("updated_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+ALTER TABLE "wiki_pages"
+ADD CONSTRAINT "wiki_pages_locked_by_id_users_id_fk" FOREIGN KEY ("locked_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
+--> statement-breakpoint
+CREATE INDEX "asset_page_idx" ON "assets_to_pages" USING btree ("asset_id", "page_id");
+--> statement-breakpoint
+CREATE INDEX "group_action_permissions_idx" ON "group_action_permissions" USING btree ("group_id", "action");
+--> statement-breakpoint
+CREATE INDEX "group_module_permissions_idx" ON "group_module_permissions" USING btree ("group_id", "module");
+--> statement-breakpoint
+CREATE INDEX "group_permission_idx" ON "group_permissions" USING btree ("group_id", "permission_id");
+--> statement-breakpoint
+CREATE INDEX "page_group_perm_idx" ON "page_permissions" USING btree ("page_id", "permission_id", "group_id");
+--> statement-breakpoint
+CREATE INDEX "user_group_idx" ON "user_groups" USING btree ("user_id", "group_id");
+--> statement-breakpoint
+CREATE INDEX "email_idx" ON "users" USING btree ("email");
+--> statement-breakpoint
+CREATE INDEX "idx_search" ON "wiki_pages" USING gin ("search");
+--> statement-breakpoint
+CREATE INDEX "trgm_idx_title" ON "wiki_pages" USING btree ("title");
+--> statement-breakpoint
+-- Enable the pg_trgm extension for fuzzy text matching
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
+--> statement-breakpoint
+-- Create trigram GIN indexes for fast similarity searches
+CREATE INDEX IF NOT EXISTS trgm_idx_title ON wiki_pages USING GIN (title gin_trgm_ops);
+CREATE INDEX IF NOT EXISTS trgm_idx_content ON wiki_pages USING GIN (content gin_trgm_ops);
+--> statement-breakpoint
+-- Add comment to explain what these indexes are for
+COMMENT ON INDEX trgm_idx_title IS 'Trigram index on wiki page titles for fuzzy search';
+COMMENT ON INDEX trgm_idx_content IS 'Trigram index on wiki page content for fuzzy search';
+--> statement-breakpoint
+-- Display information about the created indexes
+SELECT indexname,
+ indexdef
+FROM pg_indexes
+WHERE indexname IN ('trgm_idx_title', 'trgm_idx_content');
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/0000_snapshot.json b/packages/db/drizzle/meta/0000_snapshot.json
new file mode 100644
index 0000000..22d892f
--- /dev/null
+++ b/packages/db/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,1455 @@
+{
+ "id": "beef51f4-c3ed-43a3-a992-7f4409b557d6",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounts": {
+ "name": "accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "accounts_user_id_users_id_fk": {
+ "name": "accounts_user_id_users_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.assets": {
+ "name": "assets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_type": {
+ "name": "file_type",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_by_id": {
+ "name": "uploaded_by_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "assets_uploaded_by_id_users_id_fk": {
+ "name": "assets_uploaded_by_id_users_id_fk",
+ "tableFrom": "assets",
+ "tableTo": "users",
+ "columnsFrom": [
+ "uploaded_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.assets_to_pages": {
+ "name": "assets_to_pages",
+ "schema": "",
+ "columns": {
+ "asset_id": {
+ "name": "asset_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "asset_page_idx": {
+ "name": "asset_page_idx",
+ "columns": [
+ {
+ "expression": "asset_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "page_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "assets_to_pages_asset_id_assets_id_fk": {
+ "name": "assets_to_pages_asset_id_assets_id_fk",
+ "tableFrom": "assets_to_pages",
+ "tableTo": "assets",
+ "columnsFrom": [
+ "asset_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "assets_to_pages_page_id_wiki_pages_id_fk": {
+ "name": "assets_to_pages_page_id_wiki_pages_id_fk",
+ "tableFrom": "assets_to_pages",
+ "tableTo": "wiki_pages",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "assets_to_pages_asset_id_page_id_pk": {
+ "name": "assets_to_pages_asset_id_page_id_pk",
+ "columns": [
+ "asset_id",
+ "page_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.group_action_permissions": {
+ "name": "group_action_permissions",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "group_action_permissions_idx": {
+ "name": "group_action_permissions_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "group_action_permissions_group_id_groups_id_fk": {
+ "name": "group_action_permissions_group_id_groups_id_fk",
+ "tableFrom": "group_action_permissions",
+ "tableTo": "groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "group_action_permissions_group_id_action_pk": {
+ "name": "group_action_permissions_group_id_action_pk",
+ "columns": [
+ "group_id",
+ "action"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.group_module_permissions": {
+ "name": "group_module_permissions",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "module": {
+ "name": "module",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "group_module_permissions_idx": {
+ "name": "group_module_permissions_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "module",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "group_module_permissions_group_id_groups_id_fk": {
+ "name": "group_module_permissions_group_id_groups_id_fk",
+ "tableFrom": "group_module_permissions",
+ "tableTo": "groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "group_module_permissions_group_id_module_pk": {
+ "name": "group_module_permissions_group_id_module_pk",
+ "columns": [
+ "group_id",
+ "module"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.group_permissions": {
+ "name": "group_permissions",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_id": {
+ "name": "permission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "group_permission_idx": {
+ "name": "group_permission_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "permission_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "group_permissions_group_id_groups_id_fk": {
+ "name": "group_permissions_group_id_groups_id_fk",
+ "tableFrom": "group_permissions",
+ "tableTo": "groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "group_permissions_permission_id_permissions_id_fk": {
+ "name": "group_permissions_permission_id_permissions_id_fk",
+ "tableFrom": "group_permissions",
+ "tableTo": "permissions",
+ "columnsFrom": [
+ "permission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "group_permissions_group_id_permission_id_pk": {
+ "name": "group_permissions_group_id_permission_id_pk",
+ "columns": [
+ "group_id",
+ "permission_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.groups": {
+ "name": "groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "is_system": {
+ "name": "is_system",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "is_editable": {
+ "name": "is_editable",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ },
+ "allow_user_assignment": {
+ "name": "allow_user_assignment",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "groups_name_unique": {
+ "name": "groups_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.page_permissions": {
+ "name": "page_permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permission_id": {
+ "name": "permission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'allow'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "page_group_perm_idx": {
+ "name": "page_group_perm_idx",
+ "columns": [
+ {
+ "expression": "page_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "permission_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "page_permissions_page_id_wiki_pages_id_fk": {
+ "name": "page_permissions_page_id_wiki_pages_id_fk",
+ "tableFrom": "page_permissions",
+ "tableTo": "wiki_pages",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "page_permissions_group_id_groups_id_fk": {
+ "name": "page_permissions_group_id_groups_id_fk",
+ "tableFrom": "page_permissions",
+ "tableTo": "groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "page_permissions_permission_id_permissions_id_fk": {
+ "name": "page_permissions_permission_id_permissions_id_fk",
+ "tableFrom": "page_permissions",
+ "tableTo": "permissions",
+ "columnsFrom": [
+ "permission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "module": {
+ "name": "module",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource": {
+ "name": "resource",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "generated": {
+ "as": "\"module\" || ':' || \"resource\" || ':' || \"action\"",
+ "type": "stored"
+ }
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "permissions_name_unique": {
+ "name": "permissions_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sessions_user_id_users_id_fk": {
+ "name": "sessions_user_id_users_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sessions_session_token_unique": {
+ "name": "sessions_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "session_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_groups": {
+ "name": "user_groups",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_group_idx": {
+ "name": "user_group_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_groups_user_id_users_id_fk": {
+ "name": "user_groups_user_id_users_id_fk",
+ "tableFrom": "user_groups",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "user_groups_group_id_groups_id_fk": {
+ "name": "user_groups_group_id_groups_id_fk",
+ "tableFrom": "user_groups",
+ "tableTo": "groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_groups_user_id_group_id_pk": {
+ "name": "user_groups_user_id_group_id_pk",
+ "columns": [
+ "user_id",
+ "group_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_idx": {
+ "name": "email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification_tokens": {
+ "name": "verification_tokens",
+ "schema": "",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verification_tokens_identifier_token_pk": {
+ "name": "verification_tokens_identifier_token_pk",
+ "columns": [
+ "identifier",
+ "token"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wiki_page_revisions": {
+ "name": "wiki_page_revisions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_id": {
+ "name": "created_by_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "wiki_page_revisions_page_id_wiki_pages_id_fk": {
+ "name": "wiki_page_revisions_page_id_wiki_pages_id_fk",
+ "tableFrom": "wiki_page_revisions",
+ "tableTo": "wiki_pages",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "wiki_page_revisions_created_by_id_users_id_fk": {
+ "name": "wiki_page_revisions_created_by_id_users_id_fk",
+ "tableFrom": "wiki_page_revisions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wiki_page_to_tag": {
+ "name": "wiki_page_to_tag",
+ "schema": "",
+ "columns": {
+ "page_id": {
+ "name": "page_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "wiki_page_to_tag_page_id_wiki_pages_id_fk": {
+ "name": "wiki_page_to_tag_page_id_wiki_pages_id_fk",
+ "tableFrom": "wiki_page_to_tag",
+ "tableTo": "wiki_pages",
+ "columnsFrom": [
+ "page_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "wiki_page_to_tag_tag_id_wiki_tags_id_fk": {
+ "name": "wiki_page_to_tag_tag_id_wiki_tags_id_fk",
+ "tableFrom": "wiki_page_to_tag",
+ "tableTo": "wiki_tags",
+ "columnsFrom": [
+ "tag_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "wiki_page_to_tag_page_id_tag_id_pk": {
+ "name": "wiki_page_to_tag_page_id_tag_id_pk",
+ "columns": [
+ "page_id",
+ "tag_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wiki_pages": {
+ "name": "wiki_pages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rendered_html": {
+ "name": "rendered_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "editor_type": {
+ "name": "editor_type",
+ "type": "editor_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_published": {
+ "name": "is_published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "created_by_id": {
+ "name": "created_by_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_by_id": {
+ "name": "updated_by_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "rendered_html_updated_at": {
+ "name": "rendered_html_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_by_id": {
+ "name": "locked_by_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_at": {
+ "name": "locked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lock_expires_at": {
+ "name": "lock_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "search": {
+ "name": "search",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": true,
+ "generated": {
+ "as": "setweight(to_tsvector('english', \"wiki_pages\".\"title\"), 'A')\n ||\n setweight(to_tsvector('english', \"wiki_pages\".\"content\"), 'B')",
+ "type": "stored"
+ }
+ }
+ },
+ "indexes": {
+ "idx_search": {
+ "name": "idx_search",
+ "columns": [
+ {
+ "expression": "search",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "trgm_idx_title": {
+ "name": "trgm_idx_title",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wiki_pages_created_by_id_users_id_fk": {
+ "name": "wiki_pages_created_by_id_users_id_fk",
+ "tableFrom": "wiki_pages",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "wiki_pages_updated_by_id_users_id_fk": {
+ "name": "wiki_pages_updated_by_id_users_id_fk",
+ "tableFrom": "wiki_pages",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "wiki_pages_locked_by_id_users_id_fk": {
+ "name": "wiki_pages_locked_by_id_users_id_fk",
+ "tableFrom": "wiki_pages",
+ "tableTo": "users",
+ "columnsFrom": [
+ "locked_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "wiki_pages_path_unique": {
+ "name": "wiki_pages_path_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "path"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wiki_tags": {
+ "name": "wiki_tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "wiki_tags_name_unique": {
+ "name": "wiki_tags_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.editor_type": {
+ "name": "editor_type",
+ "schema": "public",
+ "values": [
+ "markdown",
+ "html"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json
new file mode 100644
index 0000000..8f50c52
--- /dev/null
+++ b/packages/db/drizzle/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1745364296469,
+ "tag": "0000_light_sumo",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/db/package.json b/packages/db/package.json
index 60b2b97..e1ddd50 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -36,7 +36,9 @@
},
"dependencies": {
"@neondatabase/serverless": "^1.0.0",
+ "@repo/logger": "workspace:*",
"bcryptjs": "^3.0.2",
+ "child_process": "^1.0.2",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.42.0",
"gray-matter": "^4.0.3",
diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts
index 7daa824..70b9fc0 100644
--- a/packages/db/src/client.ts
+++ b/packages/db/src/client.ts
@@ -11,7 +11,8 @@ export type {
PermissionAction,
PermissionResource,
PermissionIdentifier,
-} from "./registry/types.js";
+ PossiblePermissionIdentifier,
+} from "./registry/index.js";
// Re-export specific functions from the registry that are client-safe
export {
diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts
index cc21ff9..7cd06e9 100644
--- a/packages/db/src/index.ts
+++ b/packages/db/src/index.ts
@@ -1,5 +1,3 @@
-import "server-only";
-
import * as dotenv from "dotenv";
import { neon, neonConfig } from "@neondatabase/serverless";
import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";
diff --git a/packages/db/src/registry/index.ts b/packages/db/src/registry/index.ts
index 4230340..de277e5 100644
--- a/packages/db/src/registry/index.ts
+++ b/packages/db/src/registry/index.ts
@@ -1,2 +1,23 @@
-export * from "./permissions.js";
+import { PERMISSIONS } from "./permissions.js";
+
+// Re-export types and functions from other registry files
export * from "./types.js";
+export * from "./permissions.js";
+
+/**
+ * Type for the strict permission identifier string derived from the actual registry.
+ * Ensures only valid, registered permission identifiers are included.
+ */
+export type PermissionIdentifier = keyof typeof PERMISSIONS;
+
+/**
+ * Function to assert that a string is a valid PermissionIdentifier.
+ * Useful for type narrowing.
+ * @param id The string to check.
+ * @returns True if the string is a valid PermissionIdentifier, false otherwise.
+ */
+export function isPermissionIdentifier(id: string): id is PermissionIdentifier {
+ // Get the actual keys from the PERMISSIONS object and check for inclusion
+ const validIds = Object.keys(PERMISSIONS) as PermissionIdentifier[];
+ return validIds.includes(id as PermissionIdentifier);
+}
diff --git a/packages/db/src/registry/permissions.ts b/packages/db/src/registry/permissions.ts
index ac87a4f..2a3464b 100644
--- a/packages/db/src/registry/permissions.ts
+++ b/packages/db/src/registry/permissions.ts
@@ -1,10 +1,10 @@
import {
Permission,
- PermissionIdentifier,
+ PossiblePermissionIdentifier,
PermissionModule,
PermissionAction,
PermissionResource,
-} from "./types.js"; // Added .js extension
+} from "./types.js";
/**
* Define all system permissions
@@ -148,28 +148,29 @@ const PERMISSION_LIST: Permission[] = [
*/
export function createPermissionId(
permission: Permission
-): PermissionIdentifier {
- return `${permission.module}:${permission.resource}:${permission.action}` as PermissionIdentifier;
+): PossiblePermissionIdentifier {
+ return `${permission.module}:${permission.resource}:${permission.action}`;
}
/**
* Build the permissions record from the list
*/
-export const PERMISSIONS = PERMISSION_LIST.reduce<
- Record
->(
- (acc, permission) => {
- const id = createPermissionId(permission);
- acc[id] = permission;
- return acc;
- },
- {} as Record
-);
+export const PERMISSIONS: Record =
+ PERMISSION_LIST.reduce>(
+ (acc, permission) => {
+ const id = createPermissionId(permission);
+ acc[id] = permission;
+ return acc;
+ },
+ {} as Record
+ );
/**
- * Validates if a string is a valid permission identifier
+ * Validates if a string is a valid permission identifier by checking against the generated PERMISSIONS object.
*/
-export function validatePermissionId(id: string): id is PermissionIdentifier {
+export function validatePermissionId(
+ id: string
+): id is PossiblePermissionIdentifier {
return id in PERMISSIONS;
}
@@ -183,8 +184,8 @@ export function getAllPermissions(): Permission[] {
/**
* Gets all permission identifiers
*/
-export function getAllPermissionIds(): PermissionIdentifier[] {
- return Object.keys(PERMISSIONS) as PermissionIdentifier[];
+export function getAllPermissionIds(): PossiblePermissionIdentifier[] {
+ return Object.keys(PERMISSIONS) as PossiblePermissionIdentifier[];
}
/**
diff --git a/packages/db/src/registry/types.ts b/packages/db/src/registry/types.ts
index 8ee1ce8..142b518 100644
--- a/packages/db/src/registry/types.ts
+++ b/packages/db/src/registry/types.ts
@@ -35,6 +35,9 @@ export interface Permission {
/**
* Type for the permission identifier string in format module:resource:action
+ * Represents any possible combination based on defined modules, resources, and actions.
+ * For a stricter type derived from actual registered permissions, use `PermissionIdentifier`
+ * from `packages/db/src/registry/index.ts`.
*/
-export type PermissionIdentifier =
+export type PossiblePermissionIdentifier =
`${PermissionModule}:${PermissionResource}:${PermissionAction}`;
diff --git a/packages/db/src/seeds/custom-seeds.example.ts b/packages/db/src/seeds/custom-seeds.example.ts
index 9091b2b..7bd23d4 100644
--- a/packages/db/src/seeds/custom-seeds.example.ts
+++ b/packages/db/src/seeds/custom-seeds.example.ts
@@ -1,7 +1,7 @@
// This file is intended for developers to add their own custom seed data.
// It is ignored by git by default (see .gitignore).
// You can copy the contents of custom-seeds.example.ts here to get started.
-// import { logger } from "~/lib/utils/logger";
+// import { logger } from "@repo/logger";
/**
* Runs custom seed operations defined by the developer.
diff --git a/packages/db/src/seeds/developer-seeds.ts b/packages/db/src/seeds/developer-seeds.ts
new file mode 100644
index 0000000..1f407f0
--- /dev/null
+++ b/packages/db/src/seeds/developer-seeds.ts
@@ -0,0 +1,24 @@
+import { seedAdminUser, seedUserUser } from "./developer/users.js";
+import { seedExamplePages } from "./developer/example-pages.js";
+
+/**
+ * Runs developer seeds
+ */
+export async function runDeveloperSeeds() {
+ console.log(" -> Running developer seeds...");
+
+ try {
+ // Seed the admin user (important to run first if other seeds depend on it)
+ await seedAdminUser();
+ await seedUserUser();
+
+ // Seed example pages
+ await seedExamplePages();
+
+ console.log(" -> Developer seeds finished.");
+ } catch (error) {
+ console.error(" ❌ Error during developer seed operations:", error);
+ // Decide if you want to throw the error or continue seeding other things
+ // throw error;
+ }
+}
diff --git a/packages/db/src/seeds/developer/example-pages.ts b/packages/db/src/seeds/developer/example-pages.ts
new file mode 100644
index 0000000..224abab
--- /dev/null
+++ b/packages/db/src/seeds/developer/example-pages.ts
@@ -0,0 +1,316 @@
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url"; // Added import
+import matter from "gray-matter"; // Import gray-matter
+import { db } from "../../index.js"; // Added .js extension
+import * as schema from "../../schema/index.js"; // Changed to namespace import, added .js extension
+import { eq } from "drizzle-orm";
+
+const __filename = fileURLToPath(import.meta.url); // Added line
+const __dirname = path.dirname(__filename); // Added line
+const PAGES_ROOT_DIR = path.join(__dirname, "pages"); // Keep this line as it now works
+
+/**
+ * Recursively processes a Markdown file, extracting frontmatter and content,
+ * and inserts it into the database if it doesn't already exist.
+ */
+async function processMarkdownFile(
+ filePath: string,
+ baseDir: string,
+ userId: number
+) {
+ try {
+ const fileContent = fs.readFileSync(filePath, "utf8");
+
+ // --- Determine Current Wiki Directory Path ---
+ const relativeFileDir = path.dirname(path.relative(baseDir, filePath));
+ let currentWikiDir = "/"; // Default to root
+ if (relativeFileDir !== ".") {
+ currentWikiDir = "/" + relativeFileDir.replace(/\\/g, "/");
+ }
+ // Ensure base URL for resolution has a trailing slash for correct relative resolution
+ const baseWikiUrl =
+ "http://dummy.com" +
+ (currentWikiDir.endsWith("/") ? currentWikiDir : currentWikiDir + "/");
+ // --------------------------------------------
+
+ // --- Preprocess content: Fix links ---
+ const linkRegex = /!?\[([^\]]*)\]\(([^)#]+)(#?[^)]*)\)/g; // Capture hash separately
+ const modifiedFileContent = fileContent.replace(
+ linkRegex,
+ (match, text, url, hash) => {
+ let newUrl = url;
+ const originalHash = hash || ""; // Keep the hash part if it exists
+ const originalUrlForWarning = url; // Keep original for warning messages
+
+ // --- Skip External Links & Fragments ---
+ if (/^[a-z]+:/i.test(url) || url.startsWith("#")) {
+ return match; // Keep as is
+ }
+
+ // --- Handle /en/ Prefix ---
+ let isEnStripped = false;
+ if (url.startsWith("/en/")) {
+ url = url.substring(3) || "/"; // Update url variable
+ isEnStripped = true;
+ }
+
+ // --- Handle Different Link Types ---
+ if (url.startsWith("/")) {
+ // A. Absolute path: Keep as is, but normalize
+ newUrl = url;
+ } else if (url.includes("/")) {
+ // B. Relative path with slashes: Resolve relative to current dir
+ try {
+ const resolved = new URL(url, baseWikiUrl); // Resolve relative to current dir's base URL
+ newUrl = resolved.pathname; // Use the absolute path from the resolved URL
+ } catch (e) {
+ console.warn(
+ ` ⚠️ Could not resolve relative URL "${originalUrlForWarning}" in file ${filePath} relative to base ${baseWikiUrl}. Error: ${
+ e instanceof Error ? e.message : e
+ }. Keeping ${
+ isEnStripped ? "stripped" : "original"
+ } URL: "${url}".`
+ );
+ newUrl = url; // Keep the original (or /en/ stripped) relative path if resolution fails
+ }
+ } else {
+ // C. Simple relative path (no slashes): Leave as is
+ return match; // Return the original match without modification
+ }
+
+ // --- Common Normalization (for cases A and B) ---
+ if (newUrl.toLowerCase().endsWith(".md")) {
+ newUrl = newUrl.slice(0, -".md".length);
+ }
+ if (newUrl.toLowerCase().endsWith("/index")) {
+ newUrl = newUrl.slice(0, -"/index".length) || "/";
+ }
+ if (newUrl !== "/" && newUrl.endsWith("/")) {
+ newUrl = newUrl.slice(0, -1);
+ }
+ // Ensure starts with / (should be true for A and B after resolution/assignment)
+ if (!newUrl.startsWith("/")) {
+ newUrl = "/" + newUrl;
+ }
+
+ return `[${text}](${newUrl}${originalHash})`; // Reconstruct link
+ }
+ );
+ // -----------------------------------
+
+ // Parse the MODIFIED content with gray-matter
+ const { data, content } = matter(modifiedFileContent);
+ const tags: string[] = data.tags || []; // Extract tags from frontmatter
+
+ // --- Determine Database Path --- (
+ let dbPath = "/"; // Default path
+ if (data.path) {
+ // Use frontmatter path if provided
+ dbPath = data.path.startsWith("/") ? data.path : `/${data.path}`;
+ } else {
+ // Derive path from file structure
+ const relativePath = path.relative(baseDir, filePath);
+ // Normalize separators and remove extension
+ dbPath = "/" + relativePath.replace(/\\/g, "/").replace(/\.md$/, "");
+ // Handle index files (e.g., /some/path/index -> /some/path)
+ if (dbPath.endsWith("/index")) {
+ dbPath = dbPath.substring(0, dbPath.length - "/index".length) || "/";
+ }
+ // Handle home.md at root specifically -> /
+ if (dbPath === "/home") {
+ dbPath = "/";
+ }
+ }
+ // --- Determine Title --- (
+ const title = data.title || path.basename(filePath, ".md"); // Use frontmatter title or filename
+
+ // --- Determine Dates --- (
+ const createdAt =
+ data.createdAt && !isNaN(new Date(data.createdAt).getTime())
+ ? new Date(data.createdAt)
+ : new Date();
+ const updatedAt =
+ data.updatedAt && !isNaN(new Date(data.updatedAt).getTime())
+ ? new Date(data.updatedAt)
+ : createdAt; // Default updated to created if not specified
+
+ // --- Check if page exists --- (
+ const existingPage = await db.query.wikiPages.findFirst({
+ where: eq(schema.wikiPages.path, dbPath), // Use schema.wikiPages
+ });
+
+ if (existingPage) {
+ console.log(
+ ` ℹ️ Page with path "${dbPath}" already exists (from file: ${path.basename(
+ filePath
+ )}). Skipping.`
+ );
+ } else {
+ // --- Create the page --- (
+ const insertedPage = await db // Use schema.wikiPages
+ .insert(schema.wikiPages)
+ .values({
+ path: dbPath.replace(/^\//, ""), // Replace leading / before storing
+ title: title,
+ content: content, // Use the MODIFIED content here
+ createdById: userId,
+ updatedById: userId,
+ isPublished: true, // Assume published for seeded pages
+ createdAt: createdAt,
+ updatedAt: updatedAt,
+ editorType: "markdown", // Assume markdown for seeded pages
+ })
+ .returning(); // Changed returning to have no arguments
+
+ if (!insertedPage || insertedPage.length === 0) {
+ console.error(` ❌ Failed to get ID for inserted page: ${dbPath}`);
+ return; // Skip tag processing if page insertion failed
+ }
+ // Add explicit check for the first element before accessing id
+ if (!insertedPage[0]) {
+ console.error(
+ ` ❌ Inserted page array exists but element 0 is missing for: ${dbPath}`
+ );
+ return;
+ }
+ const pageId = insertedPage[0].id;
+
+ console.log(
+ ` ✅ Created page: "${title}" at path "${dbPath}" (from file: ${path.basename(
+ filePath
+ )}) (ID: ${pageId})`
+ );
+
+ // --- Process and link tags ---
+ if (tags.length > 0) {
+ console.log(
+ ` 🏷️ Processing tags for page "${dbPath}": ${tags.join(", ")}`
+ );
+ for (const tagName of tags) {
+ if (!tagName || typeof tagName !== "string") {
+ console.warn(
+ ` ⚠️ Invalid tag found for page "${dbPath}":`,
+ tagName
+ );
+ continue;
+ }
+ const trimmedTagName = tagName.trim();
+ if (!trimmedTagName) {
+ console.warn(
+ ` ⚠️ Empty tag found after trimming for page "${dbPath}". Skipping.`
+ );
+ continue;
+ }
+
+ try {
+ // Find or create the tag
+ const tagRecord = await db.query.wikiTags.findFirst({
+ where: eq(schema.wikiTags.name, trimmedTagName),
+ });
+
+ let tagId: number;
+ if (tagRecord) {
+ tagId = tagRecord.id;
+ console.log(
+ ` -> Found existing tag "${trimmedTagName}" (ID: ${tagId})`
+ );
+ } else {
+ const newTag = await db
+ .insert(schema.wikiTags)
+ .values({
+ name: trimmedTagName,
+ })
+ .returning(); // Changed returning to have no arguments
+
+ if (!newTag || newTag.length === 0) {
+ console.error(
+ ` ❌ Failed to insert tag: ${trimmedTagName}`
+ );
+ continue; // Skip linking this tag
+ }
+ // Add explicit check for the first element before accessing id
+ if (!newTag[0]) {
+ console.error(
+ ` ❌ Inserted tag array exists but element 0 is missing for: ${trimmedTagName}`
+ );
+ continue;
+ }
+ tagId = newTag[0].id;
+ console.log(
+ ` -> Created new tag "${trimmedTagName}" (ID: ${tagId})`
+ );
+ }
+
+ // Link tag to page
+ await db
+ .insert(schema.wikiPageToTag)
+ .values({
+ pageId: pageId,
+ tagId: tagId,
+ })
+ .onConflictDoNothing(); // Avoid errors if link already exists
+ console.log(
+ ` 🔗 Linked tag "${trimmedTagName}" to page "${dbPath}"`
+ );
+ } catch (tagError) {
+ console.error(
+ ` ❌ Error processing tag "${trimmedTagName}" for page "${dbPath}":`,
+ tagError
+ );
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error(` ❌ Error processing file "${filePath}":`, error);
+ }
+}
+
+/**
+ * Recursively traverses a directory and processes all .md files found.
+ */
+async function traverseDirectory(dir: string, baseDir: string, userId: number) {
+ try {
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ await traverseDirectory(fullPath, baseDir, userId); // Recurse into subdirectory
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
+ await processMarkdownFile(fullPath, baseDir, userId); // Process markdown file
+ }
+ }
+ } catch (error) {
+ // Handle cases where the directory might not exist
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
+ console.warn(` ⚠️ Directory not found: ${dir}. Skipping.`);
+ } else {
+ console.error(` ❌ Error reading directory "${dir}":`, error);
+ }
+ }
+}
+
+/**
+ * Seeds wiki pages by scanning the ./pages directory.
+ */
+export async function seedExamplePages() {
+ console.log(" ↳ Seeding wiki pages from ./custom/pages directory...");
+
+ // Find the admin user to assign authorship
+ const adminUser = await db.query.users.findFirst({
+ where: eq(schema.users.email, "admin@example.com"), // Use schema.users
+ });
+
+ if (!adminUser) {
+ console.warn(
+ " ⚠️ Admin user (admin@example.com) not found. Cannot seed pages. Please ensure admin user is seeded first."
+ );
+ return;
+ }
+
+ // Start traversal
+ await traverseDirectory(PAGES_ROOT_DIR, PAGES_ROOT_DIR, adminUser.id);
+
+ console.log(" ↳ Finished seeding wiki pages from directory.");
+}
diff --git a/packages/db/src/seeds/developer/pages/about.md b/packages/db/src/seeds/developer/pages/about.md
new file mode 100644
index 0000000..57b3a31
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/about.md
@@ -0,0 +1,48 @@
+---
+path: about
+title: About
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [about, info, project, demo]
+---
+
+# About NextWiki (Public Demo)
+
+> **Note:** This is a public demonstration instance of NextWiki.
+
+NextWiki is an open-source wiki system designed for modern web environments.
+
+## Mission
+
+Our goal is to provide a flexible, performant, and user-friendly platform for knowledge management, inspired by systems like Wiki.js but built with a contemporary tech stack.
+
+## Key Technologies
+
+- **Framework**: Next.js 15
+- **Language**: TypeScript
+- **Database**: PostgreSQL with Drizzle ORM
+- **API**: tRPC
+- **Authentication**: NextAuth.js
+- **UI**: React 19, Tailwind CSS, Shadcn UI
+
+## Contributing
+
+We welcome contributions! Check out the [CONTRIBUTING.md](https://raw.githubusercontent.com/barisgit/nextwiki/main/CONTRIBUTING.md) guide on our GitHub repository.
+
+- [Visit the Repository](https://github.com/barisgit/nextwiki)
+{.links-list}
+
+## Project Goals
+
+- Provide a modern, fast, and user-friendly wiki experience.
+- Offer features comparable to established wikis like Wiki.js.
+- Maintain an open-source codebase for community contribution.
+- Explore the capabilities of the T3 Stack (Next.js, TypeScript, Tailwind, tRPC) + Drizzle.
+
+## Learn More
+
+- Visit the [GitHub repository](https://github.com/barisgit/nextwiki) for source code and contributions.
+- Check the [README](/) on the main project page for setup instructions and technical details (if viewing locally).
+
+This instance showcases the current state of development.
diff --git a/packages/db/src/seeds/developer/pages/advanced-topics.md b/packages/db/src/seeds/developer/pages/advanced-topics.md
new file mode 100644
index 0000000..5eb1e9d
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/advanced-topics.md
@@ -0,0 +1,12 @@
+---
+path: advanced-topics
+title: Advanced Topics
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [advanced, customization, api, demo]
+---
+
+# Advanced Topics (Demo)
+
+Explore guides on customization and API integration.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/advanced-topics/api-integration.md b/packages/db/src/seeds/developer/pages/advanced-topics/api-integration.md
new file mode 100644
index 0000000..303a9e6
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/advanced-topics/api-integration.md
@@ -0,0 +1,43 @@
+---
+path: advanced-topics/api-integration
+title: API Integration
+author: NextWiki Team
+createdAt: 2024-01-01T00:00:00.000Z
+updated: 2024-01-01T00:00:00.000Z
+tags: [advanced, api, integration, trpc, development]
+---
+
+# API Integration
+
+This page provides a brief overview of how external applications or scripts could potentially interact with NextWiki's backend.
+
+## tRPC API
+
+NextWiki uses tRPC for its internal API communication between the Next.js frontend and backend. While primarily designed for internal use, understanding the tRPC endpoints can be useful for advanced integrations or debugging.
+
+```typescript
+// Example hypothetical client-side tRPC usage
+import { useTrpc } from '@nextwiki/sdk';
+import { useQuery } from '@tanstack/react-query';
+
+function SomeComponent() {
+ const trpc = useTrpc();
+ const { data: page, isLoading } = useQuery(trpc.page.getByPath.queryOptions({ path: 'getting-started' }));
+
+ if (isLoading) return Loading...
;
+ if (!page) return Page not found
;
+
+ return (
+
+
{page.title}
+ {/* Render page content */}
+
+ );
+}
+```
+
+## Future Considerations
+
+A dedicated public REST API might be considered in the future for easier third-party integrations.
+
+**Note:** Direct API interaction is an advanced topic and may change between NextWiki versions. Proceed with caution.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/advanced-topics/customization-guide.md b/packages/db/src/seeds/developer/pages/advanced-topics/customization-guide.md
new file mode 100644
index 0000000..99a4473
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/advanced-topics/customization-guide.md
@@ -0,0 +1,32 @@
+---
+path: advanced-topics/customization-guide
+title: Customization Guide
+author: NextWiki Team
+createdAt: 2024-01-01T00:00:00.000Z
+updated: 2024-01-01T00:00:00.000Z
+tags: [advanced, customization, guide, theme, development]
+---
+
+# Customization Guide
+
+This page outlines potential ways to customize your NextWiki installation.
+
+## Theming
+
+NextWiki uses Tailwind CSS for styling and Shadcn UI for its component library. Customization can be achieved by:
+
+1. **Modifying Tailwind Configuration**: Adjusting `tailwind.config.js` to change colors, fonts, spacing, etc.
+2. **Overriding CSS Variables**: Shadcn UI components use CSS variables for theming. These can be overridden in your global CSS file.
+3. **Custom Components**: Building your own React components to replace or supplement existing ones.
+
+*(Detailed instructions and examples would be provided here in a full guide)*
+
+## Extending Functionality
+
+As an open-source project, you can directly modify the codebase:
+
+- **Adding tRPC Procedures**: Extend the API by adding new procedures in the backend routers.
+- **Creating New React Components**: Develop new UI elements or features.
+- **Integrating External Libraries**: Incorporate other JavaScript libraries or services.
+
+**Note:** Customizing the core code requires familiarity with the tech stack (Next.js, React, tRPC, Drizzle) and may make updating NextWiki more complex.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/deeply-nested.md b/packages/db/src/seeds/developer/pages/deeply-nested.md
new file mode 100644
index 0000000..43a1384
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/deeply-nested.md
@@ -0,0 +1,12 @@
+---
+path: deeply-nested
+title: Deeply Nested
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [example, nesting, structure, demo]
+---
+
+# Deeply Nested (Demo)
+
+This section demonstrates folder structure handling.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/deeply-nested/level-1.md b/packages/db/src/seeds/developer/pages/deeply-nested/level-1.md
new file mode 100644
index 0000000..c227adc
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/deeply-nested/level-1.md
@@ -0,0 +1,12 @@
+---
+path: deeply-nested/level-1
+title: Level 1
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [deeply, nested, level, test]
+---
+
+# Level 1
+
+This page corresponds to the level-1 directory.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2.md b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2.md
new file mode 100644
index 0000000..12feed3
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2.md
@@ -0,0 +1,12 @@
+---
+path: deeply-nested/level-1/level-2
+title: Level 2
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [deeply, nested, level, test]
+---
+
+# Level 2
+
+This page corresponds to the level-2 directory.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3.md b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3.md
new file mode 100644
index 0000000..58f4450
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3.md
@@ -0,0 +1,12 @@
+---
+path: deeply-nested/level-1/level-2/level-3
+title: Level 3
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [deeply, nested, level, test]
+---
+
+# Level 3
+
+This page corresponds to the level-3 directory.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3/level-4.md b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3/level-4.md
new file mode 100644
index 0000000..e122d08
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3/level-4.md
@@ -0,0 +1,12 @@
+---
+path: deeply-nested/level-1/level-2/level-3/level-4
+title: Level 4
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [deeply, nested, level, test]
+---
+
+# Level 4
+
+This page corresponds to the level-4 directory.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3/level-4/final-page.md b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3/level-4/final-page.md
new file mode 100644
index 0000000..9202789
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3/level-4/final-page.md
@@ -0,0 +1,23 @@
+---
+path: deeply-nested/level-1/level-2/level-3/level-4/final-page
+title: Deeply Nested Page
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [demo, nesting, deep, structure]
+---
+
+# Deeply Nested Page
+
+This page exists within a deeply nested folder structure:
+
+`deeply-nested > level-1 > level-2 > level-3`
+
+This demonstrates NextWiki's ability to handle complex page hierarchies.
+
+You should be able to see the parent folders in the navigation breadcrumbs or sidebar tree view (depending on the UI implementation).
+
+Linking to other pages still works:
+
+- [Go to Getting Started](/getting-started)
+- [Go to Markdown Examples](/features/markdown-examples)
diff --git a/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3b.md b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3b.md
new file mode 100644
index 0000000..782c76b
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/deeply-nested/level-1/level-2/level-3b.md
@@ -0,0 +1,12 @@
+---
+path: deeply-nested/level-1/level-2/level-3b
+title: Level 3b
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [deeply, nested, level, test]
+---
+
+# Level 3b
+
+This page corresponds to the level-3b directory. This is a sibling of level-3.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/features.md b/packages/db/src/seeds/developer/pages/features.md
new file mode 100644
index 0000000..4781195
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/features.md
@@ -0,0 +1,12 @@
+---
+path: features
+title: Features
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [features, examples, markdown, search, demo]
+---
+
+# Features (Demo)
+
+Explore examples of NextWiki features in this section.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/features/code-syntax-highlighting.md b/packages/db/src/seeds/developer/pages/features/code-syntax-highlighting.md
new file mode 100644
index 0000000..f5fe157
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/features/code-syntax-highlighting.md
@@ -0,0 +1,122 @@
+---
+path: features/code-syntax-highlighting
+title: Code Syntax Highlighting
+author: NextWiki Team
+createdAt: 2024-01-01T00:00:00.000Z
+updated: 2024-01-01T00:00:00.000Z
+tags: [feature, code, syntax, highlighting, examples]
+---
+
+# Code Syntax Highlighting
+
+NextWiki supports syntax highlighting for various programming languages using code blocks.
+
+## JavaScript
+
+```javascript
+import { useState, useEffect } from 'react';
+
+function Timer() {
+ const [seconds, setSeconds] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setSeconds(seconds => seconds + 1);
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+ Seconds: {seconds}
+
+ );
+}
+```
+
+## Python
+
+```python
+import requests
+
+def get_data(url):
+ try:
+ response = requests.get(url)
+ response.raise_for_status() # Raise an exception for bad status codes
+ return response.json()
+ except requests.exceptions.RequestException as e:
+ print(f"Error fetching data: {e}")
+ return None
+
+# Example usage
+api_url = "https://jsonplaceholder.typicode.com/todos/1"
+data = get_data(api_url)
+if data:
+ print(data)
+```
+
+## HTML
+
+```html
+
+
+
+ Page Title
+
+
+
+
+This is a Heading
+This is a paragraph.
+
+Click Me
+
+
+
+
+```
+
+## CSS
+
+```css
+body {
+ font-family: sans-serif;
+ margin: 20px;
+ background-color: #f0f0f0;
+}
+
+h1 {
+ color: #333;
+ text-align: center;
+}
+
+#myButton {
+ padding: 10px 20px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+#myButton:hover {
+ background-color: #0056b3;
+}
+```
+
+## SQL (PostgreSQL)
+
+```sql
+-- Select users created in the last week
+SELECT
+ user_id,
+ username,
+ email,
+ created_at
+FROM
+ users
+WHERE
+ created_at >= current_date - interval '7 days'
+ORDER BY
+ created_at DESC;
+```
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/features/markdown-examples.md b/packages/db/src/seeds/developer/pages/features/markdown-examples.md
new file mode 100644
index 0000000..ba10e1c
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/features/markdown-examples.md
@@ -0,0 +1,110 @@
+---
+path: features/markdown-examples
+title: Markdown Examples
+author: NextWiki Team
+createdAt: 2024-01-01T00:00:00.000Z
+updated: 2024-01-01T00:00:00.000Z
+tags: [feature, markdown, formatting, examples]
+---
+
+# Markdown Examples
+
+This page demonstrates various Markdown formatting options available in NextWiki.
+
+## Headings
+
+# Heading 1
+## Heading 2
+### Heading 3
+#### Heading 4
+##### Heading 5
+###### Heading 6
+
+## Text Formatting
+
+*This text is italic.*
+_This is also italic._
+
+**This text is bold.**
+__This is also bold.__
+
+***This text is bold and italic.***
+___This is also bold and italic.___
+
+~~This text is strikethrough.~~
+
+## Lists
+
+### Unordered List
+
+- Item 1
+- Item 2
+ - Sub-item 2.1
+ - Sub-item 2.2
+- Item 3
+
+### Ordered List
+
+1. First item
+2. Second item
+3. Third item
+ 1. Sub-item 3.1
+ 2. Sub-item 3.2
+
+## Links
+
+[Visit the NextWiki GitHub Repository](https://github.com/barisgit/nextwiki)
+
+[Go to Getting Started](./../getting-started.md) (Example of a relative link)
+
+## Blockquotes
+
+> This is a blockquote. It's useful for quoting text from another source.
+> > Blockquotes can be nested.
+
+### Styled Blockquotes (Admonitions)
+
+NextWiki supports styled blockquotes, similar to admonitions in Wiki.js, by adding a class after the quote:
+
+> :information_source: This is an informational message.
+> {.is-info}
+
+> :warning: This is a warning message.
+> {.is-warning}
+
+> :bulb: This is a tip or suggestion.
+> {.is-tip}
+
+(Note: The available classes like `.is-info`, `.is-warning`, `.is-tip` depend on the configured CSS.)
+
+## Code
+
+Inline `code` example.
+
+```javascript
+// Code block example
+function greet(name) {
+ console.log(`Hello, ${name}!`);
+}
+greet('World');
+```
+
+## Horizontal Rule
+
+Use three or more hyphens, asterisks, or underscores:
+
+---
+
+***
+
+___
+
+### Styled Link Lists
+
+You can apply styles to lists of links using a class identifier:
+
+- [:icon: Example Link 1 \_Description 1_](./path-to-page-1)
+- [:icon: Example Link 2 \_Description 2_](./path-to-page-2)
+{.links-list}
+
+(Note: The appearance depends on the CSS rules for `.links-list`.)
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/features/planned-features.md b/packages/db/src/seeds/developer/pages/features/planned-features.md
new file mode 100644
index 0000000..0270788
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/features/planned-features.md
@@ -0,0 +1,25 @@
+---
+path: features/planned-features
+title: Planned Features
+author: NextWiki Team
+createdAt: 2024-01-01T00:00:00.000Z
+updated: 2024-01-01T00:00:00.000Z
+tags: [feature, planned, roadmap, future]
+---
+
+# Planned Features for NextWiki
+
+NextWiki is an actively developed project. Here are some of the features planned for future releases:
+
+- [ ] **Better Permissions**: More granular permissions for different pages.
+- [ ] **Centralized settings**: Manage your wiki from a central settings page, with configuration stored in the database.
+- [ ] **S3 Asset Storage**: Option to store uploaded assets (images, files) in cloud storage like AWS S3.
+- [ ] **Better Tags & Categories**: Enhanced content organization through tagging and categorization.
+- [ ] **Version History**: Robust tracking of page changes with the ability to easily revert to previous versions.
+- [ ] **Real-time Collaboration**: Allow multiple users to edit the same page simultaneously (similar to Google Docs).
+- [ ] **Typo tolerance**: Improved fuzzy search with better typo tolerance.
+- [ ] **Image/Video Optimization**: Automatically optimize images and videos for web performance.
+- [ ] **Install as PWA**: Add a manifest and register the app as a PWA.
+- [ ] **tRPC API SDK**: Generate a tRPC API SDK package to use typesafe API calls anywhere.
+
+Keep an eye on the project's [GitHub repository](https://github.com/barisgit/nextwiki) for updates!
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/features/search-demo.md b/packages/db/src/seeds/developer/pages/features/search-demo.md
new file mode 100644
index 0000000..e39402b
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/features/search-demo.md
@@ -0,0 +1,18 @@
+---
+path: features/search-demo
+title: Search Demo
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [feature, search, demo, exact match, test]
+---
+
+# Search Demo
+
+This page corresponds to the search-demo directory.
+
+# Demos
+
+- [Exact Match *Search `Drizzle ORM`*](exact-match-page)
+- [Fuzzy Match *Search `fuzy matching`*](fuzzy-match-page)
+{.links-list}
diff --git a/packages/db/src/seeds/developer/pages/features/search-demo/exact-match-page.md b/packages/db/src/seeds/developer/pages/features/search-demo/exact-match-page.md
new file mode 100644
index 0000000..6eeda45
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/features/search-demo/exact-match-page.md
@@ -0,0 +1,32 @@
+---
+path: features/search-demo/exact-match-page
+title: Exact Match Search Demo
+author: NextWiki Team
+createdAt: 2024-01-01T00:00:00.000Z
+updated: 2024-01-01T00:00:00.000Z
+tags: [feature, search, demo, exact match, test]
+---
+
+# Exact Match Search Demo
+
+This page contains specific keywords designed to test the exact match functionality of the NextWiki search feature.
+
+Try searching for the following terms:
+
+- `Next.js 15`
+- `Drizzle ORM`
+- `tRPC`
+- `PostgreSQL`
+- `syntax highlighting`
+- `authentication`
+- `trigram similarity`
+
+## Technical Details
+
+NextWiki leverages `PostgreSQL`'s full-text search capabilities, including `tsvector` and `tsquery`. This allows for efficient indexing and searching of page content.
+
+For exact matches, the system prioritizes results where the search query precisely matches terms found in the page titles or content. This often involves direct string comparison or matching against the generated `tsvector`.
+
+We also use `Drizzle ORM` to interact with the database and `tRPC` for type-safe API communication between the frontend and backend.
+
+Authentication is handled via `NextAuth.js`, providing a secure way to manage user sessions.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/features/search-demo/fuzzy-match-page.md b/packages/db/src/seeds/developer/pages/features/search-demo/fuzzy-match-page.md
new file mode 100644
index 0000000..581f660
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/features/search-demo/fuzzy-match-page.md
@@ -0,0 +1,31 @@
+---
+path: features/search-demo/fuzzy-match-page
+title: Fuzzy Match Search Demo
+author: NextWiki Team
+createdAt: 2024-01-01T00:00:00.000Z
+updated: 2024-01-01T00:00:00.000Z
+tags: [feature, search, demo, fuzzy match, typo, test]
+---
+
+# Fuzzy Match Search Demo
+
+This page is intended to test the fuzzy matching and typo tolerance capabilities of the NextWiki search (Note: fuzzy matching might still be in progress).
+
+Try searching for variations or misspellings of these words:
+
+- `fuzzymatching` (Try: `fuzy matching`, `fuzymatching`)
+- `PostgreSQL` (Try: `Postgress`, `PosgresQL`)
+- `NextAuth` (Try: `NexAuth`, `NextAuthh`)
+- `highlighting` (Try: `hightlighting`, `highliting`)
+- `collaboration` (Try: `colaboration`, `collaberation`)
+- `javascript` (Try: `javscript`, `javascrpt`)
+
+## How it Works (or Will Work)
+
+Fuzzy matching often uses techniques like `trigram similarity` (pg_trgm extension in PostgreSQL) or Levenshtein distance to find terms that are close to the search query, even if not identical.
+
+This helps users find content even if they make a typo (`wierd` instead of `weird`) or aren't sure of the exact spelling.
+
+Effective fuzzymatching requires careful indexing and query tuning. The system might calculate a similarity score between the search term and terms in the documents and return results above a certain threshold.
+
+Testing different variations, like `javscript` for `javascript` or `colaboration` for `collaboration`, helps ensure the search is robust.
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/getting-started.md b/packages/db/src/seeds/developer/pages/getting-started.md
new file mode 100644
index 0000000..b3671e6
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/getting-started.md
@@ -0,0 +1,90 @@
+---
+path: getting-started
+title: Getting Started
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [guide, basics, navigation, editing, demo]
+---
+
+# Getting Started with the NextWiki Demo
+
+> **Note:** This is a public demonstration instance of NextWiki. Content may be reset periodically.
+{.is-warning}
+
+Welcome to NextWiki! This guide will help you get started with navigating and using the platform.
+
+## Navigation
+
+- Use the sidebar on the left to browse the available pages and folders.
+- Click on a page title to view its content.
+- Use the search bar at the top to find specific pages or content.
+
+## Creating Content
+
+To create a new page:
+1. Navigate to the desired parent folder (or the root).
+2. Click the "Create Page" button (location might vary depending on UI).
+3. Enter a title and start writing your content using Markdown.
+
+## Basic Editing
+
+NextWiki uses Markdown for formatting content. Here are some basics:
+
+- `# Header 1`, `## Header 2`, etc. for headings.
+- `*italic*` or `_italic_` for italic text.
+- `**bold**` or `__bold__` for bold text.
+- `[Link Text](URL)` for creating hyperlinks.
+- \`code\` for inline code.
+
+```python
+def greet(name):
+ print(f"Hello, {name}!")
+
+greet('World');
+```
+
+### Lists
+
+**Unordered List:**
+
+* Item 1
+* Item 2
+ * Nested Item 2a
+ * Nested Item 2b
+
+**Ordered List:**
+
+1. First item
+2. Second item
+3. Third item
+
+### Blockquotes
+
+> This is a blockquote. It's useful for quoting text from another source.
+
+### Horizontal Rule
+
+Use three or more hyphens, asterisks, or underscores:
+
+---
+
+### Tables
+
+| Header 1 | Header 2 | Header 3 |
+| :------- | :------: | -------: |
+| Align L | Center | Align R |
+| Cell 1 | Cell 2 | Cell 3 |
+| Cell 4 | Cell 5 | Cell 6 |
+
+> :bulb: **Tip:** Markdown is versatile! You can create lists, links, code blocks, and more to structure your content effectively.
+> {.is-info}
+
+## Explore More Features
+
+- [:page_facing_up: Markdown Examples](/features/markdown-examples)
+- [:gear: Customization Guide](/advanced-topics/customization-guide)
+- [:link: API Integration](/advanced-topics/api-integration)
+{.links-list}
+
+Explore the "Features" section for more advanced examples!
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/pages/home.md b/packages/db/src/seeds/developer/pages/home.md
new file mode 100644
index 0000000..3de505c
--- /dev/null
+++ b/packages/db/src/seeds/developer/pages/home.md
@@ -0,0 +1,33 @@
+---
+path: index
+title: Home
+author: NextWiki Team
+createdAt: 2025-04-23T00:00:00.000Z
+updated: 2025-04-23T00:00:00.000Z
+tags: [home, welcome, root, demo]
+---
+
+# Welcome to the NextWiki Public Demo!
+
+> **Note:** This is a public demonstration instance of NextWiki. Feel free to explore, but please be mindful that content may be reset periodically.
+
+This is the home page, seeded with sample content to showcase NextWiki's features.
+
+## Explore
+
+- Use the **sidebar** on the left to navigate the wiki structure.
+- Browse the **Features** section for examples of Markdown, code highlighting, and search.
+- Check out the **Advanced Topics** for guides on customization and API integration.
+- Navigate the **Deeply Nested** example to see how folder structures are handled.
+- Use the **search bar** at the top to find specific content quickly.
+
+Feel free to edit this page or create new ones!
+
+- [Getting Started *Getting started with NextWiki*](/getting-started)
+- [Markdown Examples *Examples of Markdown formatting*](/features/markdown-examples)
+- [Code Syntax Highlighting *Code highlighting examples*](/features/code-syntax-highlighting)
+- [Search Demo *Search functionality demonstration*](/features/search-demo)
+- [Planned Features *Upcoming features*](/features/planned-features)
+- [Customization Guide *Customization guide*](/advanced-topics/customization-guide)
+- [API Integration *API integration guide*](/advanced-topics/api-integration)
+{.links-list}
\ No newline at end of file
diff --git a/packages/db/src/seeds/developer/users.ts b/packages/db/src/seeds/developer/users.ts
new file mode 100644
index 0000000..6a80d60
--- /dev/null
+++ b/packages/db/src/seeds/developer/users.ts
@@ -0,0 +1,89 @@
+import bcrypt from "bcryptjs";
+import { db } from "../../index.js";
+import * as schema from "../../schema/index.js";
+import { eq } from "drizzle-orm";
+
+const saltRounds = 10;
+
+const ADMIN_EMAIL = "admin@example.com";
+const ADMIN_NAME = "Admin";
+const ADMIN_PASSWORD = "12345678"; // Use environment variables in production!
+
+const USER_EMAIL = "user@example.com";
+const USER_NAME = "User";
+const USER_PASSWORD = "12345678";
+
+/**
+ * Seeds the default administrator user if they don't exist.
+ */
+export async function seedAdminUser() {
+ console.log(" ↳ Seeding admin user...");
+
+ try {
+ const existingAdmin = await db.query.users.findFirst({
+ where: eq(schema.users.email, ADMIN_EMAIL),
+ });
+
+ if (existingAdmin) {
+ console.log(
+ ` ℹ️ Admin user (${ADMIN_EMAIL}) already exists. Skipping.`
+ );
+ return;
+ }
+
+ const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, saltRounds);
+
+ const [adminUser] = await db
+ .insert(schema.users)
+ .values({
+ name: ADMIN_NAME,
+ email: ADMIN_EMAIL,
+ password: hashedPassword,
+ // emailVerified: new Date(), // Optionally mark email as verified
+ })
+ .returning();
+
+ // Add admin to Administrators group
+ if (adminUser) {
+ await db.insert(schema.userGroups).values({
+ groupId: 1, // Administrators group ID
+ userId: adminUser.id,
+ });
+ }
+
+ console.log(` ✅ Created admin user: ${ADMIN_EMAIL}`);
+ } catch (error) {
+ console.error(" ❌ Error seeding admin user:", error);
+ }
+}
+
+export async function seedUserUser() {
+ console.log(" ↳ Seeding user user...");
+
+ try {
+ const existingUser = await db.query.users.findFirst({
+ where: eq(schema.users.email, USER_EMAIL),
+ });
+
+ if (existingUser) {
+ console.log(
+ ` ℹ️ User user (${USER_EMAIL}) already exists. Skipping.`
+ );
+ return;
+ }
+
+ const hashedPassword = await bcrypt.hash(USER_PASSWORD, saltRounds);
+
+ // We don't need to return the user here, as we don't need to assign it to a group
+ await db.insert(schema.users).values({
+ name: USER_NAME,
+ email: USER_EMAIL,
+ password: hashedPassword,
+ // emailVerified: new Date(), // Optionally mark email as verified
+ });
+
+ console.log(` ✅ Created user user: ${USER_EMAIL}`);
+ } catch (error) {
+ console.error(" ❌ Error seeding user user:", error);
+ }
+}
diff --git a/packages/db/src/seeds/run.ts b/packages/db/src/seeds/run.ts
index 7726602..95e7b9f 100644
--- a/packages/db/src/seeds/run.ts
+++ b/packages/db/src/seeds/run.ts
@@ -1,7 +1,5 @@
import { createDefaultGroups, seedPermissions } from "./permissions.js";
-import { runCustomSeeds } from "./custom-seeds.js";
-// import { db } from "../index.js"; // No longer needed for closing
-// import pg from "pg"; // No longer needed for closing
+import { runDeveloperSeeds } from "./developer-seeds.js";
/**
* Main function to run all seed operations.
@@ -16,8 +14,19 @@ async function seed() {
// 2. Create Default Groups and assign base permissions
await createDefaultGroups();
- // 3. Run Custom Seeds (admin user, example pages, etc.)
- await runCustomSeeds();
+ if (!process.env.SKIP_DEVELOPER_SEEDS) {
+ // 3. Run Developer Seeds (admin user, example pages, etc.)
+ await runDeveloperSeeds();
+
+ // 4. Run Custom Seeds
+ try {
+ const customSeeds = await import("./custom-seeds.js");
+ // @ts-expect-error - Custom seeds might not be defined
+ await customSeeds.runCustomSeeds();
+ } catch (error) {
+ console.warn(" Custom seeds probably not defined", error);
+ }
+ }
console.log("\n✅ Database seeding completed successfully.");
} catch (error) {
diff --git a/packages/db/tsup.config.ts b/packages/db/tsup.config.ts
index d17d116..11e1e56 100644
--- a/packages/db/tsup.config.ts
+++ b/packages/db/tsup.config.ts
@@ -20,7 +20,7 @@ export default defineConfig({
console.log("Build successful, copying pages directory...");
await new Promise((resolve, reject) => {
exec(
- "cp -R src/seeds/custom/pages dist/pages",
+ "cp -R src/seeds/developer/pages dist/pages",
(error, stdout, stderr) => {
if (error) {
console.error(`Error copying pages: ${error}`);
diff --git a/packages/ui/src/components/code-block.tsx b/packages/ui/src/components/code-block.tsx
index dd1d6c2..d923dcb 100644
--- a/packages/ui/src/components/code-block.tsx
+++ b/packages/ui/src/components/code-block.tsx
@@ -108,7 +108,7 @@ export function CodeBlock({
color="neutral"
onClick={handleCopy}
disabled={!codeString}
- className="bg-background-level2/60 hover:bg-background-level2/90 pl-2.5 pr-3 opacity-80 shadow-sm backdrop-blur-md transition-opacity hover:opacity-100"
+ className="bg-background-paper/20 dark:bg-background-level2/60 hover:bg-background-level2/90 pl-2.5 pr-3 text-gray-200 opacity-80 shadow-sm backdrop-blur-md transition-opacity hover:opacity-100"
aria-label="Copy code to clipboard"
>
{isCopied ? (
@@ -118,7 +118,7 @@ export function CodeBlock({
>
) : (
<>
-
+
Copy
>
)}
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css
index 3b258b1..202da33 100644
--- a/packages/ui/src/styles/globals.css
+++ b/packages/ui/src/styles/globals.css
@@ -16,11 +16,11 @@
--color-text-primary: var(--color-light-text-primary);
--color-text-secondary: var(--color-light-text-secondary);
--color-text-tertiary: var(--color-light-text-tertiary);
- --color-text-accent: var(--color-light-text-accent);
+ --color-text-accent: var(--color-accent-700);
--color-border-default: var(--color-light-border-default);
--color-border-light: var(--color-light-border-light);
--color-border-dark: var(--color-light-border-dark);
- --color-border-accent: var(--color-light-border-accent);
+ --color-border-accent: var(--color-accent-600);
--color-scrollbar-track: var(--color-light-scrollbar-track);
--color-scrollbar-thumb: var(--color-light-scrollbar-thumb);
@@ -35,30 +35,30 @@
--color-light-text-primary: #212529;
--color-light-text-secondary: #495057;
--color-light-text-tertiary: #6c757d;
- --color-light-text-accent: var(--color-primary-700);
+ --color-light-text-accent: var(--color-accent-700);
--color-light-border-default: #adb5bd;
--color-light-border-light: #ced4da;
--color-light-border-dark: #868e96;
- --color-light-border-accent: var(--color-primary-600);
+ --color-light-border-accent: var(--color-accent-600);
--color-light-scrollbar-track: #f1f3f5;
--color-light-scrollbar-thumb: #adb5bd;
/* Dark mode theme variables */
- --color-dark-background-default: #131313; /* Near black */
- --color-dark-background-paper: #1c1c1c; /* Slightly lighter paper */
- --color-dark-background-level1: #262626; /* Darker level 1 */
- --color-dark-background-level2: #2c2c2c; /* Darker level 2 */
- --color-dark-background-level3: #333333; /* Slightly lighter level 3 for better contrast */
+ --color-dark-background-default: #0d0d0d; /* Darker base */
+ --color-dark-background-paper: #1a1a1a; /* Increased contrast */
+ --color-dark-background-level1: #282828; /* Increased contrast */
+ --color-dark-background-level2: #353535; /* Increased contrast */
+ --color-dark-background-level3: #424242; /* Increased contrast */
--color-dark-background-light: #0a0a0a;
--color-dark-background-dark: #000000;
--color-dark-text-primary: #f8f9fa;
--color-dark-text-secondary: #e0e0e0;
--color-dark-text-tertiary: #a0a7ae;
- --color-dark-text-accent: var(--color-primary-300);
+ --color-dark-text-accent: var(--color-accent-300);
--color-dark-border-default: #303030;
--color-dark-border-light: #4a4a4a;
--color-dark-border-dark: #202020;
- --color-dark-border-accent: var(--color-primary-300);
+ --color-dark-border-accent: var(--color-accent-300);
--color-dark-scrollbar-track: #1f1f1f;
--color-dark-scrollbar-thumb: #4a4a4a;
@@ -82,7 +82,9 @@
--color-dark-inline-code-foreground: var(--color-primary-200);
/* Popover colors */
- --color-popover: var(--color-background-default);
+ --color-popover: var(
+ --color-dark-background-paper
+ ); /* Use paper for popover */
--color-popover-foreground: var(--color-text-primary);
--color-popover-border: var(--color-border-default);
@@ -112,55 +114,55 @@
--color-neon-glow-accent: 0 0 20px #00ffee, 0 0 40px #00ffee66;
--color-neon-glow-complementary: 0 0 20px #ff2d2d, 0 0 40px #ff2d2d66;
- /* Primary colors - expanded with darker tones */
- --color-primary-50: #fff9e6;
- --color-primary-100: #fff3cc;
- --color-primary-200: #ffe899;
- --color-primary-300: #ffd766;
- --color-primary-400: #ffc933;
- --color-primary-500: #ffba2e;
- --color-primary-600: #e6a829;
- --color-primary-700: #cc9621;
- --color-primary-800: #b38418;
- --color-primary-900: #99720f;
- --color-primary-1000: #7a5d0a;
- --color-primary-1100: #5c4706;
- --color-primary-1200: #3d3003;
+ /* Primary colors (Magenta - #FF00FF) */
+ --color-primary-50: #fff0ff;
+ --color-primary-100: #ffe0ff;
+ --color-primary-200: #ffb3ff;
+ --color-primary-300: #ff80ff;
+ --color-primary-400: #ff4dff;
+ --color-primary-500: #ff00ff;
+ --color-primary-600: #e600e6;
+ --color-primary-700: #b300b3;
+ --color-primary-800: #800080;
+ --color-primary-900: #4d004d;
+ --color-primary-1000: #330033;
+ --color-primary-1100: #1a001a;
+ --color-primary-1200: #0d000d;
--color-primary: var(--color-primary-500);
--color-primary-foreground: #ffffff;
- /* Secondary colors - expanded with darker tones */
- --color-secondary-50: #fff5ed;
- --color-secondary-100: #ffebdb;
- --color-secondary-200: #ffd7b7;
- --color-secondary-300: #ffc393;
- --color-secondary-400: #ffaf6f;
- --color-secondary-500: #ffad69;
- --color-secondary-600: #e59c5e;
- --color-secondary-700: #b27a4a;
- --color-secondary-800: #7f5836;
- --color-secondary-900: #4c3622;
- --color-secondary-1000: #382715;
- --color-secondary-1100: #24180c;
- --color-secondary-1200: #100a03;
+ /* Secondary colors (Orange - #FFA500) */
+ --color-secondary-50: #fff8e6;
+ --color-secondary-100: #ffedb3;
+ --color-secondary-200: #ffe080;
+ --color-secondary-300: #ffd44d;
+ --color-secondary-400: #ffc71a;
+ --color-secondary-500: #ffa500;
+ --color-secondary-600: #e69500;
+ --color-secondary-700: #b37400;
+ --color-secondary-800: #805300;
+ --color-secondary-900: #4d3200;
+ --color-secondary-1000: #332100;
+ --color-secondary-1100: #1a1100;
+ --color-secondary-1200: #0d0800;
--color-secondary: var(--color-secondary-500);
- --color-secondary-foreground: #ffffff;
-
- /* Accent colors - expanded with darker tones */
- --color-accent-50: #e6f7f9;
- --color-accent-100: #cceff3;
- --color-accent-200: #99dfe7;
- --color-accent-300: #66cfdb;
- --color-accent-400: #47bfd1;
- --color-accent-500: #33bfcf;
- --color-accent-600: #2da7b5;
- --color-accent-700: #237f8b;
- --color-accent-800: #195761;
- --color-accent-900: #0f2f37;
- --color-accent-1000: #0a202a;
- --color-accent-1100: #05121c;
- --color-accent-1200: #00050f;
- --color-accent: var(--color-accent-500);
+ --color-secondary-foreground: #ffffff; /* White foreground might be low contrast, adjust if needed */
+
+ /* Accent colors (Purple-Blue mix - #7f5aff) */
+ --color-accent-50: #e6e6ff;
+ --color-accent-100: #ccccff;
+ --color-accent-200: #9999ff;
+ --color-accent-300: #6666ff;
+ --color-accent-400: #3333ff;
+ --color-accent-500: #0000ff;
+ --color-accent-600: #0000cc;
+ --color-accent-700: #000099;
+ --color-accent-800: #000066;
+ --color-accent-900: #000033;
+ --color-accent-1000: #00001a;
+ --color-accent: var(
+ --color-accent-600
+ ); /* Use a slightly darker shade for default accent */
--color-accent-foreground: #ffffff;
/* Muted colors */
@@ -171,22 +173,22 @@
--color-muted-foreground-darker: #2f2f35;
--color-muted-foreground-lighter: #7d7d85;
- /* Complementary colors - expanded with darker tones */
- --color-complementary-50: #fde6e8;
- --color-complementary-100: #fbcdd1;
- --color-complementary-200: #f79ba3;
- --color-complementary-300: #f36975;
- --color-complementary-400: #ef4957;
- --color-complementary-500: #ef3747;
- --color-complementary-600: #d62f3d;
- --color-complementary-700: #a82430;
- --color-complementary-800: #7a1923;
- --color-complementary-900: #4c0f16;
- --color-complementary-1000: #380a0f;
- --color-complementary-1100: #240508;
- --color-complementary-1200: #100001;
+ /* Complementary colors (Light Yellow - Base #FFD700) - Increased contrast */
+ --color-complementary-50: #fff9e6;
+ --color-complementary-100: #fff3cc;
+ --color-complementary-200: #ffe699;
+ --color-complementary-300: #ffd966;
+ --color-complementary-400: #ffcc33;
+ --color-complementary-500: #ffd700;
+ --color-complementary-600: #e6c200;
+ --color-complementary-700: #ccad00;
+ --color-complementary-800: #997f00;
+ --color-complementary-900: #665500;
+ --color-complementary-1000: #4d4000;
+ --color-complementary-1100: #332a00;
+ --color-complementary-1200: #1a1500;
--color-complementary: var(--color-complementary-500);
- --color-complementary-foreground: #ffffff;
+ --color-complementary-foreground: #000000; /* Black needed for contrast */
/* Success colors - expanded with darker tones */
--color-success-50: #e6f7e6;
@@ -205,22 +207,22 @@
--color-success: var(--color-success-600);
--color-success-foreground: #ffffff;
- /* Warning colors - expanded with darker tones */
- --color-warning-50: #fff8e6;
- --color-warning-100: #fff1cc;
- --color-warning-200: #ffe499;
- --color-warning-300: #ffd666;
- --color-warning-400: #ffc933;
- --color-warning-500: #ffbf1f;
- --color-warning-600: #e0a300;
- --color-warning-700: #b38300;
- --color-warning-800: #856200;
- --color-warning-900: #574100;
- --color-warning-1000: #3a2c00;
- --color-warning-1100: #1d1600;
- --color-warning-1200: #0f0b00;
+ /* Warning colors (Gold/Yellow - #FFD700) */
+ --color-warning-50: #fffbe6;
+ --color-warning-100: #fff5cc;
+ --color-warning-200: #ffeb99;
+ --color-warning-300: #ffe066;
+ --color-warning-400: #ffd733;
+ --color-warning-500: #ffd700;
+ --color-warning-600: #e6bf00;
+ --color-warning-700: #b39500;
+ --color-warning-800: #806b00;
+ --color-warning-900: #4d4100;
+ --color-warning-1000: #332b00;
+ --color-warning-1100: #1a1600;
+ --color-warning-1200: #0d0b00;
--color-warning: var(--color-warning-600);
- --color-warning-foreground: #ffffff;
+ --color-warning-foreground: #000000; /* Black needed for contrast */
/* Destructive colors - expanded with darker tones */
--color-destructive-50: #fdebeb;
@@ -291,27 +293,27 @@
--color-neutral-foreground: #ffffff;
/* Markdown specific colors */
- --color-markdown-h1: var(--color-primary-400);
- --color-markdown-h2: var(--color-accent-400);
- --color-markdown-h3: var(--color-secondary-400);
+ --color-markdown-h1: var(--color-complementary-400);
+ --color-markdown-h2: var(--color-primary-400);
+ --color-markdown-h3: var(--color-text-primary);
--color-markdown-h4: var(--color-text-secondary);
--color-markdown-text: var(--color-text-primary);
- --color-markdown-link: var(--color-primary);
+ --color-markdown-link: var(--color-accent-500);
--color-markdown-codebg: var(--color-code);
--color-markdown-codetext: var(--color-code-foreground);
--color-markdown-inlinecodebg: var(--color-inline-code);
--color-markdown-inlinecodetext: var(--color-inline-code-foreground);
--color-markdown-blockquote: var(--color-muted-foreground-lighter);
- --color-markdown-listmarker: var(--color-primary-400);
+ --color-markdown-listmarker: var(--color-accent-400);
/* Border and input colors */
--color-border: var(--color-border-default);
--color-border-hover: var(--color-border-dark);
- --color-border-focus: var(--color-primary-400);
+ --color-border-focus: var(--color-accent-400);
--color-input: var(--color-background-level1);
--color-input-hover: var(--color-background-level2);
--color-input-focus: var(--color-background-level2);
- --color-ring: var(--color-primary-500);
+ --color-ring: var(--color-accent-500);
/* Gradients */
--gradient-light-1: linear-gradient(
@@ -384,7 +386,9 @@
--color-card-active: var(--color-background-level3);
--color-card-foreground: var(--color-text-primary);
- --color-popover: var(--color-background-level1);
+ --color-popover: var(
+ --color-dark-background-paper
+ ); /* Use paper for popover */
--color-popover-foreground: var(--color-text-primary);
--color-code: var(--color-dark-code);
@@ -395,18 +399,18 @@
--color-muted-foreground: #adb5bd;
/* Update markdown-specific colors for dark mode */
- --color-markdown-h1: var(--color-primary-400);
- --color-markdown-h2: var(--color-accent-400);
- --color-markdown-h3: var(--color-secondary-400);
- --color-markdown-h4: var(--color-dark-text-secondary);
- --color-markdown-text: var(--color-dark-text-primary);
- --color-markdown-link: var(--color-primary-400);
+ --color-markdown-h1: var(--color-complementary-400);
+ --color-markdown-h2: var(--color-primary-400);
+ --color-markdown-h3: var(--color-text-primary);
+ --color-markdown-h4: var(--color-text-secondary);
+ --color-markdown-text: var(--color-text-primary);
+ --color-markdown-link: var(--color-accent-400);
--color-markdown-codebg: var(--color-dark-code);
--color-markdown-codetext: var(--color-dark-code-foreground);
--color-markdown-inlinecodebg: var(--color-dark-inline-code);
--color-markdown-inlinecodetext: var(--color-dark-inline-code-foreground);
--color-markdown-blockquote: var(--color-muted-foreground);
- --color-markdown-listmarker: var(--color-primary-400);
+ --color-markdown-listmarker: var(--color-accent-400);
/* Update input colors for dark mode */
--color-input: var(--color-background-level1);
@@ -415,26 +419,28 @@
}
}
-/* Style for the scroll indicator container */
-.scroll-indicator-container::after {
- content: "";
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 10rem; /* Adjust height as needed */
- background: linear-gradient(
- to top,
- var(--color-background-paper),
- transparent
- );
- pointer-events: none;
- z-index: 10;
- opacity: 0; /* Hidden by default */
- transition: opacity 0.4s ease-in-out;
-}
+@layer {
+ /* Style for the scroll indicator container */
+ .scroll-indicator-container::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 10rem; /* Adjust height as needed */
+ background: linear-gradient(
+ to top,
+ var(--color-background-paper),
+ transparent
+ );
+ pointer-events: none;
+ z-index: 10;
+ opacity: 0; /* Hidden by default */
+ transition: opacity 0.4s ease-in-out;
+ }
-/* Show the indicator only when scrollable AND not at the bottom */
-.scroll-indicator-container.is-scrollable[data-scroll-bottom="false"]::after {
- opacity: 1;
+ /* Show the indicator only when scrollable AND not at the bottom */
+ .scroll-indicator-container.is-scrollable[data-scroll-bottom="false"]::after {
+ opacity: 1;
+ }
}
diff --git a/packages/ui/src/styles/markdown.css b/packages/ui/src/styles/markdown.css
index 1c5ee67..d7edf9d 100644
--- a/packages/ui/src/styles/markdown.css
+++ b/packages/ui/src/styles/markdown.css
@@ -11,7 +11,7 @@
}
ul.links-list li {
- @apply bg-background-paper/50 dark:bg-background-level2 hover:border-primary/50 dark:hover:bg-background-level3 dark:border-border-dark border-l-primary dark:border-l-primary hover:border-l-primary flex items-start rounded-lg border border-l-4 border-transparent shadow-lg transition-all duration-200 hover:shadow-xl dark:shadow-none;
+ @apply bg-background-paper/50 dark:bg-background-level2 hover:border-complementary/50 dark:hover:bg-background-level3 dark:border-border-dark border-l-secondary hover:border-l-secondary flex items-start rounded-lg border border-l-4 border-transparent shadow-lg transition-all duration-200 hover:shadow-xl dark:shadow-none;
}
/* Emoji styling */
@@ -21,11 +21,11 @@
/* Specific styling for links with emojis */
ul.links-list li a {
- @apply text-primary flex w-full items-center gap-8 p-4 font-bold no-underline hover:no-underline;
+ @apply text-secondary flex w-full items-center gap-8 p-4 font-bold no-underline hover:no-underline;
}
ul.links-list li a em {
- @apply border-border -ml-6 border-l pl-2;
+ @apply border-border-default -ml-6 border-l pl-2;
}
ul.links-list li .emoji {
@@ -41,7 +41,7 @@
}
a.wiki-link-exists {
- @apply text-primary no-underline hover:underline;
+ @apply text-complementary no-underline hover:underline;
}
a.wiki-link-missing {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3d48d0d..a18b265 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -442,9 +442,15 @@ importers:
'@neondatabase/serverless':
specifier: ^1.0.0
version: 1.0.0
+ '@repo/logger':
+ specifier: workspace:*
+ version: link:../logger
bcryptjs:
specifier: ^3.0.2
version: 3.0.2
+ child_process:
+ specifier: ^1.0.2
+ version: 1.0.2
dotenv:
specifier: ^16.5.0
version: 16.5.0