diff --git a/check_hash_pw.js b/check_hash_pw.js new file mode 100644 index 0000000..52be786 --- /dev/null +++ b/check_hash_pw.js @@ -0,0 +1,10 @@ +const bcrypt = require('bcryptjs'); + +// Replace with your plaintext password and hash +const plaintextPassword = 'newpassword'; // <-- put your real password here +const hash = '$2b$10$n78/VuxwnDoOemWoqjVKnunz5PZy7SisG3VUhsPtQXKEEnMej6TWK'; + +bcrypt.compare(plaintextPassword, hash, (err, result) => { + if (err) throw err; + console.log('Password matches hash?', result); // true if matches, false if not +}); \ No newline at end of file diff --git a/hash-password.js b/hash-password.js new file mode 100644 index 0000000..d66a8cb --- /dev/null +++ b/hash-password.js @@ -0,0 +1,12 @@ +const bcrypt = require('bcryptjs'); + +const password = process.argv[2]; +if (!password) { + console.error('Usage: node hash-password.js '); + process.exit(1); +} + +bcrypt.hash(password, 10, (err, hash) => { + if (err) throw err; + console.log('Hashed password:', hash); +}); \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..9a60402 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,16 @@ +import { NextResponse, NextRequest } from 'next/server'; +import { getToken } from 'next-auth/jwt'; + +export async function middleware(request: NextRequest) { + if (request.nextUrl.pathname.startsWith('/admin')) { + const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); + if (!token || !token.isAdmin) { + return NextResponse.redirect(new URL('/account/login', request.url)); + } + } + return NextResponse.next(); +} + +export const config = { + matcher: ['/admin/:path*'], +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 55e3ee8..dad7efc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,16 @@ "version": "0.1.0", "dependencies": { "@auth/core": "^0.34.2", + "@auth/drizzle-adapter": "^1.10.0", "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "autoprefixer": "^10.4.21", + "bcryptjs": "^3.0.2", "daisyui": "^4.7.3", + "date-fns": "^4.1.0", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.2", + "lucide-react": "^0.525.0", "next": "^14.2.30", "next-auth": "^4.24.11", "pg": "^8.16.3", @@ -75,6 +79,75 @@ } } }, + "node_modules/@auth/drizzle-adapter": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@auth/drizzle-adapter/-/drizzle-adapter-1.10.0.tgz", + "integrity": "sha512-3MKsdAINTfvV4QKev8PFMNG93HJEUHh9sggDXnmUmriFogRf8qLvgqnPsTlfUyWcLwTzzrrYjeu8CGM+4IxHwQ==", + "dependencies": { + "@auth/core": "0.40.0" + } + }, + "node_modules/@auth/drizzle-adapter/node_modules/@auth/core": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", + "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/drizzle-adapter/node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@auth/drizzle-adapter/node_modules/oauth4webapi": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.3.tgz", + "integrity": "sha512-2bnHosmBLAQpXNBLOvaJMyMkr4Yya5ohE5Q9jqyxiN+aa7GFCzvDN1RRRMrp0NkfqRR2MTaQNkcSUCCjILD9oQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@auth/drizzle-adapter/node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@auth/drizzle-adapter/node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/@babel/runtime": { "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", @@ -2505,6 +2578,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2890,6 +2971,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -4898,6 +4989,15 @@ "node": ">=10" } }, + "node_modules/lucide-react": { + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 4e71a9e..7f968d2 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,16 @@ }, "dependencies": { "@auth/core": "^0.34.2", + "@auth/drizzle-adapter": "^1.10.0", "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "autoprefixer": "^10.4.21", + "bcryptjs": "^3.0.2", "daisyui": "^4.7.3", + "date-fns": "^4.1.0", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.2", + "lucide-react": "^0.525.0", "next": "^14.2.30", "next-auth": "^4.24.11", "pg": "^8.16.3", diff --git a/src/app/account/forgot-password/page.tsx b/src/app/(main)/account/forgot-password/page.tsx similarity index 100% rename from src/app/account/forgot-password/page.tsx rename to src/app/(main)/account/forgot-password/page.tsx diff --git a/src/app/account/layout.tsx b/src/app/(main)/account/layout.tsx similarity index 100% rename from src/app/account/layout.tsx rename to src/app/(main)/account/layout.tsx diff --git a/src/app/account/login/page.tsx b/src/app/(main)/account/login/page.tsx similarity index 100% rename from src/app/account/login/page.tsx rename to src/app/(main)/account/login/page.tsx diff --git a/src/app/account/profile/page.tsx b/src/app/(main)/account/profile/page.tsx similarity index 100% rename from src/app/account/profile/page.tsx rename to src/app/(main)/account/profile/page.tsx diff --git a/src/app/account/register/page.tsx b/src/app/(main)/account/register/page.tsx similarity index 100% rename from src/app/account/register/page.tsx rename to src/app/(main)/account/register/page.tsx diff --git a/src/app/build/page.tsx b/src/app/(main)/build/page.tsx similarity index 100% rename from src/app/build/page.tsx rename to src/app/(main)/build/page.tsx diff --git a/src/app/builds/page.tsx b/src/app/(main)/builds/page.tsx similarity index 100% rename from src/app/builds/page.tsx rename to src/app/(main)/builds/page.tsx diff --git a/src/app/daisyui-demo/page.tsx b/src/app/(main)/daisyui-demo/page.tsx similarity index 100% rename from src/app/daisyui-demo/page.tsx rename to src/app/(main)/daisyui-demo/page.tsx diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx new file mode 100644 index 0000000..75ad50c --- /dev/null +++ b/src/app/(main)/layout.tsx @@ -0,0 +1,20 @@ +import "../globals.css"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import Providers from "@/components/Providers"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Pew Builder - Firearm Parts Catalog & Build Management", + description: "Professional firearm parts catalog and AR-15 build management system", +}; + +export default function MainAppLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ); +} \ No newline at end of file diff --git a/src/app/my-builds/page.tsx b/src/app/(main)/my-builds/page.tsx similarity index 100% rename from src/app/my-builds/page.tsx rename to src/app/(main)/my-builds/page.tsx diff --git a/src/app/page.tsx b/src/app/(main)/page.tsx similarity index 97% rename from src/app/page.tsx rename to src/app/(main)/page.tsx index 26e4591..9c3e35e 100644 --- a/src/app/page.tsx +++ b/src/app/(main)/page.tsx @@ -1,4 +1,4 @@ -import BetaTester from "../components/BetaTester"; +import BetaTester from "@/components/BetaTester"; import Link from 'next/link'; export default function LandingPage() { diff --git a/src/app/parts/categoryMapping.ts b/src/app/(main)/parts/categoryMapping.ts similarity index 100% rename from src/app/parts/categoryMapping.ts rename to src/app/(main)/parts/categoryMapping.ts diff --git a/src/app/parts/page.tsx b/src/app/(main)/parts/page.tsx similarity index 100% rename from src/app/parts/page.tsx rename to src/app/(main)/parts/page.tsx diff --git a/src/app/products/[slug]/page.tsx b/src/app/(main)/products/[slug]/page.tsx similarity index 100% rename from src/app/products/[slug]/page.tsx rename to src/app/(main)/products/[slug]/page.tsx diff --git a/src/app/products/page.tsx b/src/app/(main)/products/page.tsx similarity index 100% rename from src/app/products/page.tsx rename to src/app/(main)/products/page.tsx diff --git a/src/app/test-products/page.tsx b/src/app/(main)/test-products/page.tsx similarity index 100% rename from src/app/test-products/page.tsx rename to src/app/(main)/test-products/page.tsx diff --git a/src/app/admin/AdminNavbar.tsx b/src/app/admin/AdminNavbar.tsx new file mode 100644 index 0000000..fc21560 --- /dev/null +++ b/src/app/admin/AdminNavbar.tsx @@ -0,0 +1,259 @@ +"use client"; +import { useState, ReactNode } from 'react'; +import { usePathname } from 'next/navigation'; +import { + Dialog, + DialogBackdrop, + DialogPanel, + Menu, + MenuButton, + MenuItem, + MenuItems, + TransitionChild, +} from '@headlessui/react'; +import { + Bars3Icon, + BellIcon, + CalendarIcon, + ChartPieIcon, + Cog6ToothIcon, + DocumentDuplicateIcon, + FolderIcon, + HomeIcon, + UsersIcon, + XMarkIcon, +} from '@heroicons/react/24/outline'; +import { ChevronDownIcon, MagnifyingGlassIcon } from '@heroicons/react/20/solid'; + +const navigation = [ + { name: 'Dashboard', href: '/admin', icon: HomeIcon }, + { name: 'Users', href: '/admin/users', icon: UsersIcon }, + { name: 'Category Mapping', href: '/admin/category-mapping', icon: ChartPieIcon }, + // { name: 'Settings', href: '/admin/settings', icon: Cog6ToothIcon }, // optional/future +]; +const userNavigation = [ + { name: 'Your profile', href: '#' }, + { name: 'Sign out', href: '#' }, +]; + +function classNames(...classes: string[]) { + return classes.filter(Boolean).join(' '); +} + +export default function AdminNavbar({ children }: { children: ReactNode }) { + const [sidebarOpen, setSidebarOpen] = useState(false); + const pathname = usePathname(); + + return ( + <> +
+ + + +
+ + +
+ +
+
+ + {/* Sidebar component */} +
+
+ Your Company +
+ +
+
+
+
+ + {/* Static sidebar for desktop */} +
+ {/* Sidebar component */} +
+
+ Your Company +
+ +
+
+ +
+
+ + + {/* Separator */} + +
+ + ); +} \ No newline at end of file diff --git a/src/app/admin/category-mapping/page.tsx b/src/app/admin/category-mapping/page.tsx new file mode 100644 index 0000000..f15fce2 --- /dev/null +++ b/src/app/admin/category-mapping/page.tsx @@ -0,0 +1,166 @@ +"use client"; +import { useEffect, useState, ChangeEvent, FormEvent } from "react"; + +type Mapping = { + id: number; + feedname: string; + affiliatecategory: string; + buildercategoryid: number; + notes?: string; +}; + +type MappingForm = { + feedname: string; + affiliatecategory: string; + buildercategoryid: string; + notes?: string; +}; + +export default function CategoryMappingAdmin() { + const [mappings, setMappings] = useState([]); + const [form, setForm] = useState({ feedname: "", affiliatecategory: "", buildercategoryid: "", notes: "" }); + const [editingId, setEditingId] = useState(null); + const [editForm, setEditForm] = useState({ feedname: "", affiliatecategory: "", buildercategoryid: "", notes: "" }); + const [productCategories, setProductCategories] = useState([]); + + // Fetch all mappings + const fetchMappings = async () => { + const res = await fetch("/api/category-mapping"); + const data = await res.json(); + setMappings(data.data || []); + }; + + const fetchProductCategories = async () => { + const res = await fetch("/api/product-categories"); + const data = await res.json(); + setProductCategories(data.data || []); + }; + + useEffect(() => { + fetchMappings(); + fetchProductCategories(); + }, []); + + // Add new mapping + const handleAdd = async (e: FormEvent) => { + e.preventDefault(); + await fetch("/api/category-mapping", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ...form, + buildercategoryid: parseInt(form.buildercategoryid, 10) + }), + }); + setForm({ feedname: "", affiliatecategory: "", buildercategoryid: "", notes: "" }); + fetchMappings(); + }; + + // Edit mapping + const handleEdit = (mapping: Mapping) => { + setEditingId(mapping.id); + setEditForm({ ...mapping, buildercategoryid: mapping.buildercategoryid.toString() }); + }; + + const handleUpdate = async (e: FormEvent) => { + e.preventDefault(); + await fetch("/api/category-mapping", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...editForm, buildercategoryid: parseInt(editForm.buildercategoryid, 10), id: editingId }), + }); + setEditingId(null); + fetchMappings(); + }; + + // Delete mapping + const handleDelete = async (id: number) => { + await fetch("/api/category-mapping", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + fetchMappings(); + }; + + return ( +
+

Affiliate Category Mapping Admin

+
+

AR15 Category Hierarchy

+ + + + + + + + + + + {productCategories + .filter(cat => cat.type === 'AR15') + .map(cat => { + const parent = productCategories.find(p => p.id === cat.parent_category_id); + return ( + + + + + + + ); + })} + +
IDCategoryParentType
{cat.id}{cat.name}{parent ? parent.name : 'Top Level'}{cat.type}
+
+
+ setForm(f => ({ ...f, feedname: e.target.value }))} required /> + setForm(f => ({ ...f, affiliatecategory: e.target.value }))} required /> + setForm(f => ({ ...f, buildercategoryid: e.target.value }))} required /> + setForm(f => ({ ...f, notes: e.target.value }))} /> + +
+ + + + + + + + + + + + {mappings.map((m) => ( + + {editingId === m.id ? ( + <> + + + + + + + ) : ( + <> + + + + + + + )} + + ))} + +
Feed NameAffiliate CategoryBuilder Category IDNotesActions
setEditForm(f => ({ ...f, feedname: e.target.value }))} /> setEditForm(f => ({ ...f, affiliatecategory: e.target.value }))} /> setEditForm(f => ({ ...f, buildercategoryid: e.target.value }))} /> setEditForm(f => ({ ...f, notes: e.target.value }))} /> + + + {m.feedname}{m.affiliatecategory}{m.buildercategoryid}{m.notes} + + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..155d88b --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,9 @@ +import AdminNavbar from './AdminNavbar'; + +export default async function AdminLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..13d98bf --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,278 @@ +"use client"; + +import { useState } from 'react'; +import { + Users, + ShoppingCart, + TrendingUp, + AlertTriangle, + CheckCircle, + Clock, + BarChart3, + Activity, + Calendar, + Settings +} from 'lucide-react'; + +export default function AdminDashboard() { + const [selectedPeriod, setSelectedPeriod] = useState('7d'); + + // Sample data - in a real app, this would come from your database + const stats = { + totalUsers: 1247, + activeUsers: 892, + totalProducts: 15420, + totalBuilds: 3421, + revenue: 45678, + growth: 12.5, + pendingApprovals: 23, + systemAlerts: 2 + }; + + const recentActivity = [ + { id: 1, user: 'John Doe', action: 'Created new build', time: '2 minutes ago', type: 'build' }, + { id: 2, user: 'Jane Smith', action: 'Updated profile', time: '5 minutes ago', type: 'profile' }, + { id: 3, user: 'Mike Johnson', action: 'Added product to cart', time: '8 minutes ago', type: 'cart' }, + { id: 4, user: 'Sarah Wilson', action: 'Completed purchase', time: '12 minutes ago', type: 'purchase' }, + { id: 5, user: 'Admin User', action: 'Updated category mapping', time: '15 minutes ago', type: 'admin' } + ]; + + const topProducts = [ + { name: 'AR-15 Lower Receiver', sales: 156, revenue: 12480 }, + { name: 'Tactical Scope', sales: 89, revenue: 17800 }, + { name: 'Gun Case', sales: 234, revenue: 11700 }, + { name: 'Ammo Storage', sales: 67, revenue: 3350 }, + { name: 'Cleaning Kit', sales: 189, revenue: 5670 } + ]; + + const systemStatus = [ + { service: 'Database', status: 'healthy', uptime: '99.9%' }, + { service: 'API', status: 'healthy', uptime: '99.8%' }, + { service: 'File Storage', status: 'warning', uptime: '98.5%' }, + { service: 'Email Service', status: 'healthy', uptime: '99.7%' } + ]; + + return ( +
+ {/* Header */} +
+
+

Admin Dashboard

+

Welcome back! Here's what's happening with your platform.

+
+
+ + +
+
+ + {/* Stats Cards */} +
+
+
+
+ +
+
+

Total Users

+

{stats.totalUsers.toLocaleString()}

+
+
+
+ + +{stats.growth}% + from last month +
+
+ +
+
+
+ +
+
+

Total Products

+

{stats.totalProducts.toLocaleString()}

+
+
+
+ + Active + in catalog +
+
+ +
+
+
+ +
+
+

Total Builds

+

{stats.totalBuilds.toLocaleString()}

+
+
+
+ + {stats.activeUsers} + active users +
+
+ +
+
+
+ +
+
+

Revenue

+

${stats.revenue.toLocaleString()}

+
+
+
+ + +8.2% + from last month +
+
+
+ + {/* Alerts and Notifications */} +
+
+
+
+

Recent Activity

+
+
+
+ {recentActivity.map((activity) => ( +
+
+
+

{activity.user}

+

{activity.action}

+
+
{activity.time}
+
+ ))} +
+
+
+
+ +
+ {/* Pending Actions */} +
+
+

Pending Actions

+
+
+
+
+
+ + Approvals Needed +
+ + {stats.pendingApprovals} + +
+
+
+ + System Alerts +
+ + {stats.systemAlerts} + +
+
+
+
+ + {/* System Status */} +
+
+

System Status

+
+
+
+ {systemStatus.map((service, index) => ( +
+ {service.service} +
+
+ {service.uptime} +
+
+ ))} +
+
+
+
+
+ + {/* Top Products */} +
+
+

Top Products

+
+
+
+ + + + + + + + + + {topProducts.map((product, index) => ( + + + + + + ))} + +
+ Product + + Sales + + Revenue +
+ {product.name} + + {product.sales} + + ${product.revenue.toLocaleString()} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..4ccae74 --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +import { db } from '@/db'; +import { users } from '@/db/schema'; +import { format } from 'date-fns'; + +async function getUsers() { + try { + const allUsers = await db.query.users.findMany({ + columns: { + id: true, + email: true, + name: true, + first_name: true, + last_name: true, + isAdmin: true, + emailVerified: true, + createdAt: true, + lastLogin: true, + buildPrivacySetting: true + }, + orderBy: (users, { desc }) => [desc(users.createdAt)] + }); + return allUsers; + } catch (error) { + console.error('Error fetching users:', error); + return []; + } +} + +export default async function AdminUsersPage() { + const usersList = await getUsers(); + + const adminCount = usersList.filter(user => user.isAdmin).length; + const verifiedCount = usersList.filter(user => user.emailVerified).length; + + return ( +
+
+

Users

+
+
+ Total: {usersList.length} | Admins: {adminCount} | Verified: {verifiedCount} +
+
+
+ + {/* Stats Cards */} +
+
+
{usersList.length}
+
Total Users
+
+
+
{adminCount}
+
Admins
+
+
+
{verifiedCount}
+
Verified
+
+
+
{usersList.length - verifiedCount}
+
Unverified
+
+
+ +
+
+ + + + + + + + + + + + + + + {usersList.map((user) => ( + + + + + + + + + + + ))} + +
+ User + + Email + + Status + + Role + + Joined + + Last Login + + Privacy + + Actions +
+
+
+
+ + {user.first_name?.[0] || user.last_name?.[0] || user.email[0].toUpperCase()} + +
+
+
+
+ {user.first_name && user.last_name + ? `${user.first_name} ${user.last_name}` + : user.name || 'No name' + } +
+
+ ID: {user.id} +
+
+
+
+
{user.email}
+
+ + {user.emailVerified ? 'Verified' : 'Unverified'} + + + + {user.isAdmin ? 'Admin' : 'User'} + + + {user.createdAt ? format(new Date(user.createdAt), 'MMM d, yyyy') : 'N/A'} + + {user.lastLogin ? format(new Date(user.lastLogin), 'MMM d, yyyy') : 'Never'} + + + {user.buildPrivacySetting || 'public'} + + +
+ + +
+
+
+ + {usersList.length === 0 && ( +
+
No users found
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 724b52c..96e44ac 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,16 +1,14 @@ import NextAuth from 'next-auth'; import GoogleProvider from 'next-auth/providers/google'; import CredentialsProvider from 'next-auth/providers/credentials'; - -// In-memory user store (for demo only) -type User = { email: string; password: string }; -declare global { - // eslint-disable-next-line no-var - var _users: User[] | undefined; -} -const users: User[] = global._users || (global._users = []); +import { DrizzleAdapter } from '@auth/drizzle-adapter'; +import { db } from '@/db'; +import { users } from '@/db/schema'; +import bcrypt from 'bcryptjs'; const handler = NextAuth({ + adapter: DrizzleAdapter(db), + session: { strategy: 'jwt' }, providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID ?? '', @@ -23,25 +21,16 @@ const handler = NextAuth({ password: { label: "Password", type: "password" } }, async authorize(credentials) { + console.log('Credentials received:', credentials); if (!credentials?.email || !credentials?.password) return null; - // Check in-memory user store - const user = users.find( - (u) => u.email === credentials.email && u.password === credentials.password - ); - if (user) { - return { - id: user.email, - email: user.email, - name: user.email.split('@')[0], - }; - } - // For demo, still allow the test user - if (credentials.email === "test@example.com" && credentials.password === "password") { - return { - id: "1", - email: credentials.email, - name: "Test User", - }; + // Query the real users table using Drizzle + const foundUser = await db.query.users.findFirst({ + where: (u, { eq }) => eq(u.email, credentials.email), + }); + console.log('User found:', foundUser); + if (foundUser && foundUser.hashedPassword && await bcrypt.compare(credentials.password, foundUser.hashedPassword)) { + console.log('Returning user:', foundUser); + return foundUser; } return null; } @@ -53,17 +42,25 @@ const handler = NextAuth({ // error: '/account/error', // Uncomment when error page is ready }, callbacks: { - async session({ session, token }) { - // Add any additional user data to the session here + async session({ session, user, token }) { + console.log('Session callback - user:', user); + console.log('Session callback - token:', token); + if (session.user) { + (session.user as any).isAdmin = (user as any)?.isAdmin ?? token?.isAdmin ?? false; + console.log('Session callback - final isAdmin:', (session.user as any).isAdmin); + } return session; }, async jwt({ token, user }) { - // Add any additional user data to the JWT here - if (user) { - token.id = user.id; + console.log('JWT callback - user:', user); + console.log('JWT callback - token before:', token); + if (user && typeof user === 'object' && 'isAdmin' in user) { + (token as any).isAdmin = (user as any).isAdmin; + console.log('JWT callback - setting isAdmin to:', (user as any).isAdmin); } + console.log('JWT callback - token after:', token); return token; - }, + } }, }) diff --git a/src/app/api/auth/check-admin/route.ts b/src/app/api/auth/check-admin/route.ts new file mode 100644 index 0000000..a98aac1 --- /dev/null +++ b/src/app/api/auth/check-admin/route.ts @@ -0,0 +1,17 @@ +import { getToken } from 'next-auth/jwt'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(request: NextRequest) { + const token = await getToken({ + req: request, + secret: process.env.NEXTAUTH_SECRET + }); + + console.log('Check Admin API - Token:', token); + console.log('Check Admin API - isAdmin:', token?.isAdmin); + + return NextResponse.json({ + isAdmin: token?.isAdmin || false, + token: token + }); +} \ No newline at end of file diff --git a/src/app/api/categories/route.ts b/src/app/api/categories/route.ts index 746498b..e67472d 100644 --- a/src/app/api/categories/route.ts +++ b/src/app/api/categories/route.ts @@ -1,7 +1,8 @@ -import { db } from "@/db"; -import { componentType } from "@/db/schema"; +import { NextResponse } from 'next/server'; +import { db } from '@/db'; +import { categories } from '@/db/schema'; export async function GET() { - const allComponentTypes = await db.select().from(componentType); - return Response.json({ success: true, data: allComponentTypes }); + const allCategories = await db.select().from(categories); + return NextResponse.json({ success: true, data: allCategories }); } \ No newline at end of file diff --git a/src/app/api/category-mapping/route.ts b/src/app/api/category-mapping/route.ts new file mode 100644 index 0000000..8508595 --- /dev/null +++ b/src/app/api/category-mapping/route.ts @@ -0,0 +1,66 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/db'; +import { affiliateCategoryMap } from '@/db/schema'; +import { eq } from 'drizzle-orm'; + +// GET: List all mappings +export async function GET() { + try { + const mappings = await db.select().from(affiliateCategoryMap); + return NextResponse.json({ success: true, data: mappings }); + } catch (error: any) { + console.error('GET /api/category-mapping error:', error); + return NextResponse.json({ success: false, error: error.message || 'Unknown error', data: [] }, { status: 500 }); + } +} + +// POST: Create a new mapping +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { feedname, affiliatecategory, buildercategoryid, notes } = body; + if (!feedname || !affiliatecategory || !buildercategoryid) { + return NextResponse.json({ success: false, error: 'Missing required fields' }, { status: 400 }); + } + const [inserted] = await db.insert(affiliateCategoryMap).values({ feedname, affiliatecategory, buildercategoryid, notes }).returning(); + return NextResponse.json({ success: true, data: inserted }); + } catch (error: any) { + console.error('POST /api/category-mapping error:', error); + return NextResponse.json({ success: false, error: error.message || 'Unknown error' }, { status: 500 }); + } +} + +// PUT: Update a mapping +export async function PUT(req: NextRequest) { + try { + const body = await req.json(); + const { id, feedname, affiliatecategory, buildercategoryid, notes } = body; + if (!id) { + return NextResponse.json({ success: false, error: 'Missing id' }, { status: 400 }); + } + const [updated] = await db.update(affiliateCategoryMap) + .set({ feedname, affiliatecategory, buildercategoryid, notes }) + .where(eq(affiliateCategoryMap.id, id)) + .returning(); + return NextResponse.json({ success: true, data: updated }); + } catch (error: any) { + console.error('PUT /api/category-mapping error:', error); + return NextResponse.json({ success: false, error: error.message || 'Unknown error' }, { status: 500 }); + } +} + +// DELETE: Remove a mapping +export async function DELETE(req: NextRequest) { + try { + const body = await req.json(); + const { id } = body; + if (!id) { + return NextResponse.json({ success: false, error: 'Missing id' }, { status: 400 }); + } + await db.delete(affiliateCategoryMap).where(eq(affiliateCategoryMap.id, id)); + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error('DELETE /api/category-mapping error:', error); + return NextResponse.json({ success: false, error: error.message || 'Unknown error' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/product-categories/route.ts b/src/app/api/product-categories/route.ts new file mode 100644 index 0000000..0c9dabf --- /dev/null +++ b/src/app/api/product-categories/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/db'; +import { product_categories } from '@/db/schema'; + +export async function GET() { + const allCategories = await db.select().from(product_categories); + return NextResponse.json({ success: true, data: allCategories }); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2ceb6e1..7365e29 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,6 @@ import "./globals.css"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; -import Providers from "@/components/Providers"; const inter = Inter({ subsets: ["latin"] }); @@ -18,9 +17,7 @@ export default function RootLayout({ return ( - - {children} - + {children} ); diff --git a/src/db/schema.ts b/src/db/schema.ts index f6c0288..54bce38 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -424,7 +424,7 @@ export const accounts = pgTable("accounts", { /* From here down is the authentication library Lusia tables */ -export const users = pgTable("users", +export const users = pgTable("user", { id: varchar("id", { length: 21 }).primaryKey(), name: varchar("name"), @@ -462,21 +462,16 @@ export const users = pgTable("users", export type User = typeof users.$inferSelect; export type NewUser = typeof users.$inferInsert; -export const sessions = pgTable( - "sessions", - { - id: varchar("id", { length: 255 }).primaryKey(), - userId: varchar("user_id", { length: 21 }).notNull(), - expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(), - createdAt: timestamp("created_at", { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`), - updatedAt: timestamp("updated_at", { mode: 'string' }).default(sql`CURRENT_TIMESTAMP`), - }, - (t) => ({ - userIdx: index("session_user_idx").on(t.userId), - }), - ); - - export const emailVerificationCodes = pgTable( +export const session = pgTable( + "session", + { + sessionToken: varchar("sessionToken", { length: 255 }).primaryKey(), + userId: varchar("userId", { length: 21 }).notNull(), + expires: timestamp("expires", { withTimezone: true, mode: "date" }).notNull(), + } +); + +export const emailVerificationCodes = pgTable( "email_verification_codes", { id: serial("id").primaryKey(), @@ -555,4 +550,22 @@ export const sessions = pgTable( // price: integer("price"), // createdAt: timestamp("created_at").defaultNow(), // // Add more fields as needed -// }); \ No newline at end of file +// }); + +export const affiliateCategoryMap = pgTable("affiliate_category_map", { + id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "affiliate_category_map_id_seq", startWith: 1, increment: 1 }), + feedname: varchar("feedname", { length: 100 }).notNull(), + affiliatecategory: varchar("affiliatecategory", { length: 255 }).notNull(), + buildercategoryid: integer("buildercategoryid").notNull(), + notes: varchar("notes", { length: 255 }), +}); + +export const product_categories = pgTable("product_categories", { + id: integer().primaryKey().generatedAlwaysAsIdentity({ name: "product_categories_id_seq", startWith: 1, increment: 1 }), + name: varchar({ length: 100 }).notNull(), + parent_category_id: integer("parent_category_id"), + type: varchar({ length: 50 }), + sort_order: integer("sort_order"), + created_at: timestamp("created_at", { mode: 'string' }).defaultNow(), + updated_at: timestamp("updated_at", { mode: 'string' }).defaultNow(), +}); \ No newline at end of file