fix: change db schema
This commit is contained in:
+2
-2
@@ -26,12 +26,12 @@ public class AuthController {
|
||||
@PostMapping("/register")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public AuthResponse register(@Valid @RequestBody RegisterRequest request) {
|
||||
return authService.register(request.email(), request.password(), request.displayName());
|
||||
return authService.register(request.username(), request.email(), request.password(), request.displayName());
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public AuthResponse login(@Valid @RequestBody LoginRequest request) {
|
||||
return authService.login(request.email(), request.password());
|
||||
return authService.login(request.username(), request.password());
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
package aplp.backend.web.identity.application.dtos;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record LoginRequest(
|
||||
@NotBlank @Email(message = "must be a valid email") String email,
|
||||
@NotBlank @Size(min = 3, max = 50) String username,
|
||||
@NotBlank String password
|
||||
) {
|
||||
}
|
||||
+1
@@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record RegisterRequest(
|
||||
@NotBlank @Size(min = 3, max = 50) String username,
|
||||
@NotBlank @Email(message = "must be a valid email") @Size(max = 320) String email,
|
||||
@NotBlank @Size(min = 8, max = 128, message = "must be between 8 and 128 characters") String password,
|
||||
@NotBlank @Size(max = 100) String displayName
|
||||
+8
-5
@@ -9,6 +9,7 @@ import aplp.backend.web.identity.application.dtos.AuthResponse;
|
||||
import aplp.backend.web.identity.application.mappers.AuthMapper;
|
||||
import aplp.backend.web.identity.domain.exceptions.EmailAlreadyExistsException;
|
||||
import aplp.backend.web.identity.domain.exceptions.InvalidCredentialsException;
|
||||
import aplp.backend.web.identity.domain.exceptions.UsernameAlreadyExistsException;
|
||||
import aplp.backend.web.identity.domain.entities.RefreshToken;
|
||||
import aplp.backend.web.identity.domain.exceptions.RefreshTokenInvalidException;
|
||||
import aplp.backend.web.identity.domain.repositories.RefreshTokenRepository;
|
||||
@@ -53,22 +54,24 @@ public class AuthService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AuthResponse register(String email, String rawPassword, String displayName) {
|
||||
public AuthResponse register(String username, String email, String rawPassword, String displayName) {
|
||||
Instant now = clock.instant();
|
||||
String normalizedEmail = email.trim().toLowerCase();
|
||||
if (userRepository.existsByEmail(normalizedEmail)) {
|
||||
throw new EmailAlreadyExistsException(normalizedEmail);
|
||||
}
|
||||
User user = User.register(normalizedEmail, passwordEncoder.encode(rawPassword), displayName, now);
|
||||
if (userRepository.existsByUsername(username)) {
|
||||
throw new UsernameAlreadyExistsException(username);
|
||||
}
|
||||
User user = User.register(username, normalizedEmail, passwordEncoder.encode(rawPassword), displayName, now);
|
||||
User saved = userRepository.save(user);
|
||||
learnerProvisioner.provision(saved.id(), displayName);
|
||||
return issueTokens(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AuthResponse login(String email, String rawPassword) {
|
||||
String normalizedEmail = email.trim().toLowerCase();
|
||||
User user = userRepository.findByEmail(normalizedEmail).orElseThrow(InvalidCredentialsException::new);
|
||||
public AuthResponse login(String username, String rawPassword) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow(InvalidCredentialsException::new);
|
||||
if (!passwordEncoder.matches(rawPassword, user.passwordHash())) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
+13
-5
@@ -21,6 +21,9 @@ public class User {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 320)
|
||||
private String email;
|
||||
|
||||
@@ -43,9 +46,10 @@ public class User {
|
||||
protected User() {
|
||||
}
|
||||
|
||||
private User(Long id, String email, String passwordHash, String displayName,
|
||||
private User(Long id, String username, String email, String passwordHash, String displayName,
|
||||
UserStatus status, Instant createdAt, Instant updatedAt) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.passwordHash = passwordHash;
|
||||
this.displayName = displayName;
|
||||
@@ -54,13 +58,13 @@ public class User {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public static User register(String email, String passwordHash, String displayName, Instant now) {
|
||||
return new User(null, normalizeEmail(email), passwordHash, displayName, UserStatus.ACTIVE, now, now);
|
||||
public static User register(String username, String email, String passwordHash, String displayName, Instant now) {
|
||||
return new User(null, username, normalizeEmail(email), passwordHash, displayName, UserStatus.ACTIVE, now, now);
|
||||
}
|
||||
|
||||
public static User reconstruct(Long id, String email, String passwordHash, String displayName,
|
||||
public static User reconstruct(Long id, String username, String email, String passwordHash, String displayName,
|
||||
UserStatus status, Instant createdAt, Instant updatedAt) {
|
||||
return new User(id, email, passwordHash, displayName, status, createdAt, updatedAt);
|
||||
return new User(id, username, email, passwordHash, displayName, status, createdAt, updatedAt);
|
||||
}
|
||||
|
||||
public void updatePasswordHash(String newHash, Instant now) {
|
||||
@@ -81,6 +85,10 @@ public class User {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String username() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String email() {
|
||||
return email;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package aplp.backend.web.identity.domain.exceptions;
|
||||
|
||||
import aplp.backend.core.common.exception.DomainException;
|
||||
import aplp.backend.core.common.exception.ErrorCode;
|
||||
|
||||
public class UsernameAlreadyExistsException extends DomainException {
|
||||
|
||||
public UsernameAlreadyExistsException(String username) {
|
||||
super(ErrorCode.USERNAME_ALREADY_EXISTS, "Username is already taken: " + username);
|
||||
}
|
||||
}
|
||||
+4
@@ -8,9 +8,13 @@ public interface UserRepository {
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
Optional<User> findById(Long id);
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
User save(User user);
|
||||
}
|
||||
+4
@@ -7,5 +7,9 @@ public interface UserJpaRepository extends JpaRepository<User, Long> {
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
java.util.Optional<User> findByEmail(String email);
|
||||
|
||||
java.util.Optional<User> findByUsername(String username);
|
||||
}
|
||||
+10
@@ -20,6 +20,11 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
return jpaRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findByUsername(String username) {
|
||||
return jpaRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findById(Long id) {
|
||||
return jpaRepository.findById(id);
|
||||
@@ -30,6 +35,11 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
return jpaRepository.existsByEmail(email);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByUsername(String username) {
|
||||
return jpaRepository.existsByUsername(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User save(User user) {
|
||||
return jpaRepository.save(user);
|
||||
@@ -1,6 +1,6 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://localhost:5432/aplp}
|
||||
url: ${DB_URL:jdbc:postgresql://192.168.2.100:5432/aplp}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:Pa55w0rd}
|
||||
hikari:
|
||||
|
||||
@@ -6,10 +6,15 @@ spring:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
default_schema: web
|
||||
open-in-view: false
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
schemas: web
|
||||
default-schema: web
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
CREATE TABLE users (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
email VARCHAR(320) NOT NULL,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_users_email UNIQUE (email)
|
||||
CONSTRAINT uq_users_email UNIQUE (email),
|
||||
CONSTRAINT uq_users_username UNIQUE (username)
|
||||
);
|
||||
|
||||
CREATE TABLE refresh_token (
|
||||
@@ -32,3 +34,4 @@ CREATE TABLE learner (
|
||||
|
||||
CREATE INDEX idx_refresh_token_user_id ON refresh_token (user_id);
|
||||
CREATE INDEX idx_users_email ON users (email);
|
||||
CREATE INDEX idx_users_username ON users (username);
|
||||
|
||||
@@ -38,10 +38,10 @@ class AuthFlowIntegrationTest {
|
||||
|
||||
@Test
|
||||
void fullAuthFlow() throws Exception {
|
||||
TokenPair registered = register(EMAIL, PASSWORD, "Learner One");
|
||||
TokenPair registered = register("learnerone", EMAIL, PASSWORD, "Learner One");
|
||||
viewMyProfile(registered.accessToken());
|
||||
|
||||
TokenPair loggedIn = login(EMAIL, PASSWORD);
|
||||
TokenPair loggedIn = login("learnerone", PASSWORD);
|
||||
viewMyProfile(loggedIn.accessToken());
|
||||
|
||||
TokenPair refreshed = refresh(loggedIn.refreshToken());
|
||||
@@ -54,7 +54,7 @@ class AuthFlowIntegrationTest {
|
||||
void registrationSetsXRequestIdAndRejectsDuplicate() throws Exception {
|
||||
MvcResult first = mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json("email", "dup@aplp.dev", "password", PASSWORD, "displayName", "Dup")))
|
||||
.content(json("username", "dupuser", "email", "dup@aplp.dev", "password", PASSWORD, "displayName", "Dup")))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(header().exists("X-Request-Id"))
|
||||
.andReturn();
|
||||
@@ -62,7 +62,7 @@ class AuthFlowIntegrationTest {
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json("email", "dup@aplp.dev", "password", PASSWORD, "displayName", "Dup")))
|
||||
.content(json("username", "dupuser2", "email", "dup@aplp.dev", "password", PASSWORD, "displayName", "Dup")))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("EMAIL_ALREADY_EXISTS"));
|
||||
}
|
||||
@@ -80,10 +80,10 @@ class AuthFlowIntegrationTest {
|
||||
|
||||
@Test
|
||||
void loginWithWrongPasswordIsRejected() throws Exception {
|
||||
register("wrong@aplp.dev", PASSWORD, "Wrong");
|
||||
register("wronguser", "wrong@aplp.dev", PASSWORD, "Wrong");
|
||||
mockMvc.perform(post("/api/v1/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json("email", "wrong@aplp.dev", "password", "not-the-password")))
|
||||
.content(json("username", "wronguser", "password", "not-the-password")))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.code").value("INVALID_CREDENTIALS"));
|
||||
}
|
||||
@@ -92,14 +92,14 @@ class AuthFlowIntegrationTest {
|
||||
void validationErrorsReturnUniformShape() throws Exception {
|
||||
mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"email\":\"not-an-email\",\"password\":\"x\",\"displayName\":\"\"}"))
|
||||
.content("{\"username\":\"\",\"email\":\"not-an-email\",\"password\":\"x\",\"displayName\":\"\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateLearnerDisplayName() throws Exception {
|
||||
TokenPair pair = register("update-me@aplp.dev", PASSWORD, "Old Name");
|
||||
TokenPair pair = register("updateme", "update-me@aplp.dev", PASSWORD, "Old Name");
|
||||
mockMvc.perform(patch("/api/v1/learners/me")
|
||||
.header("Authorization", "Bearer " + pair.accessToken())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -110,7 +110,7 @@ class AuthFlowIntegrationTest {
|
||||
|
||||
@Test
|
||||
void refreshWithRevokedTokenIsRejected() throws Exception {
|
||||
TokenPair pair = loginOrRegister("revoked@aplp.dev");
|
||||
TokenPair pair = loginOrRegister("revokeduser", "revoked@aplp.dev");
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/logout")
|
||||
.header("Authorization", "Bearer " + pair.accessToken())
|
||||
@@ -141,10 +141,10 @@ class AuthFlowIntegrationTest {
|
||||
.andExpect(jsonPath("$.status").value("UP"));
|
||||
}
|
||||
|
||||
private TokenPair register(String email, String password, String displayName) throws Exception {
|
||||
private TokenPair register(String username, String email, String password, String displayName) throws Exception {
|
||||
MvcResult result = mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json("email", email, "password", password, "displayName", displayName)))
|
||||
.content(json("username", username, "email", email, "password", password, "displayName", displayName)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
JsonNode body = bodyOf(result);
|
||||
@@ -152,22 +152,21 @@ class AuthFlowIntegrationTest {
|
||||
return new TokenPair(body.get("accessToken").asText(), body.get("refreshToken").asText());
|
||||
}
|
||||
|
||||
private TokenPair login(String email, String password) throws Exception {
|
||||
private TokenPair login(String username, String password) throws Exception {
|
||||
MvcResult result = mockMvc.perform(post("/api/v1/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json("email", email, "password", password)))
|
||||
.content(json("username", username, "password", password)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
JsonNode body = bodyOf(result);
|
||||
assertThat(body.get("user").get("email").asText()).isEqualTo(email);
|
||||
return new TokenPair(body.get("accessToken").asText(), body.get("refreshToken").asText());
|
||||
}
|
||||
|
||||
private TokenPair loginOrRegister(String email) throws Exception {
|
||||
private TokenPair loginOrRegister(String username, String email) throws Exception {
|
||||
try {
|
||||
return register(email, PASSWORD, "Revoked");
|
||||
return register(username, email, PASSWORD, "Revoked");
|
||||
} catch (AssertionError ignored) {
|
||||
return login(email, PASSWORD);
|
||||
return login(username, PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,13 @@ spring:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
default_schema: web
|
||||
flyway:
|
||||
enabled: true
|
||||
schemas: web
|
||||
default-schema: web
|
||||
|
||||
app:
|
||||
jwt:
|
||||
|
||||
Reference in New Issue
Block a user