feat: add Google OAuth2 Sign-In with verified student badge for DevCollab

This commit is contained in:
Shanmuga Krishnan S M
2026-08-06 12:23:50 +05:30
parent 2be0c04b80
commit 302a39e933
13 changed files with 802 additions and 29 deletions

View File

@@ -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());
}
}

View File

@@ -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;

View 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();
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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:}