From 0c65542fb964cdcaddf0011c003277b60ae4e6af Mon Sep 17 00:00:00 2001 From: Sidharth Prabhu Date: Tue, 21 Apr 2026 12:59:07 +0530 Subject: [PATCH] Multiple user support added --- .../controller/SystemAuthController.java | 27 ++ .../sales/service/SystemUserService.java | 92 ++-- frontend/src/App.tsx | 4 + frontend/src/components/Header.tsx | 5 +- frontend/src/components/Sidebar.tsx | 1 + frontend/src/pages/Dashboard.tsx | 15 +- frontend/src/pages/Login.tsx | 5 + frontend/src/pages/Settings.tsx | 399 ++++++++++++++++++ frontend/src/pages/StoreDashboard.tsx | 37 +- 9 files changed, 518 insertions(+), 67 deletions(-) create mode 100644 frontend/src/pages/Settings.tsx diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/SystemAuthController.java b/backend/src/main/java/com/rit/canteen/sales/controller/SystemAuthController.java index e50942ae..7635b635 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/SystemAuthController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/SystemAuthController.java @@ -62,4 +62,31 @@ public class SystemAuthController { userService.deleteManager(id); // Using existing delete logic return ResponseEntity.noContent().build(); } + + @GetMapping("/admins") + public ResponseEntity> getAdmins() { + return ResponseEntity.ok(userService.getMasters()); + } + + @PostMapping("/admins") + public ResponseEntity addAdmin(@RequestBody SystemUser admin) { + return ResponseEntity.ok(userService.createMaster(admin)); + } + + @PostMapping("/update-master") + public ResponseEntity updateMaster(@RequestBody Map data) { + try { + Object idObj = data.get("id"); + Long id = (idObj != null) ? Long.valueOf(idObj.toString()) : 0L; + + String email = (String) data.get("email"); + String password = (String) data.get("password"); + String name = (String) data.get("name"); + + userService.updateMasterAccount(id, email, password, name); + return ResponseEntity.ok(Map.of("success", true, "message", "Credentials updated successfully")); + } catch (Exception e) { + return ResponseEntity.status(500).body(Map.of("success", false, "message", e.getMessage())); + } + } } diff --git a/backend/src/main/java/com/rit/canteen/sales/service/SystemUserService.java b/backend/src/main/java/com/rit/canteen/sales/service/SystemUserService.java index 84fcf996..90e38363 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/SystemUserService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/SystemUserService.java @@ -28,29 +28,10 @@ public class SystemUserService { @PostConstruct public void init() { - // Handle migration from old credentials if they exist - repository.findByEmail("admin@ritcanteen.com").ifPresent(oldUser -> { - boolean hasNewAdmin = repository.findByEmail("admin").isPresent(); - if (hasNewAdmin) { - repository.delete(oldUser); - System.out.println(">>> REMOVED OBSOLETE MASTER USER: admin@ritcanteen.com"); - } else { - oldUser.setEmail(masterUsername); - oldUser.setPassword(passwordEncoder.encode(masterPassword)); - repository.save(oldUser); - System.out.println(">>> MIGRATED MASTER USER: admin / admin"); - } - }); - - // Ensure 'admin' user exists and has the correct password - Optional adminUser = repository.findByEmail(masterUsername); - if (adminUser.isPresent()) { - SystemUser admin = adminUser.get(); - admin.setPassword(passwordEncoder.encode(masterPassword)); - admin.setRole("MASTER"); - repository.save(admin); - System.out.println(">>> UPDATED MASTER USER PASSWORD: " + masterUsername + " / " + masterPassword); - } else { + // Ensure at least one 'MASTER' user exists in the database + List masters = repository.findByRole("MASTER"); + + if (masters.isEmpty()) { SystemUser master = new SystemUser(); master.setName("Admin Master"); master.setEmail(masterUsername); @@ -59,7 +40,9 @@ public class SystemUserService { master.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback")); master.setViewOnly(false); repository.save(master); - System.out.println(">>> SEEDED MASTER USER: " + masterUsername + " / " + masterPassword); + System.out.println(">>> SEEDED DEFAULT MASTER USER (Failsafe Source): " + masterUsername + " / " + masterPassword); + } else { + System.out.println(">>> MASTER USER(S) FOUND IN DATABASE. Skipping default seeding."); } } @@ -83,24 +66,81 @@ public class SystemUserService { return repository.save(staff); } + public List getMasters() { + return repository.findByRole("MASTER"); + } + + public SystemUser createMaster(SystemUser admin) { + admin.setRole("MASTER"); + admin.setPassword(passwordEncoder.encode(admin.getPassword())); + // Grant full permissions by default for new admins + admin.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback")); + return repository.save(admin); + } + public void deleteManager(Long id) { if (id != null) { repository.deleteById(id); } } + public void updateMasterAccount(Long id, String email, String password, String name) { + SystemUser user; + if (id == null || id == 0) { + // If failsafe user (ID 0) or null, try to find the first master in the database + user = repository.findByRole("MASTER").stream().findFirst() + .orElseGet(() -> { + SystemUser newMaster = new SystemUser(); + newMaster.setRole("MASTER"); + newMaster.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback")); + return newMaster; + }); + } else { + user = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Master account not found with ID: " + id)); + } + + if (email != null) user.setEmail(email); + if (password != null && !password.isEmpty()) { + user.setPassword(passwordEncoder.encode(password)); + } + if (name != null) user.setName(name); + repository.save(user); + } + public Optional authenticate(String email, String password) { System.out.println(">>> Attempting authentication for: " + email); + + // 1. Try Database First Optional user = repository.findByEmail(email); if (user.isPresent()) { boolean matches = passwordEncoder.matches(password, user.get().getPassword()); - System.out.println(">>> User found. Password match: " + matches); + System.out.println(">>> User found in DB. Password match: " + matches); if (matches) { return user; } } else { - System.out.println(">>> User NOT found: " + email); + System.out.println(">>> User NOT found in DB. Checking Failsafe eligibility..."); + + // 2. Try Failsafe (Properties) - ONLY if no Master users exist in DB + List masters = repository.findByRole("MASTER"); + if (masters.isEmpty()) { + if (email.equals(masterUsername) && password.equals(masterPassword)) { + System.out.println(">>> FAILSAFE AUTHENTICATION SUCCESSFUL (No DB Master Found)"); + + SystemUser failsafeUser = new SystemUser(); + failsafeUser.setId(0L); + failsafeUser.setName("Failsafe Admin"); + failsafeUser.setEmail(masterUsername); + failsafeUser.setRole("MASTER"); + failsafeUser.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback")); + return Optional.of(failsafeUser); + } + } else { + System.out.println(">>> Failsafe disabled because custom master account exists in database."); + } } + return Optional.empty(); } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 523d519c..8127cf61 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,6 +27,7 @@ import Ritz from './pages/Ritz.tsx'; import RitzCirculation from './pages/RitzCirculation.tsx'; import ManageWallets from './pages/ManageWallets.tsx'; import ManageCoupons from './pages/ManageCoupons.tsx'; +import Settings from './pages/Settings.tsx'; const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { @@ -106,6 +107,9 @@ function App() { } /> } /> } /> + + {/* Settings */} + } /> diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 3a321a54..e0dd5394 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,4 +1,5 @@ import { Bell, Search, Settings, HelpCircle, Calendar } from 'lucide-react'; +import { Link } from 'react-router-dom'; const Header = () => { const currentDate = new Date().toLocaleDateString('en-US', { @@ -33,9 +34,9 @@ const Header = () => { - +

Dashboard

-

Good morning

+

+ Good morning, {(() => { + const saved = localStorage.getItem('systemUser'); + return saved ? JSON.parse(saved).name : 'Partner'; + })()} +

i The default time settings for the merchant view are from 12:00 AM to 11:59 PM. @@ -175,14 +180,6 @@ const Dashboard = () => { View Full Report -

- -
- -
-
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 2e12e5a4..57e06c15 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -27,6 +27,7 @@ const Login = () => { sessionStorage.removeItem('isLoggedIn'); sessionStorage.removeItem('userRole'); sessionStorage.removeItem('userPermissions'); + localStorage.removeItem('systemUser'); }, []); const handleLogin = async (e: React.FormEvent) => { @@ -51,6 +52,10 @@ const Login = () => { sessionStorage.setItem('isLoggedIn', 'true'); sessionStorage.setItem('userRole', user.role.toLowerCase()); sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || [])); + + // Persist user profile for personalized greetings and settings + localStorage.setItem('systemUser', JSON.stringify(user)); + navigate('/store-dashboard'); } else { const error = await response.text(); diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx new file mode 100644 index 00000000..52438371 --- /dev/null +++ b/frontend/src/pages/Settings.tsx @@ -0,0 +1,399 @@ +import React, { useState, useEffect } from 'react'; +import { + User, + Mail, + Lock, + ShieldCheck, + Save, + CheckCircle2, + AlertCircle, + Shield, + Key, + Users, + UserPlus, + ArrowRight, + ShieldAlert +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; + +const Settings = () => { + const [activeTab, setActiveTab] = useState<'profile' | 'team'>('profile'); + const [currentUser, setCurrentUser] = useState(() => { + const saved = localStorage.getItem('systemUser'); + return saved ? JSON.parse(saved) : null; + }); + + // Profile Form State + const [profileData, setProfileData] = useState({ + name: currentUser?.name || '', + email: currentUser?.email || '', + password: '', + confirmPassword: '' + }); + + // Team State + const [admins, setAdmins] = useState([]); + const [newAdmin, setNewAdmin] = useState({ name: '', email: '', password: '' }); + const [showAddModal, setShowAddModal] = useState(false); + + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const [message, setMessage] = useState(''); + + useEffect(() => { + if (activeTab === 'team') { + fetchAdmins(); + } + }, [activeTab]); + + const fetchAdmins = async () => { + try { + const response = await fetch('/api/system/admins'); + const data = await response.json(); + setAdmins(data); + } catch (err) { + console.error('Failed to fetch admins'); + } + }; + + const handleProfileChange = (e: React.ChangeEvent) => { + setProfileData(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const handleUpdateProfile = async (e: React.FormEvent) => { + e.preventDefault(); + if (profileData.password && profileData.password !== profileData.confirmPassword) { + setStatus('error'); + setMessage('Passwords do not match'); + return; + } + + setStatus('loading'); + try { + const response = await fetch('/api/system/update-master', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: currentUser?.id, + name: profileData.name, + email: profileData.email, + password: profileData.password || null + }) + }); + + const data = await response.json(); + if (data.success) { + setStatus('success'); + setMessage(data.message); + const updatedUser = { ...currentUser, name: profileData.name, email: profileData.email }; + localStorage.setItem('systemUser', JSON.stringify(updatedUser)); + setCurrentUser(updatedUser); + setTimeout(() => setStatus('idle'), 3000); + } else { + setStatus('error'); + setMessage(data.message || 'Update failed'); + } + } catch (err) { + setStatus('error'); + setMessage('Connection error'); + } + }; + + const handleAddAdmin = async (e: React.FormEvent) => { + e.preventDefault(); + setStatus('loading'); + try { + const response = await fetch('/api/system/admins', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newAdmin) + }); + + if (response.ok) { + setStatus('success'); + setMessage('New administrator added'); + setNewAdmin({ name: '', email: '', password: '' }); + setShowAddModal(false); + fetchAdmins(); + setTimeout(() => setStatus('idle'), 3000); + } else { + setStatus('error'); + setMessage('Failed to add administrator'); + } + } catch (err) { + setStatus('error'); + setMessage('Connection error'); + } + }; + + return ( +
+
+
+

Settings & Security

+

Manage your account and system access control

+
+ +
+ + +
+
+ + + {activeTab === 'profile' ? ( + + {/* Left Card: Summary */} +
+
+
+ +
+

{currentUser?.name}

+
+ + {currentUser?.role} +
+ +
+
+ + {currentUser?.email} +
+
+ + Full System Access +
+
+
+
+ + {/* Right Card: Form */} +
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ + {status === 'success' && ( + + {message} + + )} + {status === 'error' && ( + + {message} + + )} + +
+ + +
+ +
+
+ + ) : ( + +
+
+

Administrator Team

+

Found {admins.length} active system administrators

+
+ +
+ +
+ {admins.map((admin) => ( +
+
+
+ +
+ {admin.id === currentUser?.id && ( + You + )} +
+

{admin.name}

+
+
+ + {admin.email} +
+
+ + {admin.role} ACCESS +
+
+
+ ))} +
+
+ )} + + + {/* Add Admin Modal */} + + {showAddModal && ( +
+ setShowAddModal(false)} className="absolute inset-0 bg-[#231651]/20 backdrop-blur-sm" /> + +
+
+ +
+

New Administrator

+

Grant full system access to a team member

+
+
+
+ + setNewAdmin({...newAdmin, name: e.target.value})} + className="w-full bg-gray-50 border border-gray-100 rounded-xl py-3 px-4 outline-none focus:border-indigo-500 transition-all font-medium" + placeholder="e.g. John Doe" + /> +
+
+ + setNewAdmin({...newAdmin, email: e.target.value})} + className="w-full bg-gray-50 border border-gray-100 rounded-xl py-3 px-4 outline-none focus:border-indigo-500 transition-all font-medium" + placeholder="john@example.com" + /> +
+
+ + setNewAdmin({...newAdmin, password: e.target.value})} + className="w-full bg-gray-50 border border-gray-100 rounded-xl py-3 px-4 outline-none focus:border-indigo-500 transition-all font-medium" + placeholder="••••••••" + /> +
+ +
+ +

+ CRITICAL: This user will have full MASTER privileges. They can manage sales, tokens, and other administrators. +

+
+ +
+ + +
+
+
+
+ )} +
+
+ ); +}; + +export default Settings; diff --git a/frontend/src/pages/StoreDashboard.tsx b/frontend/src/pages/StoreDashboard.tsx index d8a00d35..536833ba 100644 --- a/frontend/src/pages/StoreDashboard.tsx +++ b/frontend/src/pages/StoreDashboard.tsx @@ -167,39 +167,16 @@ const StoreDashboard = () => {

Store Dashboard

-

Good morning, RIT Canteen

+

+ Good morning, {(() => { + const saved = localStorage.getItem('systemUser'); + return saved ? JSON.parse(saved).name : 'Partner'; + })()} +

-
-
- -
- -
-
-
- -
- -
-
-
-
- - -
-
- A -
+ {/* Action buttons or profile placeholder can go here if needed later */}