From f7145193736cdc892bb77500581c1c7e9cf4dae5 Mon Sep 17 00:00:00 2001 From: Shanmuga Krishnan S M Date: Fri, 7 Aug 2026 14:03:00 +0530 Subject: [PATCH] feat(admin): add Admin Dashboard GUI portal at /admin for managing email subscribers --- .../portal/controller/AdminController.java | 162 ++++++++ .../src/main/resources/application.properties | 4 + src/App.tsx | 2 + src/pages/Admin/AdminDashboard.tsx | 358 ++++++++++++++++++ 4 files changed, 526 insertions(+) create mode 100644 backend/src/main/java/com/rit/portal/controller/AdminController.java create mode 100644 src/pages/Admin/AdminDashboard.tsx diff --git a/backend/src/main/java/com/rit/portal/controller/AdminController.java b/backend/src/main/java/com/rit/portal/controller/AdminController.java new file mode 100644 index 0000000..cc17cbc --- /dev/null +++ b/backend/src/main/java/com/rit/portal/controller/AdminController.java @@ -0,0 +1,162 @@ +package com.rit.portal.controller; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.*; + +@RestController +@RequestMapping("/api/admin") +@CrossOrigin(origins = "*") +public class AdminController { + + @Value("${admin.username:ritadmin}") + private String adminUsername; + + @Value("${admin.password:ritadmin2026!}") + private String adminPassword; + + private Path getRecipientsFilePath() { + // Try VPS deployment path first, fallback to workspace relative path + Path vpsPath = Paths.get("/var/www/freshers-hub/scripts/recipients.txt"); + if (Files.exists(vpsPath.getParent())) { + return vpsPath; + } + Path localPath = Paths.get("scripts/recipients.txt").toAbsolutePath(); + if (!Files.exists(localPath.getParent())) { + try { + Files.createDirectories(localPath.getParent()); + } catch (IOException ignored) {} + } + return localPath; + } + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody Map payload) { + String username = payload.get("username"); + String password = payload.get("password"); + + Map response = new HashMap<>(); + if (adminUsername.equals(username) && adminPassword.equals(password)) { + response.put("success", true); + response.put("message", "Admin login successful"); + response.put("token", "ADMIN_SESSION_TOKEN_RIT_2026"); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "Invalid admin username or password"); + return ResponseEntity.status(401).body(response); + } + } + + @GetMapping("/recipients") + public ResponseEntity> getRecipients() { + Path path = getRecipientsFilePath(); + List recipients = new ArrayList<>(); + if (Files.exists(path)) { + try { + List lines = Files.readAllLines(path); + for (String line : lines) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + recipients.add(trimmed); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return ResponseEntity.ok(recipients); + } + + @PostMapping("/recipients") + public ResponseEntity> addRecipient(@RequestBody Map payload) { + String email = payload.get("email"); + Map response = new HashMap<>(); + + if (email == null || !email.contains("@")) { + response.put("success", false); + response.put("message", "Please enter a valid email address"); + return ResponseEntity.badRequest().body(response); + } + + email = email.trim().toLowerCase(); + Path path = getRecipientsFilePath(); + List existing = getRecipientsList(path); + + if (existing.contains(email)) { + response.put("success", false); + response.put("message", "Email is already subscribed to notifications"); + return ResponseEntity.badRequest().body(response); + } + + existing.add(email); + saveRecipientsList(path, existing); + + response.put("success", true); + response.put("message", "Added recipient: " + email); + response.put("recipients", existing); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/recipients") + public ResponseEntity> removeRecipient(@RequestParam String email) { + Map response = new HashMap<>(); + if (email == null) { + response.put("success", false); + response.put("message", "Email parameter is required"); + return ResponseEntity.badRequest().body(response); + } + + String target = email.trim().toLowerCase(); + Path path = getRecipientsFilePath(); + List existing = getRecipientsList(path); + + boolean removed = existing.removeIf(e -> e.equalsIgnoreCase(target)); + if (removed) { + saveRecipientsList(path, existing); + response.put("success", true); + response.put("message", "Removed recipient: " + target); + response.put("recipients", existing); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "Email not found in recipient list"); + return ResponseEntity.badRequest().body(response); + } + } + + private List getRecipientsList(Path path) { + List recipients = new ArrayList<>(); + if (Files.exists(path)) { + try { + List lines = Files.readAllLines(path); + for (String line : lines) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + recipients.add(trimmed); + } + } + } catch (IOException ignored) {} + } + return recipients; + } + + private void saveRecipientsList(Path path, List recipients) { + try { + if (path.getParent() != null && !Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + Files.write(path, recipients, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 2b88b38..f5fd496 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -14,3 +14,7 @@ server.port=8085 # ─── GOOGLE OAUTH2 CONFIGURATION ─── google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com} + +# ─── ADMIN PORTAL CREDENTIALS ─── +admin.username=${ADMIN_USERNAME:ritadmin} +admin.password=${ADMIN_PASSWORD:ritadmin2026!} diff --git a/src/App.tsx b/src/App.tsx index 19058ff..fff7e8c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import BusRoutes from '@/pages/BusRoutes/BusRoutes'; import Faculty from '@/pages/Faculty/Faculty'; import DevCollab from '@/pages/DevCollab/DevCollab'; import LeetcodeLeaderboard from '@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard'; +import AdminDashboard from '@/pages/Admin/AdminDashboard'; const queryClient = new QueryClient({ defaultOptions: { @@ -49,6 +50,7 @@ function App() { } /> } /> } /> + } /> {/* Protected Routes — Require Google Sign-In */} (null); + const [loadingLogin, setLoadingLogin] = useState(false); + + // Subscribers management state + const [recipients, setRecipients] = useState([]); + const [newEmail, setNewEmail] = useState(''); + const [loadingRecipients, setLoadingRecipients] = useState(false); + const [submittingEmail, setSubmittingEmail] = useState(false); + const [toastMessage, setToastMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Check saved session token on mount + useEffect(() => { + const savedToken = localStorage.getItem('RIT_ADMIN_TOKEN'); + if (savedToken === 'ADMIN_SESSION_TOKEN_RIT_2026') { + setIsLoggedIn(true); + fetchRecipients(); + } + }, []); + + const showToast = (type: 'success' | 'error', text: string) => { + setToastMessage({ type, text }); + setTimeout(() => setToastMessage(null), 4000); + }; + + const handleLogin = (e: React.FormEvent) => { + e.preventDefault(); + if (!username || !password) { + setLoginError('Please enter both username and password'); + return; + } + setLoadingLogin(true); + setLoginError(null); + + fetch(getBackendUrl('/api/admin/login'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }) + .then((res) => { + if (!res.ok) throw new Error('Invalid username or password'); + return res.json(); + }) + .then((data) => { + if (data.success) { + localStorage.setItem('RIT_ADMIN_TOKEN', data.token); + setIsLoggedIn(true); + fetchRecipients(); + } else { + setLoginError(data.message || 'Login failed'); + } + }) + .catch((err) => { + setLoginError(err.message || 'Server connection error'); + }) + .finally(() => setLoadingLogin(false)); + }; + + const handleLogout = () => { + localStorage.removeItem('RIT_ADMIN_TOKEN'); + setIsLoggedIn(false); + setUsername(''); + setPassword(''); + }; + + const fetchRecipients = () => { + setLoadingRecipients(true); + fetch(getBackendUrl('/api/admin/recipients')) + .then((res) => res.json()) + .then((data) => { + if (Array.isArray(data)) { + setRecipients(data); + } + }) + .catch(() => showToast('error', 'Failed to fetch email subscribers')) + .finally(() => setLoadingRecipients(false)); + }; + + const handleAddRecipient = (e: React.FormEvent) => { + e.preventDefault(); + if (!newEmail || !newEmail.includes('@')) { + showToast('error', 'Please enter a valid email address'); + return; + } + + setSubmittingEmail(true); + fetch(getBackendUrl('/api/admin/recipients'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: newEmail }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + showToast('success', data.message || 'Email added successfully'); + setNewEmail(''); + if (Array.isArray(data.recipients)) { + setRecipients(data.recipients); + } else { + fetchRecipients(); + } + } else { + showToast('error', data.message || 'Could not add email'); + } + }) + .catch(() => showToast('error', 'Failed to add recipient')) + .finally(() => setSubmittingEmail(false)); + }; + + const handleRemoveRecipient = (email: string) => { + if (!confirm(`Are you sure you want to remove ${email} from deployment notifications?`)) return; + + fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { + method: 'DELETE', + }) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + showToast('success', data.message || 'Removed email recipient'); + if (Array.isArray(data.recipients)) { + setRecipients(data.recipients); + } else { + fetchRecipients(); + } + } else { + showToast('error', data.message || 'Could not remove email'); + } + }) + .catch(() => showToast('error', 'Failed to remove recipient')); + }; + + return ( +
+ {/* Background Decorative Blur */} +
+ + {/* Toast Notification */} + {toastMessage && ( +
+ {toastMessage.type === 'success' ? : } + {toastMessage.text} +
+ )} + +
+ {/* Navigation Breadcrumb */} +
+
+
+ +
+
+

+ RIT Portal Admin Console +

+

Manage deployment email subscribers & system settings

+
+
+ + + ← Back to Main Site + +
+ + {!isLoggedIn ? ( + /* Login Card */ + +
+
+ +
+

Admin Authentication

+

Enter your admin credentials to access subscriber controls

+
+ + {loginError && ( +
+ {loginError} +
+ )} + +
+
+ +
+ + setUsername(e.target.value)} + placeholder="Enter username (e.g. ritadmin)" + className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-orange-500 transition-colors" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + placeholder="Enter password" + className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-orange-500 transition-colors" + /> +
+
+ + +
+
+ ) : ( + /* Authenticated Admin Dashboard */ + + {/* Dashboard Header Bar */} +
+
+
+ + 🟢 Authenticated Session + + User: ritadmin +
+

Deployment Email Subscribers

+

+ Every git push automatically sends live health reports to all emails listed below. +

+
+ + +
+ + {/* Add New Subscriber Form */} +
+

+ + Add New Email Recipient +

+ +
+
+ + setNewEmail(e.target.value)} + placeholder="e.g. successor@ritchennai.edu.in" + className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-orange-500 transition-colors" + /> +
+ + +
+
+ + {/* Active Subscribers List Table */} +
+
+
+ +

+ Active Email Subscribers ({recipients.length}) +

+
+ + +
+ + {loadingRecipients ? ( +
Loading subscribers list...
+ ) : recipients.length === 0 ? ( +
+ No email subscribers found. Use the form above to add a recipient! +
+ ) : ( +
+ {recipients.map((email, idx) => ( +
+
+
+ +
+
+

{email}

+

Receives deployment reports on git push

+
+
+ + +
+ ))} +
+ )} +
+
+ )} +
+
+ ); +}