From 1c865ef48fe0d690cc1335872a4ecf9eafe66665 Mon Sep 17 00:00:00 2001 From: Huzaifa Inam Date: Fri, 7 Aug 2026 13:00:15 +0500 Subject: [PATCH] v0.1 --- server.ts | 28 +- src/App.tsx | 3 +- src/components/LoginModal.tsx | 68 +- src/components/Sidebar.tsx | 361 ++++++---- src/components/TimelineView.tsx | 1094 +++++++++++++++++++++---------- src/types.ts | 9 +- src/utils/timelineGenerator.ts | 291 ++++++++ 7 files changed, 1380 insertions(+), 474 deletions(-) create mode 100644 src/utils/timelineGenerator.ts diff --git a/server.ts b/server.ts index 53e89fa..a89ac83 100644 --- a/server.ts +++ b/server.ts @@ -143,26 +143,33 @@ app.post("/api/auth/login", async (req, res) => { const { username, password } = req.body; if (!username || !password) { - return res.status(400).json({ error: "Username and password are required." }); + return res.status(400).json({ error: "Username/email and password are required." }); } let users = memoryStore?.users || []; + const term = String(username).trim().toLowerCase(); - // Search user in memory store or default - let user = users.find((u: any) => u.username.toLowerCase() === username.toLowerCase()); + // Search user in memory store or default by username OR email + let user = users.find((u: any) => + (u.username && u.username.toLowerCase() === term) || + (u.email && u.email.toLowerCase() === term) + ); - if (!user && !memoryStore) { + if (!user && (!memoryStore || !memoryStore.users || memoryStore.users.length === 0)) { // Check initial defaults const defaults = [ { id: 1, username: 'admin', password: 'Admin', role: 'admin', fullName: 'System Administrator', email: 'admin@pinnacle.local', dept: 'Executive Management', active: true }, { id: 2, username: 'editor', password: 'Editor', role: 'editor', fullName: 'Project Manager', email: 'pm@pinnacle.local', dept: 'Project Management Office', active: true }, { id: 3, username: 'viewer', password: 'Viewer', role: 'viewer', fullName: 'Board Observer', email: 'observer@pinnacle.local', dept: 'Board of Directors', active: true } ]; - user = defaults.find(u => u.username.toLowerCase() === username.toLowerCase()); + user = defaults.find(u => + (u.username && u.username.toLowerCase() === term) || + (u.email && u.email.toLowerCase() === term) + ); } if (!user) { - return res.status(401).json({ error: "Invalid username or password." }); + return res.status(401).json({ error: "Invalid username/email or password." }); } if (user.active === false) { @@ -172,7 +179,7 @@ app.post("/api/auth/login", async (req, res) => { const isValid = verifyPassword(password, user.password); if (!isValid) { - return res.status(401).json({ error: "Invalid username or password." }); + return res.status(401).json({ error: "Invalid username/email or password." }); } // Upgrade password to hash if stored plain @@ -191,6 +198,7 @@ app.post("/api/auth/login", async (req, res) => { res.json({ success: true, token, + mustChangePassword: !!user.mustChangePassword, user: userWithoutPassword }); }); @@ -303,7 +311,11 @@ app.post("/api/auth/reset-password", (req: any, res: any) => { } let users = memoryStore?.users || []; - let user = users.find((u: any) => u.username.toLowerCase() === String(username).toLowerCase()); + const term = String(username || '').trim().toLowerCase(); + let user = users.find((u: any) => + (u.username && u.username.toLowerCase() === term) || + (u.email && u.email.toLowerCase() === term) + ); if (user) { user.password = hashPassword(newPassword); diff --git a/src/App.tsx b/src/App.tsx index 52c3899..564f212 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -251,7 +251,7 @@ export default function App() { onCloseMobile={() => setIsMobileMenuOpen(false)} /> -
+
{activeTab === 'dashboard' && ( setActiveTab(tab)} /> )} diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx index 2af76b1..e8c86ee 100644 --- a/src/components/LoginModal.tsx +++ b/src/components/LoginModal.tsx @@ -51,7 +51,7 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o clearMessages(); if (!username.trim() || !password.trim()) { - setErrorMsg('Please enter both username and password.'); + setErrorMsg('Please enter your username or email address and password.'); return; } @@ -108,7 +108,7 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o } else if (found && found.active === false) { setErrorMsg('Your account has been deactivated by an administrator.'); } else { - setErrorMsg(err.message || 'Invalid username or password.'); + setErrorMsg(err.message || 'Invalid username/email or password.'); } } finally { setIsLoading(false); @@ -172,6 +172,14 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o }, 1200); }; + // Mask email utility for production privacy + const maskEmail = (emailStr: string): string => { + if (!emailStr || !emailStr.includes('@')) return emailStr || 'your registered address'; + const [name, domain] = emailStr.split('@'); + const maskedName = name.length > 2 ? `${name[0]}***${name[name.length - 1]}` : `${name[0]}***`; + return `${maskedName}@${domain}`; + }; + // Step 1: Request Forgot Password Verification Code const handleRequestForgotCode = async (e: React.FormEvent) => { e.preventDefault(); @@ -193,12 +201,14 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o return; } - // Generate 6-digit code + // Generate 6-digit verification code const code = Math.floor(100000 + Math.random() * 900000).toString(); setGeneratedCode(code); setTargetUser(matched); - // Try sending notification via email endpoint + setIsLoading(true); + + // Dispatch notification via backend email service try { await fetch('/api/auth/forgot-password', { method: 'POST', @@ -207,10 +217,38 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o }); } catch (e) { console.warn('Forgot password email notification fallback'); + } finally { + setIsLoading(false); } + const masked = maskEmail(matched.email); + console.log(`[SECURE AUTH ENGINE] Verification PIN for ${matched.email}: ${code}`); + setResetStep(2); - setSuccessMsg(`Reset code generated! For testing, your 6-digit verification code is: ${code}`); + setSuccessMsg(`A 6-digit security verification PIN has been dispatched to ${masked}. Please check your email inbox and enter the code below to reset your password.`); + }; + + // Step 1.5: Resend verification code + const handleResendCode = async () => { + clearMessages(); + if (!targetUser) return; + + const code = Math.floor(100000 + Math.random() * 900000).toString(); + setGeneratedCode(code); + + try { + await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier: targetUser.email, resetCode: code }) + }); + } catch (e) { + console.warn('Resend PIN fallback'); + } + + const masked = maskEmail(targetUser.email); + console.log(`[SECURE AUTH ENGINE] Resent Verification PIN for ${targetUser.email}: ${code}`); + setSuccessMsg(`A new 6-digit verification PIN has been re-dispatched to ${masked}.`); }; // Step 2: Confirm Code and Reset Password @@ -219,7 +257,7 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o clearMessages(); if (inputCode.trim() !== generatedCode) { - setErrorMsg('Invalid verification code. Please check the 6-digit code provided.'); + setErrorMsg('Invalid verification code. Please check the 6-digit PIN sent to your email.'); return; } @@ -304,13 +342,13 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o
setUsername(e.target.value)} - placeholder="Enter username (e.g. admin, pm, observer)" + placeholder="Enter username or email address (e.g. admin or admin@pinnacle.local)" required autoFocus className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-slate-200 focus:outline-none focus:border-amber-500 transition" @@ -488,13 +526,23 @@ export const LoginModal: React.FC = ({ users, onLoginSuccess, o
- +
+ + +
setInputCode(e.target.value)} - placeholder="Enter code (e.g. 123456)" + placeholder="Enter 6-digit PIN" required className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-amber-300 font-mono text-center tracking-widest text-base font-bold focus:outline-none focus:border-amber-500" /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 3e42dcc..545fe14 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { PieChart, Layers, @@ -12,9 +12,12 @@ import { FileText, Mail, Server, - Terminal, Database, - X + X, + ChevronLeft, + ChevronRight, + PanelLeftClose, + PanelLeft } from 'lucide-react'; import { UserRole } from '../types'; @@ -26,6 +29,7 @@ interface SidebarProps { openRisksCount: number; isOpenMobile?: boolean; onCloseMobile?: () => void; + onCollapseChange?: (collapsed: boolean) => void; } export const Sidebar: React.FC = ({ @@ -35,8 +39,35 @@ export const Sidebar: React.FC = ({ pendingDecisionsCount, openRisksCount, isOpenMobile = false, - onCloseMobile + onCloseMobile, + onCollapseChange }) => { + // Collapsed state for desktop view (persist in localStorage) + const [isCollapsed, setIsCollapsed] = useState(() => { + return localStorage.getItem('pinnacle_sidebar_collapsed') === 'true'; + }); + + const toggleCollapse = () => { + setIsCollapsed((prev) => { + const next = !prev; + localStorage.setItem('pinnacle_sidebar_collapsed', String(next)); + if (onCollapseChange) onCollapseChange(next); + return next; + }); + }; + + // Keyboard shortcut Ctrl+B or Cmd+B to toggle sidebar collapse on desktop + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'b') { + e.preventDefault(); + toggleCollapse(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + const navItems = [ { id: 'dashboard', label: 'Project Dashboard', icon: PieChart }, { id: 'phases', label: 'Phases & Milestones', icon: Layers }, @@ -77,140 +108,227 @@ export const Sidebar: React.FC = ({ } }; - const sidebarContent = ( -
- {/* Mobile Sidebar Close Button */} - {isOpenMobile && ( -
- Navigation Menu - +
+ )} + + {/* Desktop Collapse / Expand Control Header */} + {!mobileView && ( +
- - -
- )} - -
- {/* Navigation Group */} -
-
- Navigation + {!collapsed && ( + + Portal Menu + + )} + +
+ )} + + {/* Scrollable Navigation Items */} +
+ {/* Main Navigation Group */} +
+ {!collapsed ? ( +
+ Navigation +
+ ) : ( +
+ )} + +
+ + {/* Administration Group */} +
+ {!collapsed ? ( +
+ Administration +
+ ) : ( +
+ )} + +
+ + {/* Deployment & DevOps Group */} +
+ {!collapsed ? ( +
+ Cloud & Deployment +
+ ) : ( +
+ )} +
-
- {/* Administration Group */} -
-
- Administration -
- -
- - {/* Deployment & DevOps Group */} -
-
- Cloud & Deployment -
- + {/* Footer Info / Toggle Shortcut Note */} +
+ {!collapsed ? ( +
+
+
+ + PostgreSQL + Coolify +
+ + ⌘B + +
+

Automated SSL via Traefik reverse proxy.

+
+ ) : ( + + )}
- - {/* Footer Info Box */} -
-
-
- - PostgreSQL + Coolify -
-

Automated SSL certificates via Traefik reverse proxy.

-
-
-
- ); + ); + }; return ( <> - {/* Desktop Sidebar */} -