Compare commits
2 Commits
2be0c04b80
...
3ddcff8035
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ddcff8035 | ||
|
|
302a39e933 |
@@ -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": "<Google ID Token>" }
|
||||||
|
* Verifies the Google ID token, registers/logs in user, returns user profile.
|
||||||
|
*/
|
||||||
|
@PostMapping("/google")
|
||||||
|
public ResponseEntity<?> googleLogin(@RequestBody Map<String, String> 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<String, Object> 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<String, Object> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,9 @@ public class CollabRequest {
|
|||||||
@Column(name = "author_name", nullable = false)
|
@Column(name = "author_name", nullable = false)
|
||||||
private String authorName;
|
private String authorName;
|
||||||
|
|
||||||
|
@Column(name = "author_email")
|
||||||
|
private String authorEmail;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String department;
|
private String department;
|
||||||
|
|
||||||
|
|||||||
47
backend/src/main/java/com/rit/portal/entity/User.java
Normal file
47
backend/src/main/java/com/rit/portal/entity/User.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
@@ -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<User, Long> {
|
||||||
|
Optional<User> findByEmail(String email);
|
||||||
|
Optional<User> findByGoogleId(String googleId);
|
||||||
|
}
|
||||||
@@ -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<String, Object> 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<User> 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<User> findByEmail(String email) {
|
||||||
|
return userRepository.findByEmail(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,3 +11,6 @@ spring.jpa.properties.hibernate.format_sql=true
|
|||||||
|
|
||||||
# ─── SERVER PORT ───
|
# ─── SERVER PORT ───
|
||||||
server.port=8085
|
server.port=8085
|
||||||
|
|
||||||
|
# ─── GOOGLE OAUTH2 CONFIGURATION ───
|
||||||
|
google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com}
|
||||||
|
|||||||
28
canteen_locustfile.py
Normal file
28
canteen_locustfile.py
Normal file
@@ -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)
|
||||||
@@ -23,6 +23,9 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
|
<!-- Google Identity Services (Sign-In with Google) -->
|
||||||
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||||
|
|
||||||
<title>RIT Freshers Hub – Student Portal</title>
|
<title>RIT Freshers Hub – Student Portal</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
27
locustfile.py
Normal file
27
locustfile.py
Normal file
@@ -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)"
|
||||||
|
)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { AuthProvider } from '@/context/AuthContext';
|
||||||
import MainLayout from '@/layouts/MainLayout';
|
import MainLayout from '@/layouts/MainLayout';
|
||||||
import Home from '@/pages/Home/Home';
|
import Home from '@/pages/Home/Home';
|
||||||
import Notes from '@/pages/Notes/Notes';
|
import Notes from '@/pages/Notes/Notes';
|
||||||
@@ -36,6 +37,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<AuthProvider>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
@@ -51,6 +53,7 @@ function App() {
|
|||||||
<Route path="/leetcode" element={<LeetcodeLeaderboard />} />
|
<Route path="/leetcode" element={<LeetcodeLeaderboard />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
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 { NAV_LINKS } from '@/constants';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||||
|
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth();
|
||||||
|
const profileRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => setIsScrolled(window.scrollY > 20);
|
const handleScroll = () => setIsScrolled(window.scrollY > 20);
|
||||||
@@ -19,6 +23,17 @@ export default function Navbar() {
|
|||||||
setIsMobileOpen(false);
|
setIsMobileOpen(false);
|
||||||
}, [location.pathname]);
|
}, [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 checkIsActive = (linkPath: string) => {
|
||||||
const [pathName, searchString] = linkPath.split('?');
|
const [pathName, searchString] = linkPath.split('?');
|
||||||
if (location.pathname !== pathName) return false;
|
if (location.pathname !== pathName) return false;
|
||||||
@@ -104,8 +119,111 @@ export default function Navbar() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side */}
|
{/* Right Side: Auth + Mobile Toggle */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Auth Section */}
|
||||||
|
{isAuthenticated && user ? (
|
||||||
|
<div className="relative" ref={profileRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsProfileOpen(!isProfileOpen)}
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded-xl hover:bg-white/10 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{user.pictureUrl ? (
|
||||||
|
<img
|
||||||
|
src={user.pictureUrl}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-8 h-8 rounded-full border-2 border-white/30 object-cover"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-white font-bold text-sm">
|
||||||
|
{user.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="hidden lg:flex flex-col items-start">
|
||||||
|
<span className="text-white text-[11px] font-semibold leading-tight max-w-[100px] truncate">
|
||||||
|
{user.name.split(' ')[0]}
|
||||||
|
</span>
|
||||||
|
{isVerifiedStudent && (
|
||||||
|
<span className="flex items-center gap-0.5 text-[9px] text-emerald-400 font-bold">
|
||||||
|
<ShieldCheck className="w-2.5 h-2.5" /> Verified
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="w-3 h-3 text-slate-400 hidden lg:block" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Profile Dropdown */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isProfileOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 8, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 8, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="absolute right-0 top-full mt-2 w-64 bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden z-50"
|
||||||
|
>
|
||||||
|
<div className="p-4 border-b border-slate-700/60">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{user.pictureUrl ? (
|
||||||
|
<img
|
||||||
|
src={user.pictureUrl}
|
||||||
|
alt={user.name}
|
||||||
|
className="w-10 h-10 rounded-full border-2 border-orange-500/40 object-cover"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-white font-bold text-lg">
|
||||||
|
{user.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-white text-sm font-bold truncate">{user.name}</p>
|
||||||
|
<p className="text-slate-400 text-[11px] truncate">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isVerifiedStudent ? (
|
||||||
|
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-emerald-500/15 border border-emerald-500/25">
|
||||||
|
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||||
|
<span className="text-[11px] font-bold text-emerald-400">Verified RIT Student</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-amber-500/15 border border-amber-500/25">
|
||||||
|
<span className="text-[11px] font-semibold text-amber-400">
|
||||||
|
Sign in with @ritchennai.edu.in for verified badge
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
setIsProfileOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-slate-300 hover:text-white hover:bg-red-500/15 text-xs font-semibold transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
onClick={loginWithGoogle}
|
||||||
|
className="hidden md:flex items-center gap-2 px-4 py-2 rounded-xl text-white text-[11px] font-semibold border border-white/15 hover:border-orange-500/40 hover:bg-white/5 transition-all cursor-pointer"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
<LogIn className="w-3.5 h-3.5" />
|
||||||
|
Sign In
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mobile Toggle */}
|
{/* Mobile Toggle */}
|
||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
@@ -148,6 +266,33 @@ export default function Navbar() {
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Mobile Auth Button */}
|
||||||
|
{!isAuthenticated ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
loginWithGoogle();
|
||||||
|
setIsMobileOpen(false);
|
||||||
|
}}
|
||||||
|
className="col-span-2 mt-2 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-white text-xs font-semibold border border-orange-500/40 bg-orange-500/10 hover:bg-orange-500/20 transition-all cursor-pointer"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
<LogIn className="w-4 h-4" />
|
||||||
|
Sign In with Google
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
setIsMobileOpen(false);
|
||||||
|
}}
|
||||||
|
className="col-span-2 mt-2 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-red-400 text-xs font-semibold border border-red-500/30 bg-red-500/10 hover:bg-red-500/20 transition-all cursor-pointer"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Sign Out ({user?.name.split(' ')[0]})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
226
src/context/AuthContext.tsx
Normal file
226
src/context/AuthContext.tsx
Normal file
@@ -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<AuthContextType>({
|
||||||
|
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<string, unknown>) => void;
|
||||||
|
prompt: (callback?: (notification: { isNotDisplayed: () => boolean; isSkippedMoment: () => boolean }) => void) => void;
|
||||||
|
renderButton: (element: HTMLElement, config: Record<string, unknown>) => void;
|
||||||
|
revoke: (email: string, callback: () => void) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────
|
||||||
|
// Provider
|
||||||
|
// ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'rit_auth_user';
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [user, setUser] = useState<AuthUser | null>(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 (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
isAuthenticated: !!user,
|
||||||
|
isVerifiedStudent: !!user?.verifiedStudent,
|
||||||
|
isLoading,
|
||||||
|
loginWithGoogle,
|
||||||
|
logout,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
import {
|
import {
|
||||||
Code2, Plus, GitBranch, Tag, User, Send, CheckCircle2,
|
Code2, Plus, GitBranch, Tag, User, Send, CheckCircle2,
|
||||||
Search, Filter, Sparkles, X, ExternalLink, Layers,
|
Search, Filter, Sparkles, X, ExternalLink, Layers,
|
||||||
ChevronRight, MessageSquare, Clock, Users, ArrowUpRight
|
ChevronRight, MessageSquare, Clock, Users, ArrowUpRight,
|
||||||
|
ShieldCheck, LogIn, ShieldAlert
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { getBackendUrl } from '@/lib/utils';
|
import { getBackendUrl } from '@/lib/utils';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
|
||||||
export interface CollabRequestItem {
|
export interface CollabRequestItem {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -84,6 +86,8 @@ const INITIAL_MOCK_REQUESTS: CollabRequestItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function DevCollab() {
|
export default function DevCollab() {
|
||||||
|
const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle } = useAuth();
|
||||||
|
|
||||||
const [requests, setRequests] = useState<CollabRequestItem[]>(INITIAL_MOCK_REQUESTS);
|
const [requests, setRequests] = useState<CollabRequestItem[]>(INITIAL_MOCK_REQUESTS);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeTagFilter, setActiveTagFilter] = useState('All');
|
const [activeTagFilter, setActiveTagFilter] = useState('All');
|
||||||
@@ -93,24 +97,34 @@ export default function DevCollab() {
|
|||||||
const [showPostModal, setShowPostModal] = useState(false);
|
const [showPostModal, setShowPostModal] = useState(false);
|
||||||
const [selectedCollab, setSelectedCollab] = useState<CollabRequestItem | null>(null);
|
const [selectedCollab, setSelectedCollab] = useState<CollabRequestItem | null>(null);
|
||||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||||
|
const [showAuthGate, setShowAuthGate] = useState(false);
|
||||||
|
|
||||||
// New Request Form
|
// New Request Form — auto-populated from Google profile for verified students
|
||||||
const [newAuthorName, setNewAuthorName] = useState('');
|
const [newAuthorName, setNewAuthorName] = useState(user?.name || '');
|
||||||
const [newDept, setNewDept] = useState('CSE');
|
const [newDept, setNewDept] = useState('CSE');
|
||||||
const [newYear, setNewYear] = useState('1st Year');
|
const [newYear, setNewYear] = useState('1st Year');
|
||||||
const [newTag, setNewTag] = useState(TAG_OPTIONS[0]);
|
const [newTag, setNewTag] = useState(TAG_OPTIONS[0]);
|
||||||
const [newCollaboratorsNeeded, setNewCollaboratorsNeeded] = useState(1);
|
const [newCollaboratorsNeeded, setNewCollaboratorsNeeded] = useState(1);
|
||||||
const [newIdea, setNewIdea] = useState('');
|
const [newIdea, setNewIdea] = useState('');
|
||||||
const [newGithub, setNewGithub] = useState('');
|
const [newGithub, setNewGithub] = useState('');
|
||||||
const [newContact, setNewContact] = useState('');
|
const [newContact, setNewContact] = useState(user?.email || '');
|
||||||
const [submittingPost, setSubmittingPost] = useState(false);
|
const [submittingPost, setSubmittingPost] = useState(false);
|
||||||
|
|
||||||
// Application Form
|
// Application Form — auto-populated from Google profile
|
||||||
const [appApplicantName, setAppApplicantName] = useState('');
|
const [appApplicantName, setAppApplicantName] = useState(user?.name || '');
|
||||||
const [appDept, setAppDept] = useState('CSE');
|
const [appDept, setAppDept] = useState('CSE');
|
||||||
const [appYear, setAppYear] = useState('1st Year');
|
const [appYear, setAppYear] = useState('1st Year');
|
||||||
const [appContact, setAppContact] = useState('');
|
const [appContact, setAppContact] = useState(user?.email || '');
|
||||||
const [appMessage, setAppMessage] = useState('');
|
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 [submittingApp, setSubmittingApp] = useState(false);
|
||||||
|
|
||||||
const fetchCollabRequests = () => {
|
const fetchCollabRequests = () => {
|
||||||
@@ -306,10 +320,11 @@ export default function DevCollab() {
|
|||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.03 }}
|
whileHover={{ scale: 1.03 }}
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={() => 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"
|
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' }}
|
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }}
|
||||||
>
|
>
|
||||||
|
{isVerifiedStudent && <ShieldCheck className="w-4 h-4" />}
|
||||||
<Plus className="w-4.5 h-4.5" />
|
<Plus className="w-4.5 h-4.5" />
|
||||||
Post Collaboration Request
|
Post Collaboration Request
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@@ -471,7 +486,7 @@ export default function DevCollab() {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedCollab(item)}
|
onClick={() => requireVerifiedStudent(() => setSelectedCollab(item))}
|
||||||
className="w-full py-2.5 rounded-xl bg-[#FFF7ED] hover:bg-[#F97316] text-[#F97316] hover:text-white border border-[#FED7AA] hover:border-transparent text-xs font-semibold transition-all flex items-center justify-center gap-2 cursor-pointer shadow-xs"
|
className="w-full py-2.5 rounded-xl bg-[#FFF7ED] hover:bg-[#F97316] text-[#F97316] hover:text-white border border-[#FED7AA] hover:border-transparent text-xs font-semibold transition-all flex items-center justify-center gap-2 cursor-pointer shadow-xs"
|
||||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
>
|
>
|
||||||
@@ -494,7 +509,7 @@ export default function DevCollab() {
|
|||||||
Be the first developer to post a request or try adjusting your filters!
|
Be the first developer to post a request or try adjusting your filters!
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPostModal(true)}
|
onClick={() => requireVerifiedStudent(() => setShowPostModal(true))}
|
||||||
className="px-5 py-2.5 rounded-xl bg-[#F97316] text-white text-xs font-semibold hover:bg-[#EA580C] transition-all"
|
className="px-5 py-2.5 rounded-xl bg-[#F97316] text-white text-xs font-semibold hover:bg-[#EA580C] transition-all"
|
||||||
>
|
>
|
||||||
Post First Collaboration Request
|
Post First Collaboration Request
|
||||||
@@ -818,6 +833,96 @@ export default function DevCollab() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* ─── AUTH GATE MODAL (Verified Student Required) ─────────────────────── */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showAuthGate && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setShowAuthGate(false)}
|
||||||
|
className="absolute inset-0 bg-slate-900/60 backdrop-blur-xs"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 30 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 30 }}
|
||||||
|
className="relative w-full max-w-md bg-white rounded-3xl shadow-2xl p-8 z-10 text-center"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAuthGate(false)}
|
||||||
|
className="absolute top-4 right-4 p-2 rounded-full hover:bg-slate-100 text-slate-400 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-orange-100 flex items-center justify-center mx-auto mb-5">
|
||||||
|
<ShieldAlert className="w-8 h-8 text-[#F97316]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
className="text-xl font-bold text-[#1E293B] mb-2"
|
||||||
|
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||||
|
>
|
||||||
|
{!isAuthenticated
|
||||||
|
? 'Sign In Required'
|
||||||
|
: 'Verified Student Account Required'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="text-sm text-[#64748B] mb-6 leading-relaxed"
|
||||||
|
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||||
|
>
|
||||||
|
{!isAuthenticated ? (
|
||||||
|
<>To post or apply for collaboration requests, please sign in with your <strong className="text-[#F97316]">@ritchennai.edu.in</strong> Google account.</>
|
||||||
|
) : (
|
||||||
|
<>You are signed in as <strong>{user?.email}</strong>, which is not a verified RIT college email. Please sign in with your <strong className="text-[#F97316]">@ritchennai.edu.in</strong> email to access DevCollab features.</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{!isAuthenticated ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
loginWithGoogle();
|
||||||
|
setShowAuthGate(false);
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-center gap-2.5 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm shadow-lg"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||||
|
fontFamily: 'Poppins, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogIn className="w-4.5 h-4.5" />
|
||||||
|
Sign In with Google
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
loginWithGoogle();
|
||||||
|
setShowAuthGate(false);
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-center gap-2.5 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm shadow-lg"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||||
|
fontFamily: 'Poppins, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShieldCheck className="w-4.5 h-4.5" />
|
||||||
|
Switch to College Account
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-[11px] text-[#94A3B8]">
|
||||||
|
We only verify your email domain — no personal data is stored beyond your name and email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user