diff --git a/backend/src/main/java/online/wesal/wesal/controller/AuthController.java b/backend/src/main/java/online/wesal/wesal/controller/AuthController.java index 071fc4b..ffe7b13 100644 --- a/backend/src/main/java/online/wesal/wesal/controller/AuthController.java +++ b/backend/src/main/java/online/wesal/wesal/controller/AuthController.java @@ -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 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 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())); diff --git a/backend/src/main/java/online/wesal/wesal/controller/InvitationController.java b/backend/src/main/java/online/wesal/wesal/controller/InvitationController.java index 425fcfc..c329365 100644 --- a/backend/src/main/java/online/wesal/wesal/controller/InvitationController.java +++ b/backend/src/main/java/online/wesal/wesal/controller/InvitationController.java @@ -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> 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())); diff --git a/backend/src/main/java/online/wesal/wesal/controller/PostController.java b/backend/src/main/java/online/wesal/wesal/controller/PostController.java index 14f686a..2d3c19b 100644 --- a/backend/src/main/java/online/wesal/wesal/controller/PostController.java +++ b/backend/src/main/java/online/wesal/wesal/controller/PostController.java @@ -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) { diff --git a/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java b/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java index eada67c..e487661 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java +++ b/backend/src/main/java/online/wesal/wesal/dto/CreateUserRequest.java @@ -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() { diff --git a/backend/src/main/java/online/wesal/wesal/dto/LoginRequest.java b/backend/src/main/java/online/wesal/wesal/dto/LoginRequest.java index be8c25e..a086e04 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/LoginRequest.java +++ b/backend/src/main/java/online/wesal/wesal/dto/LoginRequest.java @@ -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() { diff --git a/backend/src/main/java/online/wesal/wesal/entity/User.java b/backend/src/main/java/online/wesal/wesal/entity/User.java index 3740136..0d5333a 100644 --- a/backend/src/main/java/online/wesal/wesal/entity/User.java +++ b/backend/src/main/java/online/wesal/wesal/entity/User.java @@ -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() { diff --git a/backend/src/main/java/online/wesal/wesal/repository/UserRepository.java b/backend/src/main/java/online/wesal/wesal/repository/UserRepository.java index 308181e..69c1737 100644 --- a/backend/src/main/java/online/wesal/wesal/repository/UserRepository.java +++ b/backend/src/main/java/online/wesal/wesal/repository/UserRepository.java @@ -11,9 +11,9 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByPhoneNumber(String phoneNumber); Optional 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 != ''") diff --git a/backend/src/main/java/online/wesal/wesal/service/AuthService.java b/backend/src/main/java/online/wesal/wesal/service/AuthService.java index 3a0dabc..bfc49a1 100644 --- a/backend/src/main/java/online/wesal/wesal/service/AuthService.java +++ b/backend/src/main/java/online/wesal/wesal/service/AuthService.java @@ -21,23 +21,23 @@ public class AuthService { @Autowired private JwtUtil jwtUtil; - public String authenticate(String emailOrUsername, String password) { - Optional userOpt = userRepository.findByEmail(emailOrUsername); + public String authenticate(String phoneNumberOrUsername, String password) { + Optional 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()); } } diff --git a/backend/src/main/java/online/wesal/wesal/service/InvitationService.java b/backend/src/main/java/online/wesal/wesal/service/InvitationService.java index 3617d8b..ce749d7 100644 --- a/backend/src/main/java/online/wesal/wesal/service/InvitationService.java +++ b/backend/src/main/java/online/wesal/wesal/service/InvitationService.java @@ -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 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) diff --git a/backend/src/main/java/online/wesal/wesal/service/UserDetailsServiceImpl.java b/backend/src/main/java/online/wesal/wesal/service/UserDetailsServiceImpl.java index ce92452..372d18a 100644 --- a/backend/src/main/java/online/wesal/wesal/service/UserDetailsServiceImpl.java +++ b/backend/src/main/java/online/wesal/wesal/service/UserDetailsServiceImpl.java @@ -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 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 ); diff --git a/backend/src/main/java/online/wesal/wesal/service/UserService.java b/backend/src/main/java/online/wesal/wesal/service/UserService.java index d9c5be2..e16dbd7 100644 --- a/backend/src/main/java/online/wesal/wesal/service/UserService.java +++ b/backend/src/main/java/online/wesal/wesal/service/UserService.java @@ -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); } diff --git a/frontend/lib/services/notification_service.dart b/frontend/lib/services/notification_service.dart index 952c79e..455c5b1 100644 --- a/frontend/lib/services/notification_service.dart +++ b/frontend/lib/services/notification_service.dart @@ -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 topics = [ 'all', @@ -257,10 +244,30 @@ class NotificationService { } } + Future _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 _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',