Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
Expand All @@ -47,6 +51,11 @@
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.Map;

@RestController
@RequestMapping("/api/users")
Expand All @@ -18,39 +19,77 @@ public class UserController {
private final LoginService loginService;
private final AuthService authService;
private final RegistroRepository registroRepository;
private final com.magicvs.backend.service.RegistrationVerificationService verificationService;

public UserController(RegistroService registroService, LoginService loginService, AuthService authService, RegistroRepository registroRepository) {
public UserController(RegistroService registroService, LoginService loginService, AuthService authService, RegistroRepository registroRepository, com.magicvs.backend.service.RegistrationVerificationService verificationService) {
this.registroService = registroService;
this.loginService = loginService;
this.authService = authService;
this.registroRepository = registroRepository;
this.verificationService = verificationService;
}

@GetMapping("/exists")
public ResponseEntity<Map<String, Boolean>> exists(@RequestParam(name = "usernameOrEmail") String usernameOrEmail) {
String value = usernameOrEmail.trim();
boolean exists = loginService.existsByUsernameOrEmail(value);
return ResponseEntity.ok(Map.of("exists", exists));
}

// ---- Endpoints expuestos para Registro y Login ----

@PostMapping("/register")
public ResponseEntity<UserResponse> register(@RequestBody RegistroRequest request) {
public ResponseEntity<?> register(@RequestBody RegistroRequest request) {
try {
User user = registroService.registrar(request.username, request.email, request.password, request.displayName);
String token = authService.createSession(user.getId());
UserResponse resp = UserResponse.fromEntity(user);
resp.token = token;
return ResponseEntity.status(HttpStatus.CREATED).body(resp);
} catch (IllegalArgumentException ex) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(java.util.Map.of("message", ex.getMessage()));
}
}

@PostMapping("/login")
public ResponseEntity<UserResponse> login(@RequestBody LoginRequest request) {
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
User user = loginService.login(request.usernameOrEmail, request.password);
String token = authService.createSession(user.getId());
UserResponse resp = UserResponse.fromEntity(user);
resp.token = token;
return ResponseEntity.ok(resp);
} catch (IllegalArgumentException ex) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(java.util.Map.of("message", ex.getMessage()));
}
}

@PostMapping("/register/initiate")
public ResponseEntity<?> initiate(@RequestBody RegistroRequest request) {
try {
var pending = verificationService.initiate(request.username, request.email, request.password, request.displayName);
return ResponseEntity.ok(java.util.Map.of("pendingId", pending.getId()));
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(java.util.Map.of("message", ex.getMessage()));
} catch (Exception ex) {
String msg = ex.getMessage() != null ? ex.getMessage() : "Error interno al iniciar el registro";
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(java.util.Map.of("message", msg));
}
}

@PostMapping("/register/confirm")
public ResponseEntity<?> confirm(@RequestBody ConfirmRequest request) {
try {
User user = verificationService.confirm(request.pendingId, request.code);
String token = authService.createSession(user.getId());
UserResponse resp = UserResponse.fromEntity(user);
resp.token = token;
return ResponseEntity.status(HttpStatus.CREATED).body(resp);
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(java.util.Map.of("message", ex.getMessage()));
} catch (Exception ex) {
String msg = ex.getMessage() != null ? ex.getMessage() : "Error interno al confirmar registro";
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(java.util.Map.of("message", msg));
}
}

Expand Down Expand Up @@ -109,4 +148,9 @@ public static UserResponse fromEntity(User user) {
return resp;
}
}

public static class ConfirmRequest {
public Long pendingId;
public String code;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.magicvs.backend.model;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "pending_registrations")
public class PendingRegistration {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50)
private String username;

@Column(nullable = false, length = 100)
private String email;

@Column(name = "password_hash", nullable = false, length = 255)
private String passwordHash;

@Column(name = "display_name", length = 100)
private String displayName;

@Column(name = "verification_hash", length = 255)
private String verificationHash;

@Column(name = "created_at")
private LocalDateTime createdAt;

@Column(name = "expires_at")
private LocalDateTime expiresAt;

@Column(name = "attempts")
private Integer attempts = 0;

public PendingRegistration() {
}

@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}

public Long getId() {
return id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPasswordHash() {
return passwordHash;
}

public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}

public String getDisplayName() {
return displayName;
}

public void setDisplayName(String displayName) {
this.displayName = displayName;
}

public String getVerificationHash() {
return verificationHash;
}

public void setVerificationHash(String verificationHash) {
this.verificationHash = verificationHash;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public LocalDateTime getExpiresAt() {
return expiresAt;
}

public void setExpiresAt(LocalDateTime expiresAt) {
this.expiresAt = expiresAt;
}

public Integer getAttempts() {
return attempts;
}

public void setAttempts(Integer attempts) {
this.attempts = attempts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.magicvs.backend.repository;

import com.magicvs.backend.model.PendingRegistration;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface PendingRegistrationRepository extends JpaRepository<PendingRegistration, Long> {
Optional<PendingRegistration> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.springframework.stereotype.Service;

import java.util.Locale;
import com.magicvs.backend.util.ValidationUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Service
public class LoginService {
Expand All @@ -18,15 +20,26 @@ public LoginService(LoginRepository loginRepository) {
public User login(String usernameOrEmail, String password) {
String value = usernameOrEmail.trim();

if (!ValidationUtils.isUsernameOrEmail(value)) {
throw new IllegalArgumentException("Formato de usuario o email inválido");
}

User user = loginRepository.findByUsername(value)
.or(() -> loginRepository.findByEmail(value.toLowerCase(Locale.ROOT)))
.orElseThrow(() -> new IllegalArgumentException("Usuario o contraseña incorrectos"));
.orElseThrow(() -> new IllegalArgumentException("Credenciales incorrectas"));

// Comparación directa por simplicidad (recuerda encriptar en producción)
if (!user.getPasswordHash().equals(password)) {
throw new IllegalArgumentException("Usuario o contraseña incorrectos");
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if (!encoder.matches(password, user.getPasswordHash())) {
throw new IllegalArgumentException("Credenciales incorrectas");
}

return user;
}

public boolean existsByUsernameOrEmail(String usernameOrEmail) {
String value = usernameOrEmail.trim();
boolean byUsername = loginRepository.findByUsername(value).isPresent();
boolean byEmail = loginRepository.findByEmail(value.toLowerCase(Locale.ROOT)).isPresent();
return byUsername || byEmail;
}
}
Loading