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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
package online.wesal.wesal.dto; package online.wesal.wesal.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Pattern;
public class CreateUserRequest { public class CreateUserRequest {
@Email
@NotBlank @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 @NotBlank
@Size(min = 6) @Size(min = 6)
@ -19,18 +19,18 @@ public class CreateUserRequest {
public CreateUserRequest() {} public CreateUserRequest() {}
public CreateUserRequest(String email, String password, String displayName) { public CreateUserRequest(String phoneNumber, String password, String displayName) {
this.email = email; this.phoneNumber = phoneNumber;
this.password = password; this.password = password;
this.displayName = displayName; this.displayName = displayName;
} }
public String getEmail() { public String getPhoneNumber() {
return email; return phoneNumber;
} }
public void setEmail(String email) { public void setPhoneNumber(String phoneNumber) {
this.email = email; this.phoneNumber = phoneNumber;
} }
public String getPassword() { public String getPassword() {

View File

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

View File

@ -1,9 +1,9 @@
package online.wesal.wesal.entity; package online.wesal.wesal.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Pattern;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -19,9 +19,9 @@ public class User {
private Long id; private Long id;
@Column(unique = true, nullable = false) @Column(unique = true, nullable = false)
@Email
@NotBlank @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) @Column(unique = true)
private String username; private String username;
@ -50,8 +50,8 @@ public class User {
public User() {} public User() {}
public User(String email, String password, String displayName) { public User(String phoneNumber, String password, String displayName) {
this.email = email; this.phoneNumber = phoneNumber;
this.password = password; this.password = password;
this.displayName = displayName; this.displayName = displayName;
} }
@ -64,12 +64,12 @@ public class User {
this.id = id; this.id = id;
} }
public String getEmail() { public String getPhoneNumber() {
return email; return phoneNumber;
} }
public void setEmail(String email) { public void setPhoneNumber(String phoneNumber) {
this.email = email; this.phoneNumber = phoneNumber;
} }
public String getUsername() { public String getUsername() {

View File

@ -11,9 +11,9 @@ import java.util.Optional;
@Repository @Repository
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email); Optional<User> findByPhoneNumber(String phoneNumber);
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
boolean existsByEmail(String email); boolean existsByPhoneNumber(String phoneNumber);
boolean existsByUsername(String username); boolean existsByUsername(String username);
@Query("SELECT u FROM User u WHERE u.subscriptions LIKE %:subscription% AND u.fcmToken IS NOT NULL AND u.fcmToken != ''") @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 @Autowired
private JwtUtil jwtUtil; private JwtUtil jwtUtil;
public String authenticate(String emailOrUsername, String password) { public String authenticate(String phoneNumberOrUsername, String password) {
Optional<User> userOpt = userRepository.findByEmail(emailOrUsername); Optional<User> userOpt = userRepository.findByPhoneNumber(phoneNumberOrUsername);
if (userOpt.isEmpty()) { if (userOpt.isEmpty()) {
userOpt = userRepository.findByUsername(emailOrUsername); userOpt = userRepository.findByUsername(phoneNumberOrUsername);
} }
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
System.out.println("Authenticating user: " + user.getEmail()); System.out.println("Authenticating user: " + user.getPhoneNumber());
if (passwordEncoder.matches(password, user.getPassword())) { if (passwordEncoder.matches(password, user.getPassword())) {
System.out.println("Password matches!"); System.out.println("Password matches!");
if (!user.isActivated()) { if (!user.isActivated()) {
user.setActivated(true); user.setActivated(true);
userRepository.save(user); 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; private SubscriptionNotificationService subscriptionNotificationService;
@Transactional @Transactional
public InvitationResponse createInvitation(CreateInvitationRequest request, String userEmail) { public InvitationResponse createInvitation(CreateInvitationRequest request, String userPhoneNumber) {
User creator = userRepository.findByEmail(userEmail) User creator = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("User not found")); .orElseThrow(() -> new RuntimeException("User not found"));
Tag tag = tagRepository.findById(request.getTagId()) Tag tag = tagRepository.findById(request.getTagId())
@ -85,8 +85,8 @@ public class InvitationService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public CategorizedInvitationsResponse getAllCategorizedInvitations(String userEmail) { public CategorizedInvitationsResponse getAllCategorizedInvitations(String userPhoneNumber) {
User user = userRepository.findByEmail(userEmail) User user = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again")); .orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
List<InvitationResponse> created = invitationRepository.findByCreatorIdOrderByCreationDateDesc(user.getId()) List<InvitationResponse> created = invitationRepository.findByCreatorIdOrderByCreationDateDesc(user.getId())
@ -110,8 +110,8 @@ public class InvitationService {
} }
@Transactional @Transactional
public void acceptInvitation(Long invitationId, String userEmail) { public void acceptInvitation(Long invitationId, String userPhoneNumber) {
User user = userRepository.findByEmail(userEmail) User user = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again")); .orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
Invitation invitation = invitationRepository.findById(invitationId) Invitation invitation = invitationRepository.findById(invitationId)
@ -171,8 +171,8 @@ public class InvitationService {
} }
@Transactional @Transactional
public void cancelInvitation(Long invitationId, String userEmail) { public void cancelInvitation(Long invitationId, String userPhoneNumber) {
User user = userRepository.findByEmail(userEmail) User user = userRepository.findByPhoneNumber(userPhoneNumber)
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again")); .orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
Invitation invitation = invitationRepository.findById(invitationId) Invitation invitation = invitationRepository.findById(invitationId)

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:googleapis_auth/auth_io.dart'; import 'package:googleapis_auth/auth_io.dart';
import 'package:flutter/services.dart' show rootBundle;
class NotificationService { class NotificationService {
static final NotificationService _instance = NotificationService._internal(); static final NotificationService _instance = NotificationService._internal();
@ -17,22 +18,8 @@ class NotificationService {
// Firebase project configuration // Firebase project configuration
static const String _projectId = 'wesalapp-bc676'; static const String _projectId = 'wesalapp-bc676';
// Service account credentials (JSON string) // Service account credentials loaded from file
static const String _serviceAccountJson = ''' static 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"
}
''';
static const List<String> topics = [ static const List<String> topics = [
'all', '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 { Future<String?> _getAccessToken() async {
try { try {
await _loadServiceAccountCredentials();
if (_serviceAccountJson == null) {
print('Service account credentials not loaded');
return null;
}
final credentials = ServiceAccountCredentials.fromJson( final credentials = ServiceAccountCredentials.fromJson(
_serviceAccountJson, _serviceAccountJson!,
); );
final client = await clientViaServiceAccount(credentials, [ final client = await clientViaServiceAccount(credentials, [
'https://www.googleapis.com/auth/firebase.messaging', 'https://www.googleapis.com/auth/firebase.messaging',