Merge pull request #16 from sBubshait/feature/phone-signin
Feature/phone signin
This commit is contained in:
commit
52ddacd31c
@ -55,7 +55,7 @@ public class SecurityConfig {
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/", "/login", "/swagger-ui/**", "/v3/api-docs/**", "/docs/**", "/docs").permitAll()
|
||||
.requestMatchers("/", "/login", "/checkInvitation", "/register", "/swagger-ui/**", "/v3/api-docs/**", "/docs/**", "/docs").permitAll()
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
@ -8,9 +8,15 @@ import online.wesal.wesal.dto.LoginRequest;
|
||||
import online.wesal.wesal.dto.LoginResponse;
|
||||
import online.wesal.wesal.dto.UpdateUserRequest;
|
||||
import online.wesal.wesal.dto.UsernameSelectionRequest;
|
||||
import online.wesal.wesal.dto.CheckInvitationRequest;
|
||||
import online.wesal.wesal.dto.RegisterRequest;
|
||||
import online.wesal.wesal.dto.RegisterResponse;
|
||||
import online.wesal.wesal.entity.User;
|
||||
import online.wesal.wesal.entity.InvitationCode;
|
||||
import online.wesal.wesal.service.AuthService;
|
||||
import online.wesal.wesal.service.UserService;
|
||||
import online.wesal.wesal.service.InvitationCodeService;
|
||||
import online.wesal.wesal.config.JwtUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@ -29,6 +35,12 @@ public class AuthController {
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private InvitationCodeService invitationCodeService;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@GetMapping("/")
|
||||
@Operation(summary = "Health check", description = "Returns API status")
|
||||
public ResponseEntity<Map<String, Integer>> healthCheck() {
|
||||
@ -36,10 +48,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 +96,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() : "",
|
||||
@ -107,14 +119,105 @@ public class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/checkInvitation")
|
||||
@Operation(summary = "Check invitation code", description = "Validates if an invitation code is valid and not expired")
|
||||
public ResponseEntity<Map<String, Object>> checkInvitation(@Valid @RequestBody CheckInvitationRequest request) {
|
||||
try {
|
||||
boolean isValid = invitationCodeService.isValidCode(request.getCode());
|
||||
if (isValid) {
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", true,
|
||||
"message", "Invitation code is valid",
|
||||
"data", Map.of("code", request.getCode(), "valid", true)
|
||||
));
|
||||
} else {
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", false,
|
||||
"message", "Invalid or expired invitation code",
|
||||
"data", Map.of("code", request.getCode(), "valid", false)
|
||||
));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", false,
|
||||
"message", "Error validating invitation code"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "Register new user", description = "Register a new user with invitation code, phone number, and password")
|
||||
public ResponseEntity<Map<String, Object>> register(@Valid @RequestBody RegisterRequest request) {
|
||||
try {
|
||||
User user = invitationCodeService.registerUserWithCode(
|
||||
request.getCode(),
|
||||
request.getPhoneNumber(),
|
||||
request.getPassword()
|
||||
);
|
||||
|
||||
// Auto-login: generate JWT token
|
||||
String token = jwtUtil.generateToken(user.getPhoneNumber());
|
||||
|
||||
RegisterResponse registerResponse = new RegisterResponse(user, token);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", true,
|
||||
"message", "User registered successfully",
|
||||
"data", registerResponse
|
||||
));
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
String message;
|
||||
if (e.getMessage().contains("already exists")) {
|
||||
message = "A user with this phone number already exists";
|
||||
} else if (e.getMessage().contains("Invalid or expired")) {
|
||||
message = "Invalid or expired invitation code";
|
||||
} else {
|
||||
message = "Registration failed. Please try again";
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", false,
|
||||
"message", message
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/admin/createUser")
|
||||
@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()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/admin/createInvitationCode")
|
||||
@Operation(summary = "Create invitation code (Admin only)", description = "Generates a new invitation code - requires admin privileges")
|
||||
public ResponseEntity<Map<String, Object>> createInvitationCode() {
|
||||
try {
|
||||
InvitationCode invitationCode = invitationCodeService.generateInvitationCode();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", true,
|
||||
"message", "Invitation code created successfully",
|
||||
"data", invitationCode
|
||||
));
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
String message;
|
||||
if (e.getMessage().contains("Unable to generate unique")) {
|
||||
message = "Failed to generate unique invitation code. Please try again.";
|
||||
} else {
|
||||
message = "Failed to create invitation code. Please try again.";
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", false,
|
||||
"message", message
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()));
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
package online.wesal.wesal.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class CheckInvitationRequest {
|
||||
|
||||
@NotBlank
|
||||
private String code;
|
||||
|
||||
public CheckInvitationRequest() {}
|
||||
|
||||
public CheckInvitationRequest(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
package online.wesal.wesal.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class RegisterRequest {
|
||||
|
||||
@NotBlank
|
||||
private String code;
|
||||
|
||||
@NotBlank
|
||||
@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)
|
||||
private String password;
|
||||
|
||||
public RegisterRequest() {}
|
||||
|
||||
public RegisterRequest(String code, String phoneNumber, String password) {
|
||||
this.code = code;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public void setPhoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package online.wesal.wesal.dto;
|
||||
|
||||
import online.wesal.wesal.entity.User;
|
||||
|
||||
public class RegisterResponse {
|
||||
|
||||
private User user;
|
||||
private String token;
|
||||
|
||||
public RegisterResponse() {}
|
||||
|
||||
public RegisterResponse(User user, String token) {
|
||||
this.user = user;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package online.wesal.wesal.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "invitation_codes")
|
||||
public class InvitationCode {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
@NotBlank
|
||||
@Size(max = 200)
|
||||
private String code;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime expirationDate;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
public InvitationCode() {
|
||||
this.createdDate = LocalDateTime.now();
|
||||
this.expirationDate = LocalDateTime.now().plusDays(30);
|
||||
}
|
||||
|
||||
public InvitationCode(String code) {
|
||||
this();
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public LocalDateTime getExpirationDate() {
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
public void setExpirationDate(LocalDateTime expirationDate) {
|
||||
this.expirationDate = expirationDate;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(LocalDateTime createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return LocalDateTime.now().isAfter(this.expirationDate);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -46,12 +46,12 @@ public class User {
|
||||
private String role = "USER";
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String subscriptions = "[\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
||||
private String subscriptions = "[\"postcomments\",\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
||||
|
||||
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() {
|
||||
@ -130,13 +130,13 @@ public class User {
|
||||
|
||||
public List<String> getSubscriptions() {
|
||||
if (subscriptions == null || subscriptions.isEmpty()) {
|
||||
return Arrays.asList("newinvites", "invitesfollowup", "appnews");
|
||||
return Arrays.asList("postcomments", "newinvites", "invitesfollowup", "appnews");
|
||||
}
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(subscriptions, new TypeReference<List<String>>() {});
|
||||
} catch (Exception e) {
|
||||
return Arrays.asList("newinvites", "invitesfollowup", "appnews");
|
||||
return Arrays.asList("postcomments", "newinvites", "invitesfollowup", "appnews");
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +145,7 @@ public class User {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
this.subscriptions = mapper.writeValueAsString(subscriptions);
|
||||
} catch (Exception e) {
|
||||
this.subscriptions = "[\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
||||
this.subscriptions = "[\"postcomments\",\"newinvites\",\"invitesfollowup\",\"appnews\"]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package online.wesal.wesal.repository;
|
||||
|
||||
import online.wesal.wesal.entity.InvitationCode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface InvitationCodeRepository extends JpaRepository<InvitationCode, Long> {
|
||||
Optional<InvitationCode> findByCode(String code);
|
||||
boolean existsByCode(String code);
|
||||
|
||||
@Query("SELECT ic FROM InvitationCode ic WHERE ic.code = :code AND ic.expirationDate > :currentTime")
|
||||
Optional<InvitationCode> findValidCodeByCode(@Param("code") String code, @Param("currentTime") LocalDateTime currentTime);
|
||||
}
|
||||
@ -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 != ''")
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
package online.wesal.wesal.service;
|
||||
|
||||
import online.wesal.wesal.entity.InvitationCode;
|
||||
import online.wesal.wesal.entity.User;
|
||||
import online.wesal.wesal.repository.InvitationCodeRepository;
|
||||
import online.wesal.wesal.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class InvitationCodeService {
|
||||
|
||||
@Autowired
|
||||
private InvitationCodeRepository invitationCodeRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
public boolean isValidCode(String code) {
|
||||
Optional<InvitationCode> invitationCodeOpt = invitationCodeRepository.findValidCodeByCode(code, LocalDateTime.now());
|
||||
return invitationCodeOpt.isPresent();
|
||||
}
|
||||
|
||||
public Optional<InvitationCode> findValidCode(String code) {
|
||||
return invitationCodeRepository.findValidCodeByCode(code, LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public User registerUserWithCode(String code, String phoneNumber, String password) {
|
||||
// Check if phone number already exists
|
||||
if (userRepository.existsByPhoneNumber(phoneNumber)) {
|
||||
throw new RuntimeException("User with this phone number already exists");
|
||||
}
|
||||
|
||||
// Validate invitation code
|
||||
Optional<InvitationCode> invitationCodeOpt = findValidCode(code);
|
||||
if (invitationCodeOpt.isEmpty()) {
|
||||
throw new RuntimeException("Invalid or expired invitation code");
|
||||
}
|
||||
|
||||
InvitationCode invitationCode = invitationCodeOpt.get();
|
||||
|
||||
// Create new user
|
||||
User user = new User();
|
||||
user.setPhoneNumber(phoneNumber);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setDisplayName("Guest");
|
||||
user.setRole("USER");
|
||||
user.setActivated(false);
|
||||
user.setSubscriptions(Arrays.asList("postcomments", "newinvites", "invitesfollowup", "appnews"));
|
||||
|
||||
// Save user
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
// Remove (delete) the invitation code so it can't be used again
|
||||
invitationCodeRepository.delete(invitationCode);
|
||||
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
public InvitationCode generateInvitationCode() {
|
||||
String code;
|
||||
int attempts = 0;
|
||||
int maxAttempts = 100;
|
||||
|
||||
// Generate a unique 6-character code
|
||||
do {
|
||||
code = generateRandomCode();
|
||||
attempts++;
|
||||
if (attempts > maxAttempts) {
|
||||
throw new RuntimeException("Unable to generate unique invitation code after " + maxAttempts + " attempts");
|
||||
}
|
||||
} while (invitationCodeRepository.existsByCode(code));
|
||||
|
||||
InvitationCode invitationCode = new InvitationCode(code);
|
||||
return invitationCodeRepository.save(invitationCode);
|
||||
}
|
||||
|
||||
private String generateRandomCode() {
|
||||
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
Random random = new Random();
|
||||
StringBuilder code = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
code.append(characters.charAt(random.nextInt(characters.length())));
|
||||
}
|
||||
|
||||
return code.toString();
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ class ApiConstants {
|
||||
|
||||
// Auth endpoints
|
||||
static const String loginEndpoint = '/login';
|
||||
static const String registerEndpoint = '/register';
|
||||
static const String checkInvitationEndpoint = '/checkInvitation';
|
||||
|
||||
// User endpoints
|
||||
static const String getUserEndpoint = '/getUser';
|
||||
|
||||
@ -7,6 +7,8 @@ import 'screens/edit_profile_screen.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'services/user_service.dart';
|
||||
import 'services/app_lifecycle_service.dart';
|
||||
import 'services/http_service.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@ -260,10 +262,14 @@ class SignInPage extends StatefulWidget {
|
||||
|
||||
class _SignInPageState extends State<SignInPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _invitationCodeController = TextEditingController();
|
||||
final _countryCodeController = TextEditingController(text: '+966');
|
||||
bool _isPasswordVisible = false;
|
||||
bool _isLoading = false;
|
||||
bool _isRegistering = false;
|
||||
int _registrationStep = 1; // 1 = invitation code, 2 = phone & password
|
||||
|
||||
void _showHelpBottomSheet() {
|
||||
showModalBottomSheet(
|
||||
@ -297,7 +303,7 @@ class _SignInPageState extends State<SignInPage> {
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'For account creation or password reset, please contact ERP Management Group from your Aramco email address.',
|
||||
'Registration is by invitation only. If you don\'t have an invitation code, please contact COD/DPSD.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
@ -331,10 +337,51 @@ class _SignInPageState extends State<SignInPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _checkInvitationCode() async {
|
||||
if (_invitationCodeController.text.trim().isEmpty) {
|
||||
_showErrorAlert('Please enter your invitation code');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await HttpService.post('/checkInvitation', {
|
||||
'code': _invitationCodeController.text.trim().toUpperCase(),
|
||||
});
|
||||
|
||||
final data = json.decode(response.body);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
if (data['status'] == true &&
|
||||
data['data'] != null &&
|
||||
data['data']['valid'] == true) {
|
||||
// Invitation code is valid, move to step 2
|
||||
setState(() {
|
||||
_registrationStep = 2;
|
||||
});
|
||||
} else {
|
||||
_showErrorAlert(data['message'] ?? 'Invalid invitation code');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
_showErrorAlert('Network error. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_phoneController.dispose();
|
||||
_passwordController.dispose();
|
||||
_invitationCodeController.dispose();
|
||||
_countryCodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -344,8 +391,10 @@ class _SignInPageState extends State<SignInPage> {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
final phoneNumber =
|
||||
_countryCodeController.text.trim() + _phoneController.text.trim();
|
||||
final result = await AuthService.login(
|
||||
_emailController.text.trim(),
|
||||
phoneNumber,
|
||||
_passwordController.text,
|
||||
);
|
||||
|
||||
@ -386,11 +435,83 @@ class _SignInPageState extends State<SignInPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleRegister() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final phoneNumber =
|
||||
_countryCodeController.text.trim() + _phoneController.text.trim();
|
||||
|
||||
final response = await HttpService.post('/register', {
|
||||
'code': _invitationCodeController.text.trim().toUpperCase(),
|
||||
'phoneNumber': phoneNumber,
|
||||
'password': _passwordController.text,
|
||||
});
|
||||
|
||||
final data = json.decode(response.body);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
if (data['status'] == true && data['data'] != null) {
|
||||
// Registration successful, save token and navigate
|
||||
final token = data['data']['token'];
|
||||
if (token != null) {
|
||||
await AuthService.saveToken(token);
|
||||
|
||||
final userResult = await UserService.getCurrentUser(
|
||||
forceRefresh: true,
|
||||
);
|
||||
if (userResult['success'] == true) {
|
||||
final userData = userResult['data'];
|
||||
|
||||
// Check if user needs onboarding (activated = false)
|
||||
if (userData['activated'] == false) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EditProfileScreen(
|
||||
userData: userData,
|
||||
isOnboarding: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
AppLifecycleService.startAllPolling();
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationPermissionScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showErrorAlert(
|
||||
'Registration successful but failed to load user data',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showErrorAlert('Registration successful but no token received');
|
||||
}
|
||||
} else {
|
||||
_showErrorAlert(data['message'] ?? 'Registration failed');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
_showErrorAlert('Network error. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorAlert(String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Login Failed'),
|
||||
title: Text(_isRegistering ? 'Registration' : 'Login Failed'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
@ -430,7 +551,7 @@ class _SignInPageState extends State<SignInPage> {
|
||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
'Sign In',
|
||||
_isRegistering ? 'Register' : 'Sign In',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -482,7 +603,9 @@ class _SignInPageState extends State<SignInPage> {
|
||||
|
||||
// Welcome text
|
||||
Text(
|
||||
'Welcome Back!',
|
||||
_isRegistering
|
||||
? 'Create Account'
|
||||
: 'Welcome Back!',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -494,7 +617,9 @@ class _SignInPageState extends State<SignInPage> {
|
||||
SizedBox(height: 8),
|
||||
|
||||
Text(
|
||||
'Sign in to socialize with your colleagues\nand transform your social life!',
|
||||
_isRegistering
|
||||
? 'Join with an invitation code to connect\nwith your colleagues!'
|
||||
: 'Sign in to socialize with your colleagues\nand transform your social life!',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
@ -504,13 +629,15 @@ class _SignInPageState extends State<SignInPage> {
|
||||
|
||||
SizedBox(height: 40),
|
||||
|
||||
// Email field
|
||||
// Registration Step 1: Invitation Code
|
||||
if (_isRegistering && _registrationStep == 1) ...[
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
controller: _invitationCodeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: Icon(Icons.email_outlined),
|
||||
labelText: 'Invitation Code',
|
||||
prefixIcon: Icon(
|
||||
Icons.confirmation_number_outlined,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -520,21 +647,107 @@ class _SignInPageState extends State<SignInPage> {
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
helperText:
|
||||
'6-character code with letters and numbers',
|
||||
),
|
||||
maxLength: 6,
|
||||
textCapitalization:
|
||||
TextCapitalization.characters,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your email';
|
||||
return 'Please enter your invitation code';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Please enter a valid email';
|
||||
if (value.length != 6) {
|
||||
return 'Invitation code must be 6 characters';
|
||||
}
|
||||
if (!RegExp(
|
||||
r'^[A-Z0-9]{6}$',
|
||||
).hasMatch(value)) {
|
||||
return 'Code must contain only letters and numbers';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
// Login or Registration Step 2: Phone & Password
|
||||
if (!_isRegistering || _registrationStep == 2) ...[
|
||||
// Phone number field with country code input
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextFormField(
|
||||
controller: _countryCodeController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Code',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Required';
|
||||
}
|
||||
if (!value.startsWith('+')) {
|
||||
return 'Must start with +';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Phone Number',
|
||||
prefixIcon: Icon(Icons.phone_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your phone number';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Please enter a valid phone number';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// Password field (only for login or registration step 2)
|
||||
if (!_isRegistering || _registrationStep == 2) ...[
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Password field
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: !_isPasswordVisible,
|
||||
@ -549,7 +762,8 @@ class _SignInPageState extends State<SignInPage> {
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isPasswordVisible = !_isPasswordVisible;
|
||||
_isPasswordVisible =
|
||||
!_isPasswordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -573,10 +787,12 @@ class _SignInPageState extends State<SignInPage> {
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
SizedBox(height: 12),
|
||||
|
||||
// Forgot password
|
||||
// Forgot password (only show for login)
|
||||
if (!_isRegistering)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
@ -593,11 +809,21 @@ class _SignInPageState extends State<SignInPage> {
|
||||
|
||||
SizedBox(height: 30),
|
||||
|
||||
// Sign in button
|
||||
// Action button based on current state
|
||||
Container(
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleSignIn,
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () {
|
||||
if (!_isRegistering) {
|
||||
_handleSignIn();
|
||||
} else if (_registrationStep == 1) {
|
||||
_checkInvitationCode();
|
||||
} else {
|
||||
_handleRegister();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
foregroundColor: Colors.white,
|
||||
@ -614,7 +840,11 @@ class _SignInPageState extends State<SignInPage> {
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Sign In',
|
||||
!_isRegistering
|
||||
? 'Sign In'
|
||||
: (_registrationStep == 1
|
||||
? 'Continue'
|
||||
: 'Register'),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -623,20 +853,65 @@ class _SignInPageState extends State<SignInPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Back button for registration step 2
|
||||
if (_isRegistering && _registrationStep == 2) ...[
|
||||
SizedBox(height: 16),
|
||||
Container(
|
||||
height: 56,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_registrationStep = 1;
|
||||
_phoneController.clear();
|
||||
_passwordController.clear();
|
||||
});
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: Color(0xFF6A4C93)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Back to Invitation Code',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Contact link for new accounts
|
||||
// Toggle between login and registration (only show on step 1)
|
||||
if (!_isRegistering || _registrationStep == 1)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Don't have an account? ",
|
||||
_isRegistering
|
||||
? 'Already have an account? '
|
||||
: "Don't have an account? ",
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _showHelpBottomSheet,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isRegistering = !_isRegistering;
|
||||
_registrationStep = 1;
|
||||
// Clear form when switching modes
|
||||
_phoneController.clear();
|
||||
_passwordController.clear();
|
||||
_invitationCodeController.clear();
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Contact Support',
|
||||
_isRegistering
|
||||
? 'Sign In'
|
||||
: 'Register Now!',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF6A4C93),
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -646,6 +921,19 @@ class _SignInPageState extends State<SignInPage> {
|
||||
],
|
||||
),
|
||||
|
||||
if (_isRegistering) ...[
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Registration is only by invitation.\nIf you don\'t have an invite, contact COD/DPSD.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
|
||||
368
frontend/lib/screens/create_invitation_screen.dart
Normal file
368
frontend/lib/screens/create_invitation_screen.dart
Normal file
@ -0,0 +1,368 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:convert';
|
||||
import '../services/http_service.dart';
|
||||
import '../constants/api_constants.dart';
|
||||
|
||||
class CreateInvitationScreen extends StatefulWidget {
|
||||
@override
|
||||
_CreateInvitationScreenState createState() => _CreateInvitationScreenState();
|
||||
}
|
||||
|
||||
class _CreateInvitationScreenState extends State<CreateInvitationScreen> {
|
||||
bool _isLoading = false;
|
||||
Map<String, dynamic>? _createdInvitation;
|
||||
|
||||
Future<void> _createInvitationCode() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await HttpService.post(ApiConstants.createInvitationCodeEndpoint, {});
|
||||
final data = json.decode(response.body);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
if (data['status'] == true && data['data'] != null) {
|
||||
setState(() {
|
||||
_createdInvitation = data['data'];
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(data['message'] ?? 'Invitation code created successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_showErrorAlert(data['message'] ?? 'Failed to create invitation code');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
_showErrorAlert('Network error. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorAlert(String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Error'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _copyToClipboard(String text) async {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Copied to clipboard'),
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(String dateString) {
|
||||
try {
|
||||
final date = DateTime.parse(dateString);
|
||||
return '${date.day}/${date.month}/${date.year} at ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Create Invitation Code',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF6A4C93), Colors.white],
|
||||
stops: [0.0, 0.3],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.confirmation_number_outlined,
|
||||
size: 48,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Generate New Invitation Code',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Create a new invitation code for user registration',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Create button
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _createInvitationCode,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isLoading
|
||||
? CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Create New Invitation Code',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Created invitation code display
|
||||
if (_createdInvitation != null) ...[
|
||||
Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Invitation Code Created',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Invitation code
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Color(0xFF6A4C93), width: 2),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_createdInvitation!['code'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6A4C93),
|
||||
letterSpacing: 2.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
IconButton(
|
||||
onPressed: () => _copyToClipboard(_createdInvitation!['code'] ?? ''),
|
||||
icon: Icon(
|
||||
Icons.copy,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
tooltip: 'Copy to clipboard',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Details
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
'ID:',
|
||||
_createdInvitation!['id']?.toString() ?? 'N/A',
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_buildDetailRow(
|
||||
'Created:',
|
||||
_formatDate(_createdInvitation!['createdDate'] ?? ''),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_buildDetailRow(
|
||||
'Expires:',
|
||||
_formatDate(_createdInvitation!['expirationDate'] ?? ''),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_buildDetailRow(
|
||||
'Status:',
|
||||
_createdInvitation!['expired'] == false ? 'Active' : 'Expired',
|
||||
valueColor: _createdInvitation!['expired'] == false ? Colors.green : Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Copy full details button
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
final details = '''
|
||||
Invitation Code: ${_createdInvitation!['code']}
|
||||
ID: ${_createdInvitation!['id']}
|
||||
Created: ${_formatDate(_createdInvitation!['createdDate'] ?? '')}
|
||||
Expires: ${_formatDate(_createdInvitation!['expirationDate'] ?? '')}
|
||||
Status: ${_createdInvitation!['expired'] == false ? 'Active' : 'Expired'}
|
||||
''';
|
||||
_copyToClipboard(details);
|
||||
},
|
||||
icon: Icon(Icons.copy_all),
|
||||
label: Text('Copy All Details'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: Color(0xFF6A4C93)),
|
||||
foregroundColor: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value, {Color? valueColor}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: valueColor ?? Colors.black87,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _displayNameController = TextEditingController();
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
@ -33,7 +33,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
void _initializeFields() {
|
||||
if (widget.userData != null) {
|
||||
_displayNameController.text = widget.userData!['displayName'] ?? '';
|
||||
_emailController.text = widget.userData!['email'] ?? '';
|
||||
_phoneController.text = widget.userData!['phoneNumber'] ?? '';
|
||||
if (!widget.isOnboarding) {
|
||||
_usernameController.text = widget.userData!['username'] ?? '';
|
||||
}
|
||||
@ -44,7 +44,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
void dispose() {
|
||||
_displayNameController.dispose();
|
||||
_usernameController.dispose();
|
||||
_emailController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -299,11 +299,11 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
controller: _phoneController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Email address',
|
||||
prefixIcon: Icon(Icons.email, color: Colors.grey[400]),
|
||||
hintText: 'Phone number',
|
||||
prefixIcon: Icon(Icons.phone, color: Colors.grey[400]),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
@ -328,7 +328,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Email cannot be changed',
|
||||
'Phone number cannot be changed',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
|
||||
@ -13,6 +13,7 @@ import '../edit_profile_screen.dart';
|
||||
import '../support_screen.dart';
|
||||
import '../information_screen.dart';
|
||||
import '../notifications_settings_screen.dart';
|
||||
import '../create_invitation_screen.dart';
|
||||
|
||||
class ProfilePage extends StatefulWidget {
|
||||
@override
|
||||
@ -752,9 +753,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _showCreateAccountDialog(context),
|
||||
icon: Icon(Icons.person_add, size: 20),
|
||||
label: Text('Create an Account'),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => CreateInvitationScreen()),
|
||||
),
|
||||
icon: Icon(Icons.confirmation_number_outlined, size: 20),
|
||||
label: Text('Create Invitation Code'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
foregroundColor: Colors.white,
|
||||
|
||||
@ -12,7 +12,7 @@ class AuthService {
|
||||
static const String _userDataKey = 'user_data';
|
||||
|
||||
static Future<Map<String, dynamic>> login(
|
||||
String emailOrUsername,
|
||||
String phoneNumberOrUsername,
|
||||
String password,
|
||||
) async {
|
||||
try {
|
||||
@ -20,7 +20,7 @@ class AuthService {
|
||||
Uri.parse('${ApiConstants.baseUrl}${ApiConstants.loginEndpoint}'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'emailOrUsername': emailOrUsername,
|
||||
'phoneNumberOrUsername': phoneNumberOrUsername,
|
||||
'password': password,
|
||||
}),
|
||||
);
|
||||
@ -57,6 +57,10 @@ class AuthService {
|
||||
return await _storage.read(key: _tokenKey);
|
||||
}
|
||||
|
||||
static Future<void> saveToken(String token) async {
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
}
|
||||
|
||||
static Future<bool> isLoggedIn() async {
|
||||
final token = await getToken();
|
||||
return token != null && token.isNotEmpty;
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user