diff --git a/backend/src/main/java/com/rit/canteen/sales/config/WebConfig.java b/backend/src/main/java/com/rit/canteen/sales/config/WebConfig.java index 59de2d85..96a35ac3 100644 --- a/backend/src/main/java/com/rit/canteen/sales/config/WebConfig.java +++ b/backend/src/main/java/com/rit/canteen/sales/config/WebConfig.java @@ -14,7 +14,7 @@ public class WebConfig { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOriginPatterns("*") + .allowedOriginPatterns("http://*", "https://*", "file://*") .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java b/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java index f3142965..95d67500 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/UserController.java @@ -138,4 +138,18 @@ public class UserController { userService.deleteUser(id); return ResponseEntity.noContent().build(); } + + /** + * Toggle user suspension status. + * PATCH /api/auth/users/{id}/suspend + */ + @PatchMapping("/users/{id}/suspend") + public ResponseEntity toggleSuspension(@PathVariable Long id) { + LoginResponse.UserDto updated = userService.toggleSuspension(id); + if (updated != null) { + return ResponseEntity.ok(updated); + } else { + return ResponseEntity.notFound().build(); + } + } } diff --git a/backend/src/main/java/com/rit/canteen/sales/model/DashboardStats.java b/backend/src/main/java/com/rit/canteen/sales/model/DashboardStats.java index 8cc0f814..0c18e795 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/DashboardStats.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/DashboardStats.java @@ -8,10 +8,11 @@ public class DashboardStats { private double growth; private long totalExpenses; private long periodExpenses; + private long suspendedUserCount; public DashboardStats() {} - public DashboardStats(long totalSales, long periodRevenue, int activeOrders, int dailyCustomers, double growth, long totalExpenses, long periodExpenses) { + public DashboardStats(long totalSales, long periodRevenue, int activeOrders, int dailyCustomers, double growth, long totalExpenses, long periodExpenses, long suspendedUserCount) { this.totalSales = totalSales; this.periodRevenue = periodRevenue; this.activeOrders = activeOrders; @@ -19,6 +20,7 @@ public class DashboardStats { this.growth = growth; this.totalExpenses = totalExpenses; this.periodExpenses = periodExpenses; + this.suspendedUserCount = suspendedUserCount; } public long getTotalSales() { return totalSales; } @@ -41,4 +43,7 @@ public class DashboardStats { public long getPeriodExpenses() { return periodExpenses; } public void setPeriodExpenses(long periodExpenses) { this.periodExpenses = periodExpenses; } + + public long getSuspendedUserCount() { return suspendedUserCount; } + public void setSuspendedUserCount(long suspendedUserCount) { this.suspendedUserCount = suspendedUserCount; } } diff --git a/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java b/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java index e6d3a5bc..c1a01f3a 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/LoginResponse.java @@ -43,15 +43,17 @@ public class LoginResponse { private String mobileNumber; private String name; private boolean isLoggedIn; + private boolean isSuspended; private java.math.BigDecimal ritzTokenBalance; public UserDto() {} - public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn, java.math.BigDecimal ritzTokenBalance) { + public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn, boolean isSuspended, java.math.BigDecimal ritzTokenBalance) { this.id = id; this.mobileNumber = mobileNumber; this.name = name; this.isLoggedIn = isLoggedIn; + this.isSuspended = isSuspended; this.ritzTokenBalance = ritzTokenBalance; } @@ -67,6 +69,9 @@ public class LoginResponse { public boolean isLoggedIn() { return isLoggedIn; } public void setLoggedIn(boolean loggedIn) { this.isLoggedIn = loggedIn; } + public boolean isSuspended() { return isSuspended; } + public void setSuspended(boolean suspended) { isSuspended = suspended; } + public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; } public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; } } diff --git a/backend/src/main/java/com/rit/canteen/sales/model/User.java b/backend/src/main/java/com/rit/canteen/sales/model/User.java index 86097543..0a553b56 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/User.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/User.java @@ -37,6 +37,9 @@ public class User { @Column(nullable = false) private java.math.BigDecimal ritzTokenBalance = java.math.BigDecimal.ZERO; + @Column(nullable = false, columnDefinition = "boolean default false") + private boolean isSuspended = false; + @Column(nullable = true) private LocalDateTime lastLoginAt; @@ -54,6 +57,9 @@ public class User { public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; } public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; } + public boolean isSuspended() { return isSuspended; } + public void setSuspended(boolean suspended) { isSuspended = suspended; } + public String getPinHash() { return pinHash; } public void setPinHash(String pinHash) { this.pinHash = pinHash; } diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java index b017d41b..5a300d94 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java @@ -19,4 +19,6 @@ public interface UserRepository extends JpaRepository { @org.springframework.data.jpa.repository.Lock(jakarta.persistence.LockModeType.PESSIMISTIC_WRITE) @Query("SELECT u FROM User u WHERE u.id = :id") Optional findByIdWithLock(@org.springframework.data.repository.query.Param("id") Long id); + + long countByIsSuspended(boolean isSuspended); } diff --git a/backend/src/main/java/com/rit/canteen/sales/service/UserService.java b/backend/src/main/java/com/rit/canteen/sales/service/UserService.java index 553dc195..ed5aeb9e 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/UserService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/UserService.java @@ -61,7 +61,7 @@ public class UserService { userRepository.save(user); LoginResponse.UserDto userDto = new LoginResponse.UserDto( - user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance() + user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance() ); return new LoginResponse(true, "Registration successful. You are now logged in.", userDto); @@ -79,6 +79,10 @@ public class UserService { User user = userOpt.get(); + if (user.isSuspended()) { + return new LoginResponse(false, "Your account has been suspended. Please contact the administrator."); + } + if (!passwordEncoder.matches(pin, user.getPinHash())) { return new LoginResponse(false, "Incorrect PIN. Please try again."); } @@ -88,7 +92,7 @@ public class UserService { userRepository.save(user); LoginResponse.UserDto userDto = new LoginResponse.UserDto( - user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance() + user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance() ); return new LoginResponse(true, "Login successful.", userDto); @@ -154,7 +158,7 @@ public class UserService { return null; } User user = userOpt.get(); - return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()); + return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance()); } /** @@ -173,6 +177,7 @@ public class UserService { user.getMobileNumber(), user.getName(), user.isLoggedIn(), + user.isSuspended(), user.getRitzTokenBalance() )); } @@ -212,7 +217,28 @@ public class UserService { } userRepository.save(user); - return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()); + return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance()); + } + + /** + * Toggle the suspension status of a user. + */ + public LoginResponse.UserDto toggleSuspension(Long userId) { + Optional userOpt = userRepository.findById(userId); + if (userOpt.isEmpty()) { + return null; + } + + User user = userOpt.get(); + user.setSuspended(!user.isSuspended()); + + // If suspended, forcefully log them out + if (user.isSuspended()) { + user.setLoggedIn(false); + } + + userRepository.save(user); + return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.isSuspended(), user.getRitzTokenBalance()); } /** diff --git a/frontend/src/pages/Customers.tsx b/frontend/src/pages/Customers.tsx index 9a8f539f..594d4c47 100644 --- a/frontend/src/pages/Customers.tsx +++ b/frontend/src/pages/Customers.tsx @@ -7,6 +7,7 @@ interface UserDto { mobileNumber: string; name: string; loggedIn: boolean; + isSuspended: boolean; ritzTokenBalance: number; } @@ -66,7 +67,13 @@ const Customers: React.FC = () => { if (response.ok) { const data = await response.json(); if (data && data.content) { - setUsers(data.content); + // Normalize the data (backend sends 'suspended', frontend expects 'isSuspended') + const normalizedUsers = data.content.map((u: any) => ({ + ...u, + isSuspended: u.isSuspended !== undefined ? u.isSuspended : u.suspended, + loggedIn: u.loggedIn !== undefined ? u.loggedIn : u.isLoggedIn + })); + setUsers(normalizedUsers); setTotalElements(data.totalElements); } else { setUsers([]); @@ -115,6 +122,25 @@ const Customers: React.FC = () => { } }; + const handleSuspendToggle = async (user: UserDto) => { + try { + const host = window.location.hostname; + const response = await fetch(`http://${host}:8080/api/auth/users/${user.id}/suspend`, { + method: 'PATCH', + }); + if (response.ok) { + showNotification(`${user.name} ${user.isSuspended ? 'unsuspended' : 'suspended'} successfully`, 'success'); + fetchUsers(); + setOpenMenuId(null); + } else { + showNotification('Failed to update suspension status', 'error'); + } + } catch (error) { + console.error('Error toggling suspension:', error); + showNotification('Error connecting to server', 'error'); + } + }; + const validateForm = () => { const newErrors: {[key: string]: string} = {}; if (!editForm.name.trim()) newErrors.name = 'Full Name is required'; @@ -283,19 +309,16 @@ const Customers: React.FC = () => { - - - ₹{user.ritzTokenBalance?.toLocaleString() || '0'} - - -
- {user.loggedIn ? 'Active Now' : 'Offline'} +
+ {user.isSuspended ? 'Suspended' : user.loggedIn ? 'Active Now' : 'Offline'} @@ -308,6 +331,10 @@ const Customers: React.FC = () => { + @@ -339,9 +366,13 @@ const Customers: React.FC = () => { {openMenuId === user.id && (
- + @@ -363,6 +394,11 @@ const Customers: React.FC = () => { RIT STUDENT + {user.isSuspended && ( + + SUSPENDED + + )} ₹{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS diff --git a/frontend/src/pages/StoreDashboard.tsx b/frontend/src/pages/StoreDashboard.tsx index 536833ba..d9285f9f 100644 --- a/frontend/src/pages/StoreDashboard.tsx +++ b/frontend/src/pages/StoreDashboard.tsx @@ -5,7 +5,8 @@ import { Wallet, ArrowRight, RotateCcw, - Star + Star, + UserMinus } from 'lucide-react'; import { AreaChart, @@ -29,7 +30,8 @@ const StoreDashboard = () => { totalSales: 0, activeOrders: 0, dailyCustomers: 0, - revenueGrowth: 0 + revenueGrowth: 0, + suspendedUserCount: 0 }); const [trendingItems, setTrendingItems] = useState([]); const [salesData, setSalesData] = useState([]); @@ -107,7 +109,8 @@ const StoreDashboard = () => { totalSales: data.stats.totalSales, activeOrders: data.stats.activeOrders, dailyCustomers: data.stats.dailyCustomers, - revenueGrowth: data.stats.growth + revenueGrowth: data.stats.growth, + suspendedUserCount: data.stats.suspendedUserCount }); } @@ -290,8 +293,21 @@ const StoreDashboard = () => {
+ {/* Restricted Accounts Card */} +
+
+
+ +
+

Restricted Accounts

+
+
+

{stats?.suspendedUserCount || 0}

+
+
+ {/* Expenses Card */} -
+
diff --git a/ordering_site/src/contexts/AuthContext.tsx b/ordering_site/src/contexts/AuthContext.tsx index b01494bf..a1c29309 100644 --- a/ordering_site/src/contexts/AuthContext.tsx +++ b/ordering_site/src/contexts/AuthContext.tsx @@ -117,12 +117,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children if (response.ok) { const updatedUserDto = await response.json(); - // The DTO from backend might have different structure, let's map it back to User type const updatedUser: User = { id: updatedUserDto.id, name: updatedUserDto.name, mobileNumber: updatedUserDto.mobileNumber, isLoggedIn: updatedUserDto.loggedIn, + isSuspended: updatedUserDto.suspended, ritzTokenBalance: updatedUserDto.ritzTokenBalance }; setUser(updatedUser); @@ -137,6 +137,33 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; + // Background suspension check + useEffect(() => { + if (!user) return; + + const checkSuspensionStatus = async () => { + try { + const response = await fetch(`${API_BASE_URL}/user/${user.mobileNumber}`, { cache: 'no-store' }); + if (response.status === 404) { + logout(); // User deleted + return; + } + if (response.ok) { + const data = await response.json(); + if (data.isSuspended || data.suspended) { + console.log("Account suspended. Logging out..."); + logout(); + } + } + } catch (error) { + console.error('Error checking user status:', error); + } + }; + + const intervalId = setInterval(checkSuspensionStatus, 30000); // Check every 30 seconds + return () => clearInterval(intervalId); + }, [user]); + return ( {children} diff --git a/ordering_site/src/pages/LoginScreen.css b/ordering_site/src/pages/LoginScreen.css index e448c540..63bd8601 100644 --- a/ordering_site/src/pages/LoginScreen.css +++ b/ordering_site/src/pages/LoginScreen.css @@ -152,6 +152,124 @@ color: var(--primary); } +.error-message { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); + color: #ef4444; + padding: 12px 16px; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + margin-bottom: 24px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +/* Suspension Modal Styles */ +.suspension-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(8px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.suspension-modal { + background: rgba(255, 255, 255, 0.95); + width: 100%; + max-width: 400px; + border-radius: 28px; + padding: 40px 32px; + text-align: center; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.suspension-icon { + width: 80px; + height: 80px; + background: #fef2f2; + color: #ef4444; + border-radius: 24px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 24px; +} + +.suspension-modal h2 { + font-size: 24px; + font-weight: 800; + color: #1e293b; + margin-bottom: 12px; +} + +.suspension-modal p { + color: #64748b; + font-size: 15px; + line-height: 1.6; + margin-bottom: 24px; +} + +.suspension-details { + background: #f8fafc; + padding: 16px; + border-radius: 16px; + margin-bottom: 32px; + border: 1px solid #e2e8f0; +} + +.suspension-details span { + color: #475569; + font-size: 13px; + font-weight: 600; + line-height: 1.5; + display: block; +} + +.suspension-close-btn { + width: 100%; + padding: 16px; + background: #231651; + color: white; + border: none; + border-radius: 16px; + font-size: 16px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.suspension-close-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px -5px rgba(35, 22, 81, 0.4); +} + +.suspension-close-btn:active { + transform: translateY(0); +} + +@keyframes zoomIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.animate-zoomIn { + animation: zoomIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + .login-button { background-color: var(--primary); color: white; diff --git a/ordering_site/src/pages/LoginScreen.tsx b/ordering_site/src/pages/LoginScreen.tsx index f1d44cab..8fb00179 100644 --- a/ordering_site/src/pages/LoginScreen.tsx +++ b/ordering_site/src/pages/LoginScreen.tsx @@ -15,6 +15,7 @@ const LoginScreen: React.FC = () => { const [pin, setPin] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [isSuspended, setIsSuspended] = useState(false); const handleMobileSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -53,6 +54,8 @@ const LoginScreen: React.FC = () => { if (result.success) { navigate('/'); + } else if (result.isSuspended) { + setIsSuspended(true); } else { setError(result.message); } @@ -215,6 +218,24 @@ const LoginScreen: React.FC = () => {

RIT Canteen Management System

+ + {isSuspended && ( +
+
+
+ +
+

Account Suspended

+

Your access to the RIT Canteen system has been restricted by an administrator.

+
+ Please contact the Admin or Manager to resolve this issue. +
+ +
+
+ )}
); }; diff --git a/ordering_site/src/types/index.ts b/ordering_site/src/types/index.ts index 87d5b1d6..96101114 100644 --- a/ordering_site/src/types/index.ts +++ b/ordering_site/src/types/index.ts @@ -33,6 +33,7 @@ export interface User { mobileNumber: string; name: string; isLoggedIn: boolean; + isSuspended: boolean; ritzTokenBalance: number; }