Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"env": {
"GIT_AUTHOR_NAME": "Shivam Kumar",
"GIT_COMMITTER_NAME": "Shivam Kumar",
"GIT_AUTHOR_EMAIL": "csautomaytic@gmail.com",
"GIT_COMMITTER_EMAIL": "csautomaytic@gmail.com"
},
Comment on lines +3 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not force a personal Git identity in shared repository settings.

These environment variables make agent-created commits from every contributor appear authored and committed by the named individual, and publish their email in repository config. Remove this block and keep author identity in each developer’s local Git/Claude configuration.

Proposed fix
-  "env": {
-    "GIT_AUTHOR_NAME": "Shivam Kumar",
-    "GIT_COMMITTER_NAME": "Shivam Kumar",
-    "GIT_AUTHOR_EMAIL": "csautomaytic@gmail.com",
-    "GIT_COMMITTER_EMAIL": "csautomaytic@gmail.com"
-  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"env": {
"GIT_AUTHOR_NAME": "Shivam Kumar",
"GIT_COMMITTER_NAME": "Shivam Kumar",
"GIT_AUTHOR_EMAIL": "csautomaytic@gmail.com",
"GIT_COMMITTER_EMAIL": "csautomaytic@gmail.com"
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/settings.json around lines 3 - 8, Remove the env block from the
Claude settings configuration, including the GIT_AUTHOR_NAME,
GIT_COMMITTER_NAME, GIT_AUTHOR_EMAIL, and GIT_COMMITTER_EMAIL entries, so shared
repository settings do not impose a personal Git identity.

"includeCoAuthoredBy": false
}
35 changes: 32 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
SESSION_SECRET=your-secure-secret
FORCE_HTTPS=false # if HTTPS issues
COOKIE_DOMAIN=yourdomain.com # if domain issues
# Database Configuration (Required)
DATABASE_URL=postgresql://user:password@host:port/database?sslmode=require

# Server Configuration
PORT=3000
NODE_ENV=development

# Session Configuration (Required in production)
SESSION_SECRET=your-secure-random-secret-change-this-in-production
FORCE_HTTPS=false # Set to true in production with HTTPS
COOKIE_DOMAIN= # Optional: Set for multi-subdomain support (e.g., .yourdomain.com)

# Email Configuration (SMTP)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false # true for 465, false for other ports
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-specific-password
SMTP_FROM=noreply@linkboard.com # Optional: defaults to SMTP_USER
APP_URL=http://localhost:3000 # Used in email links

# Optional: Error Tracking with Sentry
# Leave empty to disable Sentry (app works perfectly without it)
SENTRY_DSN= # Your Sentry DSN from sentry.io
SENTRY_ENVIRONMENT=production # Optional: defaults to NODE_ENV
SENTRY_TRACES_SAMPLE_RATE=0.1 # Optional: 10% of transactions (0.0 to 1.0)
SENTRY_PROFILES_SAMPLE_RATE=0.1 # Optional: 10% of profiles (0.0 to 1.0)

# Optional: Redis Caching
# Leave empty to disable Redis caching (app works perfectly without it)
REDIS_URL= # redis://localhost:6379 or redis://user:pass@host:port
REDIS_CACHE_TTL=3600 # Optional: Cache TTL in seconds (default: 3600 = 1 hour)
4 changes: 4 additions & 0 deletions .replit
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ externalPort = 80
localPort = 37849
externalPort = 3000

[[ports]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Port mapping localPort = 41423externalPort = 3001 has no corresponding service. The admin dashboard is a client-side React feature served through the existing app — it does not run on a separate port. Remove this unused mapping to avoid confusion and idle port exposure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .replit, line 21:

<comment>Port mapping `localPort = 41423` → `externalPort = 3001` has no corresponding service. The admin dashboard is a client-side React feature served through the existing app — it does not run on a separate port. Remove this unused mapping to avoid confusion and idle port exposure.</comment>

<file context>
@@ -18,6 +18,10 @@ externalPort = 80
 localPort = 37849
 externalPort = 3000
 
+[[ports]]
+localPort = 41423
+externalPort = 3001
</file context>

localPort = 41423
externalPort = 3001

[env]
PORT = "5000"

Expand Down
8 changes: 8 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import VerifyEmail from "@/pages/verify-email";
import Dashboard from "@/pages/dashboard";
import Home from "@/pages/home";
import Analytics from "@/pages/analytics";
import AdminOverview from "@/pages/admin/overview";
import AdminUsers from "@/pages/admin/users";
import AdminProfiles from "@/pages/admin/profiles";
import AdminSettings from "@/pages/admin/settings";
import NotFound from "@/pages/not-found";

/**
Expand All @@ -31,6 +35,10 @@ function Router() {
<Route path="/verify-email" component={VerifyEmail} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/analytics/:profileId" component={Analytics} />
<Route path="/admin" component={AdminOverview} />
<Route path="/admin/users" component={AdminUsers} />
<Route path="/admin/profiles" component={AdminProfiles} />
<Route path="/admin/settings" component={AdminSettings} />
<Route path="/:username" component={Home} />
<Route component={NotFound} />
</Switch>
Expand Down
125 changes: 125 additions & 0 deletions client/src/components/admin/AdminLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Admin Dashboard Layout Component
* Professional sidebar layout for admin dashboard
*/

import { Link, useLocation } from "wouter";
import {
LayoutDashboard,
Users,
FileText,
LogOut,
Menu,
X,
Settings
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { useState } from "react";

interface AdminLayoutProps {
children: React.ReactNode;
}

export function AdminLayout({ children }: AdminLayoutProps) {
const [location] = useLocation();
const [sidebarOpen, setSidebarOpen] = useState(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Every admin page opens with the mobile drawer and backdrop covering its content, requiring an extra close action before the dashboard can be used. Initialize the drawer closed; lg:translate-x-0 already keeps it permanently visible on desktop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/admin/AdminLayout.tsx, line 25:

<comment>Every admin page opens with the mobile drawer and backdrop covering its content, requiring an extra close action before the dashboard can be used. Initialize the drawer closed; `lg:translate-x-0` already keeps it permanently visible on desktop.</comment>

<file context>
@@ -0,0 +1,125 @@
+
+export function AdminLayout({ children }: AdminLayoutProps) {
+  const [location] = useLocation();
+  const [sidebarOpen, setSidebarOpen] = useState(true);
+
+  const navigation = [
</file context>
Suggested change
const [sidebarOpen, setSidebarOpen] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(false);


const navigation = [
{ name: "Overview", href: "/admin", icon: LayoutDashboard },
{ name: "Users", href: "/admin/users", icon: Users },
{ name: "Profiles", href: "/admin/profiles", icon: FileText },
{ name: "Settings", href: "/admin/settings", icon: Settings },
];

const handleLogout = async () => {
try {
await fetch("/api/auth/logout", { method: "POST" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A failed logout still redirects to /login, even when the server kept the session after a 500 response. Check response.ok before redirecting so the user can retry instead of being shown a login page while still authenticated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/admin/AdminLayout.tsx, line 36:

<comment>A failed logout still redirects to `/login`, even when the server kept the session after a 500 response. Check `response.ok` before redirecting so the user can retry instead of being shown a login page while still authenticated.</comment>

<file context>
@@ -0,0 +1,125 @@
+
+  const handleLogout = async () => {
+    try {
+      await fetch("/api/auth/logout", { method: "POST" });
+      window.location.href = "/login";
+    } catch (error) {
</file context>

window.location.href = "/login";
} catch (error) {
console.error("Logout error:", error);
}
};

return (
<div className="min-h-screen bg-gray-50">
{/* Mobile sidebar toggle */}
<div className="lg:hidden fixed top-0 left-0 right-0 bg-white border-b z-50 px-4 py-3 flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-900">Admin Dashboard</h1>
<Button
variant="ghost"
size="icon"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The mobile menu control has no accessible name, so screen-reader users cannot tell whether it opens or closes navigation. Add a state-specific aria-label to the icon-only button.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/admin/AdminLayout.tsx, line 50:

<comment>The mobile menu control has no accessible name, so screen-reader users cannot tell whether it opens or closes navigation. Add a state-specific `aria-label` to the icon-only button.</comment>

<file context>
@@ -0,0 +1,125 @@
+        <h1 className="text-xl font-bold text-gray-900">Admin Dashboard</h1>
+        <Button
+          variant="ghost"
+          size="icon"
+          onClick={() => setSidebarOpen(!sidebarOpen)}
+        >
</file context>

onClick={() => setSidebarOpen(!sidebarOpen)}
>
{sidebarOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</Button>
</div>

{/* Sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-40 w-64 bg-white border-r border-gray-200 transform transition-transform duration-200 ease-in-out ${
sidebarOpen ? "translate-x-0" : "-translate-x-full"
} lg:translate-x-0 mt-14 lg:mt-0`}
>
<div className="flex flex-col h-full">
{/* Logo */}
<div className="flex items-center justify-between px-6 py-5 border-b border-gray-200">
<h1 className="text-2xl font-bold text-gray-900">LinkBoard</h1>
<span className="px-2 py-1 text-xs font-medium text-blue-600 bg-blue-50 rounded">
Admin
</span>
</div>

{/* Navigation */}
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
{navigation.map((item) => {
const Icon = item.icon;
const isActive = location === item.href;

return (
<Link key={item.name} href={item.href}>
<a
className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${
isActive
? "bg-blue-50 text-blue-600"
: "text-gray-700 hover:bg-gray-50 hover:text-gray-900"
}`}
>
<Icon className="w-5 h-5 mr-3" />
{item.name}
</a>
</Link>
Comment on lines +79 to +90

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

wouter 3.3.5 Link nested anchor tag child render behavior

💡 Result:

In wouter 3.3.5, the Link component's rendering behavior regarding child elements is determined by the presence of the asChild prop [1][2]. Default Behavior By default, the Link component wraps all its children in a standard HTML tag [1][3][4]. This is the standard mode used for simple navigation links [1]. asChild Behavior When the asChild prop is provided, the Link component does not wrap its children in an tag [1][3]. Instead, it passes navigation props (such as href and, depending on implementation, other attributes) directly to the single child element provided [1][4]. Key Considerations for asChild: 1. Valid Children: The asChild mode requires a single valid React element child [5][4]. Passing multiple children or text nodes directly without a wrapping element will not function as intended and may trigger errors or unexpected behavior [5][4]. 2. Prop Forwarding: When using asChild, ensure the child component is capable of receiving and applying the props injected by Link (e.g., href, onClick) [2][6]. If you are using a custom component as a child, it must be designed to accept these attributes for navigation to work [2]. 3. Nesting: While Link supports nesting within routes, the asChild behavior itself is independent of route nesting contexts [3][7]. It is a mechanism for component composition rather than routing structure [5]. If you need to render a custom component (like a button or a styled link) as the navigation element, the asChild pattern is the correct approach to avoid invalid HTML structures (such as nested tags) [3][2].

Citations:


🏁 Script executed:

set -euo pipefail

printf '\n== package files ==\n'
git ls-files 'package.json' 'client/package.json' 'pnpm-lock.yaml' 'package-lock.json' 'yarn.lock' | sed 's#^`#-` #'

printf '\n== AdminLayout references ==\n'
rg -n '"wouter"|from '\''wouter'\''|from "wouter"|<Link|asChild' client/src/components/admin/AdminLayout.tsx client/package.json package.json 2>/dev/null || true

printf '\n== AdminLayout excerpt ==\n'
cat -n client/src/components/admin/AdminLayout.tsx | sed -n '1,170p'

Repository: WizzyWeb/LinkBoard

Length of output: 5289


🏁 Script executed:

set -euo pipefail

echo '== package.json =='
if [ -f package.json ]; then
  cat -n package.json | sed -n '1,220p'
fi

echo
echo '== client/package.json =='
if [ -f client/package.json ]; then
  cat -n client/package.json | sed -n '1,220p'
fi

echo
echo '== AdminLayout first 120 lines =='
cat -n client/src/components/admin/AdminLayout.tsx | sed -n '1,120p'

Repository: WizzyWeb/LinkBoard

Length of output: 10154


Remove the inner <a>
wouter Link already renders an anchor in v3, so this creates invalid nested markup. Move the className and children onto Link directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/admin/AdminLayout.tsx` around lines 79 - 90, Update the
navigation item rendering in AdminLayout to remove the nested a element, since
wouter Link renders the anchor itself. Move the existing className, Icon, and
item.name children directly onto Link while preserving the key, href, and
active-state styling.

Comment on lines +79 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Each sidebar navigation item renders nested anchor elements, creating invalid interactive markup and unreliable keyboard/click behavior. Put the styling and contents directly on Link (or use its composition API).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/admin/AdminLayout.tsx, line 79:

<comment>Each sidebar navigation item renders nested anchor elements, creating invalid interactive markup and unreliable keyboard/click behavior. Put the styling and contents directly on `Link` (or use its composition API).</comment>

<file context>
@@ -0,0 +1,125 @@
+              const isActive = location === item.href;
+
+              return (
+                <Link key={item.name} href={item.href}>
+                  <a
+                    className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${
</file context>
Suggested change
<Link key={item.name} href={item.href}>
<a
className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${
isActive
? "bg-blue-50 text-blue-600"
: "text-gray-700 hover:bg-gray-50 hover:text-gray-900"
}`}
>
<Icon className="w-5 h-5 mr-3" />
{item.name}
</a>
</Link>
<Link
key={item.name}
href={item.href}
className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${
isActive
? "bg-blue-50 text-blue-600"
: "text-gray-700 hover:bg-gray-50 hover:text-gray-900"
}`}
>
<Icon className="w-5 h-5 mr-3" />
{item.name}
</Link>

);
})}
</nav>

{/* Logout */}
<div className="p-4 border-t border-gray-200">
<Button
variant="outline"
className="w-full justify-start"
onClick={handleLogout}
>
<LogOut className="w-4 h-4 mr-2" />
Logout
</Button>
</div>
</div>
</aside>

{/* Main content */}
<div className="lg:pl-64 pt-14 lg:pt-0">
<main className="p-6 lg:p-8">
{children}
</main>
</div>

{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-30 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
</div>
);
}
163 changes: 163 additions & 0 deletions client/src/components/admin/AdminPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Admin Panel Component
* Displays admin controls and user list in the dashboard
*/

import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Link } from "wouter";
import { Users, Shield, ExternalLink, ChevronRight, FileText, Link as LinkIcon } from "lucide-react";

export function AdminPanel() {
// Fetch admin stats
const { data: statsData } = useQuery({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Dashboard load makes a complete /api/admin/stats request whose result is discarded, adding avoidable database work alongside the users request. Remove this query unless this panel will render its data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/admin/AdminPanel.tsx, line 23:

<comment>Dashboard load makes a complete `/api/admin/stats` request whose result is discarded, adding avoidable database work alongside the users request. Remove this query unless this panel will render its data.</comment>

<file context>
@@ -0,0 +1,163 @@
+
+export function AdminPanel() {
+  // Fetch admin stats
+  const { data: statsData } = useQuery({
+    queryKey: ["/api/admin/stats"],
+    queryFn: async () => {
</file context>

queryKey: ["/api/admin/stats"],
queryFn: async () => {
const res = await fetch("/api/admin/stats");
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
},
retry: false,
});

// Fetch all users
const { data, isLoading, error } = useQuery({
queryKey: ["/api/admin/users", 1],
queryFn: async () => {
const res = await fetch(`/api/admin/users?page=1&limit=10`);
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
},
retry: false,
});

if (error) {
return (
<Card className="border-red-200 bg-red-50">
<CardContent className="pt-6">
<p className="text-red-600 text-sm">
Unable to load admin data. Please check your permissions.
</p>
</CardContent>
</Card>
);
}

const users = data?.users || [];
const pagination = data?.pagination || { total: 0 };

return (
<Card className="border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-600" />
<CardTitle className="text-xl">Admin Panel</CardTitle>
<Badge variant="default" className="bg-blue-600">
Admin
</Badge>
</div>
<Link href="/admin">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The navigation controls render a button inside a link, which is invalid nested interactive content and produces unreliable keyboard/screen-reader semantics. Render the link as the button via Button asChild instead; apply the same pattern to “View All”.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/admin/AdminPanel.tsx, line 70:

<comment>The navigation controls render a button inside a link, which is invalid nested interactive content and produces unreliable keyboard/screen-reader semantics. Render the link as the button via `Button asChild` instead; apply the same pattern to “View All”.</comment>

<file context>
@@ -0,0 +1,163 @@
+              Admin
+            </Badge>
+          </div>
+          <Link href="/admin">
+            <Button variant="outline" size="sm" className="flex items-center gap-2">
+              Full Admin Dashboard
</file context>

<Button variant="outline" size="sm" className="flex items-center gap-2">
Full Admin Dashboard
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
<p className="text-gray-500 text-sm">Loading users...</p>
</div>
) : (
<>
<div className="mb-4">
<p className="text-sm text-gray-600">
Total Registered Users: <span className="font-semibold text-gray-900">{pagination.total}</span>
</p>
</div>

{users.length === 0 ? (
<p className="text-gray-500 text-sm text-center py-4">No users found</p>
) : (
<div className="rounded-md border bg-white">
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead>Role</TableHead>
<TableHead>Joined</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user: any) => (
<TableRow key={user.id}>
<TableCell className="font-medium">
{user.firstName && user.lastName
? `${user.firstName} ${user.lastName}`
: "—"}
</TableCell>
<TableCell className="text-sm">{user.email}</TableCell>
<TableCell>
{user.isEmailVerified ? (
<Badge variant="default" className="bg-green-100 text-green-800 text-xs">
Verified
</Badge>
) : (
<Badge variant="secondary" className="text-xs">
Unverified
</Badge>
)}
</TableCell>
<TableCell>
{user.isAdmin ? (
<Badge variant="default" className="bg-blue-100 text-blue-800 text-xs">
Admin
</Badge>
) : (
<Badge variant="outline" className="text-xs">
User
</Badge>
)}
</TableCell>
<TableCell className="text-sm text-gray-500">
{new Date(user.createdAt).toLocaleDateString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}

{pagination.total > 10 && (
<div className="mt-4 text-center">
<Link href="/admin/users">
<Button variant="outline" size="sm">
View All {pagination.total} Users
<ExternalLink className="h-4 w-4 ml-2" />
</Button>
</Link>
</div>
)}
</>
)}
</CardContent>
</Card>
);
}

Loading