feat: add Google OAuth2 Sign-In with verified student badge for DevCollab
This commit is contained in:
@@ -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)
|
||||
private String authorName;
|
||||
|
||||
@Column(name = "author_email")
|
||||
private String authorEmail;
|
||||
|
||||
@Column(nullable = false)
|
||||
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,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:}
|
||||
|
||||
Reference in New Issue
Block a user