From 302a39e9335ffc2df1143b84f1dc166409d19806 Mon Sep 17 00:00:00 2001 From: Shanmuga Krishnan S M Date: Thu, 6 Aug 2026 12:23:50 +0530 Subject: [PATCH] feat: add Google OAuth2 Sign-In with verified student badge for DevCollab --- .../rit/portal/controller/AuthController.java | 67 ++++++ .../com/rit/portal/entity/CollabRequest.java | 3 + .../main/java/com/rit/portal/entity/User.java | 47 ++++ .../rit/portal/repository/UserRepository.java | 11 + .../rit/portal/service/GoogleAuthService.java | 104 ++++++++ .../src/main/resources/application.properties | 4 + canteen_locustfile.py | 28 +++ index.html | 3 + locustfile.py | 27 +++ src/App.tsx | 33 +-- src/components/Navbar/Navbar.tsx | 153 +++++++++++- src/context/AuthContext.tsx | 226 ++++++++++++++++++ src/pages/DevCollab/DevCollab.tsx | 125 +++++++++- 13 files changed, 802 insertions(+), 29 deletions(-) create mode 100644 backend/src/main/java/com/rit/portal/controller/AuthController.java create mode 100644 backend/src/main/java/com/rit/portal/entity/User.java create mode 100644 backend/src/main/java/com/rit/portal/repository/UserRepository.java create mode 100644 backend/src/main/java/com/rit/portal/service/GoogleAuthService.java create mode 100644 canteen_locustfile.py create mode 100644 locustfile.py create mode 100644 src/context/AuthContext.tsx diff --git a/backend/src/main/java/com/rit/portal/controller/AuthController.java b/backend/src/main/java/com/rit/portal/controller/AuthController.java new file mode 100644 index 0000000..a9a912e --- /dev/null +++ b/backend/src/main/java/com/rit/portal/controller/AuthController.java @@ -0,0 +1,67 @@ +package com.rit.portal.controller; + +import com.rit.portal.entity.User; +import com.rit.portal.service.GoogleAuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired + private GoogleAuthService googleAuthService; + + /** + * POST /api/auth/google + * Body: { "credential": "" } + * Verifies the Google ID token, registers/logs in user, returns user profile. + */ + @PostMapping("/google") + public ResponseEntity googleLogin(@RequestBody Map body) { + String credential = body.get("credential"); + if (credential == null || credential.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("error", "Missing Google credential token")); + } + + User user = googleAuthService.verifyGoogleTokenAndLogin(credential); + if (user == null) { + return ResponseEntity.status(401).body(Map.of("error", "Invalid or expired Google token")); + } + + // Return user profile (the credential itself serves as the session token + // since the frontend will send it with subsequent requests) + Map response = new HashMap<>(); + response.put("id", user.getId()); + response.put("email", user.getEmail()); + response.put("name", user.getName()); + response.put("pictureUrl", user.getPictureUrl()); + response.put("verifiedStudent", user.getVerifiedStudent()); + response.put("createdAt", user.getCreatedAt()); + + return ResponseEntity.ok(response); + } + + /** + * GET /api/auth/me?email=user@example.com + * Quick lookup to check if a stored session is still valid. + */ + @GetMapping("/me") + public ResponseEntity getCurrentUser(@RequestParam String email) { + return googleAuthService.findByEmail(email) + .map(user -> { + Map response = new HashMap<>(); + response.put("id", user.getId()); + response.put("email", user.getEmail()); + response.put("name", user.getName()); + response.put("pictureUrl", user.getPictureUrl()); + response.put("verifiedStudent", user.getVerifiedStudent()); + return ResponseEntity.ok(response); + }) + .orElse(ResponseEntity.status(401).build()); + } +} diff --git a/backend/src/main/java/com/rit/portal/entity/CollabRequest.java b/backend/src/main/java/com/rit/portal/entity/CollabRequest.java index d0800db..c74b4cc 100644 --- a/backend/src/main/java/com/rit/portal/entity/CollabRequest.java +++ b/backend/src/main/java/com/rit/portal/entity/CollabRequest.java @@ -22,6 +22,9 @@ public class CollabRequest { @Column(name = "author_name", nullable = false) private String authorName; + @Column(name = "author_email") + private String authorEmail; + @Column(nullable = false) private String department; diff --git a/backend/src/main/java/com/rit/portal/entity/User.java b/backend/src/main/java/com/rit/portal/entity/User.java new file mode 100644 index 0000000..8214d7e --- /dev/null +++ b/backend/src/main/java/com/rit/portal/entity/User.java @@ -0,0 +1,47 @@ +package com.rit.portal.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String name; + + @Column(name = "picture_url") + private String pictureUrl; + + @Column(name = "google_id", unique = true) + private String googleId; + + /** + * true if the email ends with @ritchennai.edu.in + * Only verified students can post/apply to DevCollab. + */ + @Column(name = "verified_student") + @Builder.Default + private Boolean verifiedStudent = false; + + @Column(name = "created_at") + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(name = "last_login_at") + @Builder.Default + private LocalDateTime lastLoginAt = LocalDateTime.now(); +} diff --git a/backend/src/main/java/com/rit/portal/repository/UserRepository.java b/backend/src/main/java/com/rit/portal/repository/UserRepository.java new file mode 100644 index 0000000..d8605b6 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.rit.portal.repository; + +import com.rit.portal.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByGoogleId(String googleId); +} diff --git a/backend/src/main/java/com/rit/portal/service/GoogleAuthService.java b/backend/src/main/java/com/rit/portal/service/GoogleAuthService.java new file mode 100644 index 0000000..83a4bde --- /dev/null +++ b/backend/src/main/java/com/rit/portal/service/GoogleAuthService.java @@ -0,0 +1,104 @@ +package com.rit.portal.service; + +import com.rit.portal.entity.User; +import com.rit.portal.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Optional; + +@Service +public class GoogleAuthService { + + @Autowired + private UserRepository userRepository; + + @Value("${google.client.id:}") + private String googleClientId; + + private static final String COLLEGE_DOMAIN = "ritchennai.edu.in"; + private static final String GOOGLE_TOKEN_INFO_URL = "https://oauth2.googleapis.com/tokeninfo?id_token="; + + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * Verify a Google ID token by calling Google's tokeninfo endpoint. + * Returns the authenticated/registered User, or null on failure. + */ + public User verifyGoogleTokenAndLogin(String idToken) { + try { + // Call Google's tokeninfo endpoint to verify the token + @SuppressWarnings("unchecked") + Map tokenInfo = restTemplate.getForObject( + GOOGLE_TOKEN_INFO_URL + idToken, Map.class + ); + + if (tokenInfo == null || !tokenInfo.containsKey("email")) { + System.err.println("Google token verification failed: no email in response"); + return null; + } + + // Validate audience (aud) matches our Client ID if configured + if (googleClientId != null && !googleClientId.isBlank()) { + String aud = (String) tokenInfo.get("aud"); + if (aud == null || !aud.equals(googleClientId)) { + System.err.println("Google token audience mismatch: expected=" + googleClientId + " got=" + aud); + return null; + } + } + + String email = (String) tokenInfo.get("email"); + String name = (String) tokenInfo.getOrDefault("name", email.split("@")[0]); + String pictureUrl = (String) tokenInfo.get("picture"); + String googleId = (String) tokenInfo.get("sub"); + + // Check if user already exists + Optional existingUser = userRepository.findByEmail(email); + if (existingUser.isPresent()) { + User user = existingUser.get(); + // Update last login and any changed profile info + user.setLastLoginAt(LocalDateTime.now()); + if (name != null) user.setName(name); + if (pictureUrl != null) user.setPictureUrl(pictureUrl); + // Re-check verified status in case domain changed (unlikely but safe) + user.setVerifiedStudent(isCollegeDomain(email)); + return userRepository.save(user); + } + + // Create new user + User newUser = User.builder() + .email(email) + .name(name) + .pictureUrl(pictureUrl) + .googleId(googleId) + .verifiedStudent(isCollegeDomain(email)) + .createdAt(LocalDateTime.now()) + .lastLoginAt(LocalDateTime.now()) + .build(); + + return userRepository.save(newUser); + + } catch (Exception e) { + System.err.println("Google token verification error: " + e.getMessage()); + return null; + } + } + + /** + * Returns true if the email belongs to the college domain. + */ + public boolean isCollegeDomain(String email) { + return email != null && email.toLowerCase().endsWith("@" + COLLEGE_DOMAIN); + } + + /** + * Find a user by email (used by controllers to validate tokens from headers). + */ + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5d3b3b3..09a1aaa 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -11,3 +11,7 @@ spring.jpa.properties.hibernate.format_sql=true # ─── SERVER PORT ─── server.port=8085 + +# ─── GOOGLE OAUTH2 CONFIGURATION ─── +# Replace with your Google Cloud Console OAuth2 Client ID +google.client.id=${GOOGLE_CLIENT_ID:} diff --git a/canteen_locustfile.py b/canteen_locustfile.py new file mode 100644 index 0000000..a99dc73 --- /dev/null +++ b/canteen_locustfile.py @@ -0,0 +1,28 @@ +from locust import HttpUser, task, between +import random + +class CanteenStudentUser(HttpUser): + # Mimics student browsing interval between 1 and 3 seconds + wait_time = between(1, 3) + + @task(4) + def view_canteen_homepage(self): + """Simulates students visiting the main Canteen portal""" + self.client.get("/", name="Homepage (Canteen Portal)") + + @task(3) + def view_canteen_menu(self): + """Simulates students checking daily menu items""" + self.client.get("/api/menu", name="/api/menu (Daily Canteen Menu)", catch_response=True) + + @task(2) + def check_food_categories(self): + """Simulates filtering food categories (Snacks, Beverages, Meals)""" + category = random.choice(["breakfast", "lunch", "snacks", "beverages"]) + self.client.get(f"/api/menu?category={category}", name=f"/api/menu?category={category}") + + @task(1) + def simulate_food_order_check(self): + """Simulates student checking order queue status""" + order_id = random.randint(100, 999) + self.client.get(f"/api/orders/{order_id}", name="/api/orders/{id} (Order Status)", catch_response=True) diff --git a/index.html b/index.html index 7675e94..f9cfd86 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,9 @@ + + + RIT Freshers Hub – Student Portal diff --git a/locustfile.py b/locustfile.py new file mode 100644 index 0000000..7913d1d --- /dev/null +++ b/locustfile.py @@ -0,0 +1,27 @@ +from locust import HttpUser, task, between +import random + +class WebsiteStudentUser(HttpUser): + # Wait between 1 and 3 seconds between actions (mimics real human behavior) + wait_time = between(1, 3) + + @task(5) + def view_live_bus_locations(self): + """Simulates students viewing live bus locations on map""" + self.client.get("/api/bus-locations", name="/api/bus-locations (Live Locations)") + + @task(2) + def view_bus_routes(self): + """Simulates students loading static bus routes JSON""" + self.client.get("/bus_routes.json", name="/bus_routes.json (Routes Data)") + + @task(1) + def simulate_driver_broadcast(self): + """Simulates a driver app broadcasting GPS coordinates""" + route_num = random.randint(1, 15) + lat = 13.0118 + random.uniform(-0.01, 0.01) + lng = 80.0214 + random.uniform(-0.01, 0.01) + self.client.post( + f"/api/bus-locations/R{route_num:02d}?lat={lat:.6f}&lng={lng:.6f}&pin=RITDRIVER", + name="/api/bus-locations/R{id} (Driver Broadcast)" + ) diff --git a/src/App.tsx b/src/App.tsx index 4be763f..c850ae9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AuthProvider } from '@/context/AuthContext'; import MainLayout from '@/layouts/MainLayout'; import Home from '@/pages/Home/Home'; import Notes from '@/pages/Notes/Notes'; @@ -36,21 +37,23 @@ function App() { return ( - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index 4ea9673..a27b419 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -1,13 +1,17 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; -import { Menu, X } from 'lucide-react'; +import { Menu, X, LogIn, LogOut, ShieldCheck, ChevronDown } from 'lucide-react'; import { NAV_LINKS } from '@/constants'; +import { useAuth } from '@/context/AuthContext'; export default function Navbar() { const [isScrolled, setIsScrolled] = useState(false); const [isMobileOpen, setIsMobileOpen] = useState(false); + const [isProfileOpen, setIsProfileOpen] = useState(false); const location = useLocation(); + const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth(); + const profileRef = useRef(null); useEffect(() => { const handleScroll = () => setIsScrolled(window.scrollY > 20); @@ -19,6 +23,17 @@ export default function Navbar() { setIsMobileOpen(false); }, [location.pathname]); + // Close profile dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setIsProfileOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + const checkIsActive = (linkPath: string) => { const [pathName, searchString] = linkPath.split('?'); if (location.pathname !== pathName) return false; @@ -104,8 +119,111 @@ export default function Navbar() { })} - {/* Right Side */} -
+ {/* Right Side: Auth + Mobile Toggle */} +
+ {/* Auth Section */} + {isAuthenticated && user ? ( +
+ + + {/* Profile Dropdown */} + + {isProfileOpen && ( + +
+
+ {user.pictureUrl ? ( + {user.name} + ) : ( +
+ {user.name.charAt(0).toUpperCase()} +
+ )} +
+

{user.name}

+

{user.email}

+
+
+ {isVerifiedStudent ? ( +
+ + Verified RIT Student +
+ ) : ( +
+ + Sign in with @ritchennai.edu.in for verified badge + +
+ )} +
+
+ +
+
+ )} +
+
+ ) : ( + + + Sign In + + )} + {/* Mobile Toggle */} ); })} + + {/* Mobile Auth Button */} + {!isAuthenticated ? ( + + ) : ( + + )}
)} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..a964095 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,226 @@ +import { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import type { ReactNode } from 'react'; +import { getBackendUrl } from '@/lib/utils'; + +// ────────────────────────────────────────────────── +// Types +// ────────────────────────────────────────────────── + +export interface AuthUser { + id: number; + email: string; + name: string; + pictureUrl?: string; + verifiedStudent: boolean; +} + +interface AuthContextType { + user: AuthUser | null; + isAuthenticated: boolean; + isVerifiedStudent: boolean; + isLoading: boolean; + loginWithGoogle: () => void; + logout: () => void; +} + +// ────────────────────────────────────────────────── +// Google Client ID +// ────────────────────────────────────────────────── + +// IMPORTANT: Replace this with your actual Google Cloud Console OAuth2 Client ID +// Create one at: https://console.cloud.google.com/apis/credentials +// Authorized origins: https://rit-services.in, http://localhost:5173 +const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || ''; + +// ────────────────────────────────────────────────── +// Context +// ────────────────────────────────────────────────── + +const AuthContext = createContext({ + user: null, + isAuthenticated: false, + isVerifiedStudent: false, + isLoading: true, + loginWithGoogle: () => {}, + logout: () => {}, +}); + +export const useAuth = () => useContext(AuthContext); + +// ────────────────────────────────────────────────── +// Google GSI global type +// ────────────────────────────────────────────────── + +declare global { + interface Window { + google?: { + accounts: { + id: { + initialize: (config: Record) => void; + prompt: (callback?: (notification: { isNotDisplayed: () => boolean; isSkippedMoment: () => boolean }) => void) => void; + renderButton: (element: HTMLElement, config: Record) => void; + revoke: (email: string, callback: () => void) => void; + }; + }; + }; + } +} + +// ────────────────────────────────────────────────── +// Provider +// ────────────────────────────────────────────────── + +const STORAGE_KEY = 'rit_auth_user'; + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Handle the credential response from Google GSI + const handleCredentialResponse = useCallback(async (response: { credential: string }) => { + try { + const res = await fetch(getBackendUrl('/api/auth/google'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credential: response.credential }), + }); + + if (!res.ok) { + console.error('Auth failed:', res.status); + return; + } + + const userData: AuthUser = await res.json(); + setUser(userData); + localStorage.setItem(STORAGE_KEY, JSON.stringify(userData)); + } catch (err) { + console.error('Google auth error:', err); + } + }, []); + + // Initialize Google GSI + useEffect(() => { + // 1. Restore session from localStorage + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + const parsed = JSON.parse(stored) as AuthUser; + setUser(parsed); + + // Validate session is still valid against backend + fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`)) + .then(res => { + if (!res.ok) { + // Session expired, clear it + localStorage.removeItem(STORAGE_KEY); + setUser(null); + } else { + // Update with fresh data + return res.json(); + } + }) + .then(freshData => { + if (freshData) { + setUser(freshData); + localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData)); + } + }) + .catch(() => { + // Backend offline, keep cached session + }); + } catch { + localStorage.removeItem(STORAGE_KEY); + } + } + setIsLoading(false); + + // 2. Initialize Google GSI when the script loads + const initGSI = () => { + if (!window.google || !GOOGLE_CLIENT_ID) return; + + window.google.accounts.id.initialize({ + client_id: GOOGLE_CLIENT_ID, + callback: handleCredentialResponse, + auto_select: false, + cancel_on_tap_outside: true, + }); + }; + + // The GSI script may not have loaded yet (async), so poll briefly + if (window.google) { + initGSI(); + } else { + const interval = setInterval(() => { + if (window.google) { + clearInterval(interval); + initGSI(); + } + }, 200); + // Stop polling after 10 seconds + setTimeout(() => clearInterval(interval), 10000); + } + }, [handleCredentialResponse]); + + const loginWithGoogle = useCallback(() => { + if (!GOOGLE_CLIENT_ID) { + console.warn('Google Client ID not configured. Set VITE_GOOGLE_CLIENT_ID in .env'); + return; + } + + if (window.google) { + // Trigger the One Tap prompt + window.google.accounts.id.prompt((notification) => { + if (notification.isNotDisplayed() || notification.isSkippedMoment()) { + // If One Tap doesn't show (e.g. blocked by browser), fall back to button-based flow + // Render a temporary popup button + const popup = document.createElement('div'); + popup.id = 'google-signin-popup'; + popup.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:99999;background:white;padding:32px;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);'; + document.body.appendChild(popup); + + window.google!.accounts.id.renderButton(popup, { + theme: 'outline', + size: 'large', + text: 'signin_with', + shape: 'pill', + width: 300, + }); + + // Remove after 30s or when clicked + setTimeout(() => popup.remove(), 30000); + const observer = new MutationObserver(() => { + if (!document.getElementById('google-signin-popup')) { + observer.disconnect(); + } + }); + observer.observe(document.body, { childList: true }); + } + }); + } + }, []); + + const logout = useCallback(() => { + if (user?.email && window.google) { + window.google.accounts.id.revoke(user.email, () => { + console.log('Google session revoked'); + }); + } + setUser(null); + localStorage.removeItem(STORAGE_KEY); + }, [user]); + + return ( + + {children} + + ); +} diff --git a/src/pages/DevCollab/DevCollab.tsx b/src/pages/DevCollab/DevCollab.tsx index c7f346c..8335e11 100644 --- a/src/pages/DevCollab/DevCollab.tsx +++ b/src/pages/DevCollab/DevCollab.tsx @@ -3,10 +3,12 @@ import { motion, AnimatePresence } from 'framer-motion'; import { Code2, Plus, GitBranch, Tag, User, Send, CheckCircle2, Search, Filter, Sparkles, X, ExternalLink, Layers, - ChevronRight, MessageSquare, Clock, Users, ArrowUpRight + ChevronRight, MessageSquare, Clock, Users, ArrowUpRight, + ShieldCheck, LogIn, ShieldAlert } from 'lucide-react'; import { Link } from 'react-router-dom'; import { getBackendUrl } from '@/lib/utils'; +import { useAuth } from '@/context/AuthContext'; export interface CollabRequestItem { id: number; @@ -84,6 +86,8 @@ const INITIAL_MOCK_REQUESTS: CollabRequestItem[] = [ ]; export default function DevCollab() { + const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle } = useAuth(); + const [requests, setRequests] = useState(INITIAL_MOCK_REQUESTS); const [loading, setLoading] = useState(false); const [activeTagFilter, setActiveTagFilter] = useState('All'); @@ -93,24 +97,34 @@ export default function DevCollab() { const [showPostModal, setShowPostModal] = useState(false); const [selectedCollab, setSelectedCollab] = useState(null); const [toastMessage, setToastMessage] = useState(null); + const [showAuthGate, setShowAuthGate] = useState(false); - // New Request Form - const [newAuthorName, setNewAuthorName] = useState(''); + // New Request Form — auto-populated from Google profile for verified students + const [newAuthorName, setNewAuthorName] = useState(user?.name || ''); const [newDept, setNewDept] = useState('CSE'); const [newYear, setNewYear] = useState('1st Year'); const [newTag, setNewTag] = useState(TAG_OPTIONS[0]); const [newCollaboratorsNeeded, setNewCollaboratorsNeeded] = useState(1); const [newIdea, setNewIdea] = useState(''); const [newGithub, setNewGithub] = useState(''); - const [newContact, setNewContact] = useState(''); + const [newContact, setNewContact] = useState(user?.email || ''); const [submittingPost, setSubmittingPost] = useState(false); - // Application Form - const [appApplicantName, setAppApplicantName] = useState(''); + // Application Form — auto-populated from Google profile + const [appApplicantName, setAppApplicantName] = useState(user?.name || ''); const [appDept, setAppDept] = useState('CSE'); const [appYear, setAppYear] = useState('1st Year'); - const [appContact, setAppContact] = useState(''); + const [appContact, setAppContact] = useState(user?.email || ''); const [appMessage, setAppMessage] = useState(''); + + // Helper: gate action behind verified student check + const requireVerifiedStudent = (action: () => void) => { + if (!isAuthenticated || !isVerifiedStudent) { + setShowAuthGate(true); + return; + } + action(); + }; const [submittingApp, setSubmittingApp] = useState(false); const fetchCollabRequests = () => { @@ -306,10 +320,11 @@ export default function DevCollab() { setShowPostModal(true)} + onClick={() => requireVerifiedStudent(() => setShowPostModal(true))} className="flex items-center gap-2.5 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm shadow-lg shadow-orange-500/25 shrink-0" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }} > + {isVerifiedStudent && } Post Collaboration Request @@ -471,7 +486,7 @@ export default function DevCollab() { ) : (
)} + + {/* ─── AUTH GATE MODAL (Verified Student Required) ─────────────────────── */} + + {showAuthGate && ( +
+ setShowAuthGate(false)} + className="absolute inset-0 bg-slate-900/60 backdrop-blur-xs" + /> + + + +
+ +
+ +

+ {!isAuthenticated + ? 'Sign In Required' + : 'Verified Student Account Required'} +

+ +

+ {!isAuthenticated ? ( + <>To post or apply for collaboration requests, please sign in with your @ritchennai.edu.in Google account. + ) : ( + <>You are signed in as {user?.email}, which is not a verified RIT college email. Please sign in with your @ritchennai.edu.in email to access DevCollab features. + )} +

+ +
+ {!isAuthenticated ? ( + + ) : ( + + )} + +

+ We only verify your email domain — no personal data is stored beyond your name and email. +

+
+
+
+ )} +
); }