feat: singin using phone number backend

This commit is contained in:
sBubshait 2025-08-07 03:20:09 +03:00
parent 1f92121491
commit a74a6a3741
12 changed files with 87 additions and 80 deletions

View File

@ -36,10 +36,10 @@ public class AuthController {
}
@PostMapping("/login")
@Operation(summary = "User login", description = "Authenticate user with email or username and return JWT token")
@Operation(summary = "User login", description = "Authenticate user with phone number or username and return JWT token")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest loginRequest) {
try {
String token = authService.authenticate(loginRequest.getEmailOrUsername(), loginRequest.getPassword());
String token = authService.authenticate(loginRequest.getPhoneNumberOrUsername(), loginRequest.getPassword());
return ResponseEntity.ok(new LoginResponse(token));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
@ -84,7 +84,7 @@ public class AuthController {
// Create user data without password
Map<String, Object> userData = Map.of(
"id", user.getId(),
"email", user.getEmail(),
"phoneNumber", user.getPhoneNumber(),
"username", user.getUsername() != null ? user.getUsername() : "",
"displayName", user.getDisplayName(),
"avatar", user.getAvatar() != null ? user.getAvatar() : "",
@ -111,7 +111,7 @@ public class AuthController {
@Operation(summary = "Create new user (Admin only)", description = "Creates a new user - requires admin privileges")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {
try {
User user = userService.createUser(request.getEmail(), request.getPassword(), request.getDisplayName());
User user = userService.createUser(request.getPhoneNumber(), request.getPassword(), request.getDisplayName());
return ResponseEntity.ok(user);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));

View File

@ -51,8 +51,8 @@ public class InvitationController {
}
try {
String userEmail = authentication.getName();
InvitationResponse response = invitationService.createInvitation(request, userEmail);
String userPhoneNumber = authentication.getName();
InvitationResponse response = invitationService.createInvitation(request, userPhoneNumber);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
@ -87,8 +87,8 @@ public class InvitationController {
@Operation(summary = "Get all categorized invitations", description = "Get invitations in three categories: created, accepted, and available")
public ResponseEntity<ApiResponse<CategorizedInvitationsResponse>> getAllCategorizedInvitations(Authentication authentication) {
try {
String userEmail = authentication.getName();
CategorizedInvitationsResponse response = invitationService.getAllCategorizedInvitations(userEmail);
String userPhoneNumber = authentication.getName();
CategorizedInvitationsResponse response = invitationService.getAllCategorizedInvitations(userPhoneNumber);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
@ -104,8 +104,8 @@ public class InvitationController {
Authentication authentication) {
try {
String userEmail = authentication.getName();
invitationService.acceptInvitation(request.getId(), userEmail);
String userPhoneNumber = authentication.getName();
invitationService.acceptInvitation(request.getId(), userPhoneNumber);
return ResponseEntity.ok(new ApiResponse<>(true));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
@ -121,8 +121,8 @@ public class InvitationController {
Authentication authentication) {
try {
String userEmail = authentication.getName();
invitationService.cancelInvitation(request.getId(), userEmail);
String userPhoneNumber = authentication.getName();
invitationService.cancelInvitation(request.getId(), userPhoneNumber);
return ResponseEntity.ok(new ApiResponse<>(true));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));

View File

@ -89,7 +89,7 @@ public class PostController {
Long targetUserId;
if (id == null) {
// Use authenticated user's ID when no id is provided
String userEmail = authentication.getName();
String userPhoneNumber = authentication.getName();
targetUserId = userService.getCurrentUser().getId();
} else {
if (id <= 0) {

View File

@ -1,14 +1,14 @@
package online.wesal.wesal.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Pattern;
public class CreateUserRequest {
@Email
@NotBlank
private String email;
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number must be in international format starting with + and country code")
private String phoneNumber;
@NotBlank
@Size(min = 6)
@ -19,18 +19,18 @@ public class CreateUserRequest {
public CreateUserRequest() {}
public CreateUserRequest(String email, String password, String displayName) {
this.email = email;
public CreateUserRequest(String phoneNumber, String password, String displayName) {
this.phoneNumber = phoneNumber;
this.password = password;
this.displayName = displayName;
}
public String getEmail() {
return email;
public String getPhoneNumber() {
return phoneNumber;
}
public void setEmail(String email) {
this.email = email;
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getPassword() {

View File

@ -5,24 +5,24 @@ import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank
private String emailOrUsername;
private String phoneNumberOrUsername;
@NotBlank
private String password;
public LoginRequest() {}
public LoginRequest(String emailOrUsername, String password) {
this.emailOrUsername = emailOrUsername;
public LoginRequest(String phoneNumberOrUsername, String password) {
this.phoneNumberOrUsername = phoneNumberOrUsername;
this.password = password;
}
public String getEmailOrUsername() {
return emailOrUsername;
public String getPhoneNumberOrUsername() {
return phoneNumberOrUsername;
}
public void setEmailOrUsername(String emailOrUsername) {
this.emailOrUsername = emailOrUsername;
public void setPhoneNumberOrUsername(String phoneNumberOrUsername) {
this.phoneNumberOrUsername = phoneNumberOrUsername;
}
public String getPassword() {

View File

@ -1,9 +1,9 @@
package online.wesal.wesal.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Pattern;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -19,9 +19,9 @@ public class User {
private Long id;
@Column(unique = true, nullable = false)
@Email
@NotBlank
private String email;
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number must be in international format starting with + and country code")
private String phoneNumber;
@Column(unique = true)
private String username;
@ -50,8 +50,8 @@ public class User {
public User() {}
public User(String email, String password, String displayName) {
this.email = email;
public User(String phoneNumber, String password, String displayName) {
this.phoneNumber = phoneNumber;
this.password = password;
this.displayName = displayName;
}
@ -64,12 +64,12 @@ public class User {
this.id = id;
}
public String getEmail() {
return email;
public String getPhoneNumber() {
return phoneNumber;
}
public void setEmail(String email) {
this.email = email;
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getUsername() {

View File

@ -11,9 +11,9 @@ import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByPhoneNumber(String phoneNumber);
Optional<User> findByUsername(String username);
boolean existsByEmail(String email);
boolean existsByPhoneNumber(String phoneNumber);
boolean existsByUsername(String username);
@Query("SELECT u FROM User u WHERE u.subscriptions LIKE %:subscription% AND u.fcmToken IS NOT NULL AND u.fcmToken != ''")

View File

@ -21,23 +21,23 @@ public class AuthService {
@Autowired
private JwtUtil jwtUtil;
public String authenticate(String emailOrUsername, String password) {
Optional<User> userOpt = userRepository.findByEmail(emailOrUsername);
public String authenticate(String phoneNumberOrUsername, String password) {
Optional<User> userOpt = userRepository.findByPhoneNumber(phoneNumberOrUsername);
if (userOpt.isEmpty()) {
userOpt = userRepository.findByUsername(emailOrUsername);
userOpt = userRepository.findByUsername(phoneNumberOrUsername);
}
if (userOpt.isPresent()) {
User user = userOpt.get();
System.out.println("Authenticating user: " + user.getEmail());
System.out.println("Authenticating user: " + user.getPhoneNumber());
if (passwordEncoder.matches(password, user.getPassword())) {
System.out.println("Password matches!");
if (!user.isActivated()) {
user.setActivated(true);
userRepository.save(user);
}
return jwtUtil.generateToken(user.getEmail());
return jwtUtil.generateToken(user.getPhoneNumber());
}
}

View File

@ -42,8 +42,8 @@ public class InvitationService {
private SubscriptionNotificationService subscriptionNotificationService;
@Transactional
public InvitationResponse createInvitation(CreateInvitationRequest request, String userEmail) {
User creator = userRepository.findByEmail(userEmail)
public InvitationResponse createInvitation(CreateInvitationRequest request, String userPhoneNumber) {
User creator = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("User not found"));
Tag tag = tagRepository.findById(request.getTagId())
@ -85,8 +85,8 @@ public class InvitationService {
.collect(Collectors.toList());
}
public CategorizedInvitationsResponse getAllCategorizedInvitations(String userEmail) {
User user = userRepository.findByEmail(userEmail)
public CategorizedInvitationsResponse getAllCategorizedInvitations(String userPhoneNumber) {
User user = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
List<InvitationResponse> created = invitationRepository.findByCreatorIdOrderByCreationDateDesc(user.getId())
@ -110,8 +110,8 @@ public class InvitationService {
}
@Transactional
public void acceptInvitation(Long invitationId, String userEmail) {
User user = userRepository.findByEmail(userEmail)
public void acceptInvitation(Long invitationId, String userPhoneNumber) {
User user = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
Invitation invitation = invitationRepository.findById(invitationId)
@ -171,8 +171,8 @@ public class InvitationService {
}
@Transactional
public void cancelInvitation(Long invitationId, String userEmail) {
User user = userRepository.findByEmail(userEmail)
public void cancelInvitation(Long invitationId, String userPhoneNumber) {
User user = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
Invitation invitation = invitationRepository.findById(invitationId)

View File

@ -20,15 +20,15 @@ public class UserDetailsServiceImpl implements UserDetailsService {
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException {
User user = userRepository.findByPhoneNumber(phoneNumber)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + phoneNumber));
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPhoneNumber(),
user.getPassword(),
authorities
);

View File

@ -20,8 +20,8 @@ public class UserService {
public User getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String email = authentication.getName();
return userRepository.findByEmail(email)
String phoneNumber = authentication.getName();
return userRepository.findByPhoneNumber(phoneNumber)
.orElseThrow(() -> new RuntimeException("User not found"));
}
@ -44,16 +44,16 @@ public class UserService {
return "ADMIN".equals(user.getRole());
}
public User createUser(String email, String password, String displayName) {
public User createUser(String phoneNumber, String password, String displayName) {
if (!isCurrentUserAdmin()) {
throw new RuntimeException("Access denied: Admin privileges required");
}
if (userRepository.existsByEmail(email)) {
throw new RuntimeException("User with this email already exists");
if (userRepository.existsByPhoneNumber(phoneNumber)) {
throw new RuntimeException("User with this phone number already exists");
}
User user = new User(email, passwordEncoder.encode(password), displayName);
User user = new User(phoneNumber, passwordEncoder.encode(password), displayName);
return userRepository.save(user);
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:googleapis_auth/auth_io.dart';
import 'package:flutter/services.dart' show rootBundle;
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
@ -17,22 +18,8 @@ class NotificationService {
// Firebase project configuration
static const String _projectId = 'wesalapp-bc676';
// Service account credentials (JSON string)
static const String _serviceAccountJson = '''
{
"type": "service_account",
"project_id": "wesalapp-bc676",
"private_key_id": "90f1ac73e8f8b59e5ad7b0caed638ffa57675d39",
"private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCe/p4CkGeDft2F\\nLMDNyjbU0g1mLQ4bhaqy2WOHr8BX7kHt/WlpB3WeaSUItW2cXFtXhI66mW7Ejlg7\\n/Los4YLpVXner4P5Vj7sOVrhD1Jt9qmdsEjfsTaKs0t06tfUE9PdFqBl6F5HG9B1\\nkwdV2mQfiurpTb+zUXLpfvMbGT1ny7gJSXhYpRaC6UJUy7DNsxeChviXy1tPGr/r\\nmIaENt0KAZzRSSCp0bzN+gKoAm6qyOGYJ9++HlGXTDYmkGPkdc0lbSWjA2/+K7zR\\nWQNy4oOXCstdHL4lMp87UT8lL1ZnntROELyTBUslmYywxXtZinkOgBrWeUifqLW4\\nbcHy6jJ5AgMBAAECggEACSBwjZEggAneSXDCOI3tC9Zq8nyPnMDVhaK49eb+0Y1Z\\nt4GedWr6M3exqohPnHQowiNX1hpMo3fQVNEzFrRzQVWow0Gr/7oVrpW0Q8sPXkSU\\ng/rElCKmENwt7q40aXYh6UUNPAxUrRxJoRYpi6IXsT/WMEJISNDaGdExv1J5leWi\\no8Op2AhREV/ukFpTnNfzfWKjXN+i3psNCYqZAdAh+a4ZJH0vNpiaCq6uVFw7HzdR\\nF2mMha+MYtp2VupzDJ8DkL4ExQl1KCOCllahzqVrhqmEhhhTfDxPOj5q24Hnfn1p\\npzR+fC8Ex0dGB/j+9jKjQyilo/LzEJdrPxt/9QUdiQKBgQDQ2L7sQsEIihVUq3lW\\n3Od2GNjnloTo24rNLl8hxwgl9EA2VnG50gZPGAkcUA2eeA23T6C6gPbm0QBsfqSP\\nPNTbd6UYF508EE7PMaScFoJMcKDin8x4q5tfVjgao2r8LCOUXfU1ktreQR3bIMKk\\nsgsgBazfBT84ioxvDwoD+4EJqwKBgQDC5HEfouCTxvKwzmpxZ+kkHKPO5QjyxnOH\\nLnt/7jx5t7T1nWNUnusYj+uowaqKAmLz7kBhCbRGADdKuBAr0hY/aEOG3zhTH35K\\nc+8wJ3yDFkh8BhFsOYCxopIPAjEGxw5cnM4+r8QDqy61j4hsR9kSr40WwhRuSxu+\\nHqe38Vl4awKBgBYFJGxIxY2e8YzR36NW+1iqWgRhDHZ433Ou1fz7vVIzJKoWBzuu\\nd1fTkvJXRnhU9C1Fyg6gFmhT1RWbbMJliZPyU4fsxXlVxtl1xINopChnH6+FZcu7\\nXFB7CMNWQ6t/A+la1sXlTApvFzTJiXxQAXhI4OdK6FWP1irHjSjKVdqtAoGAcLQA\\ngyYCrxKux/YmcfyAQ2TYic3DNfnzVypXOuz/RfgpipwAlC/ujl60Dfwo7fRhWuTd\\nkAA3ov9++hOlLmIogXR/EGDHxrIAq3eNy5AaHghl1GsB6k76kD8OLeW7ikrUkFQR\\npQip1uFIerBNWSjXbEne0llbzUhb+77oiKPmdI8CgYBRBIUC4d/T3cgmF9uRqlpL\\nSsa8IxqohU1huAucf5UqNP9OXZ4TtZhM0PbR5SNHcGBXAl+XowCtUeCE9LlWWzpg\\ne/xTu4Mu1hwnRZ8ybujAyTPnN8KEfK8HDjORZnxzzdyPkO4BN+KOH6WZyKhKDTuR\\n6KCch1ooA8YlV43vchpKXg==\\n-----END PRIVATE KEY-----\\n",
"client_email": "firebase-adminsdk-fbsvc@wesalapp-bc676.iam.gserviceaccount.com",
"client_id": "112586481303824467416",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40wesalapp-bc676.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
''';
// Service account credentials loaded from file
static String? _serviceAccountJson;
static const List<String> topics = [
'all',
@ -257,10 +244,30 @@ class NotificationService {
}
}
Future<void> _loadServiceAccountCredentials() async {
if (_serviceAccountJson != null) return;
try {
_serviceAccountJson = await rootBundle.loadString(
'firebase-service-account.json',
);
} catch (e) {
print('Error loading service account credentials: $e');
_serviceAccountJson = null;
}
}
Future<String?> _getAccessToken() async {
try {
await _loadServiceAccountCredentials();
if (_serviceAccountJson == null) {
print('Service account credentials not loaded');
return null;
}
final credentials = ServiceAccountCredentials.fromJson(
_serviceAccountJson,
_serviceAccountJson!,
);
final client = await clientViaServiceAccount(credentials, [
'https://www.googleapis.com/auth/firebase.messaging',