bilhej/backend/src/main/java/se/bilhalsning/entity/User.java
Joakim Mörling c03b5a1401 feat: add User entity, repository, service, and Flyway users table migration
- V1__create_users_table.sql replaces placeholder: creates users table with
  id UUID PK, email UNIQUE NOT NULL, password_hash NOT NULL, subscription
  VARCHAR(20) DEFAULT 'none' with CHECK constraint (none/basic/pro),
  created_at/updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP.
  Compatible with both H2 and PostgreSQL.

- SecurityConfig: minimal @Configuration providing BCryptPasswordEncoder
  bean. Required because Spring Boot 4 no longer auto-configures a
  PasswordEncoder.

- Subscription enum: NONE, BASIC, PRO with string values matching the DB
  CHECK constraint.

- User entity: @PrePersist generates UUID and timestamps in application
  code, @PreUpdate refreshes updated_at. Email setter normalizes to
  lowercase for case-insensitive uniqueness. Explicit getters/setters
  (no Lombok per guidelines).

- UserRepository: Spring Data JPA extending JpaRepository<User, UUID>.
  findByEmail(Optional) and existsByEmail for duplicate checks.

- UserService: @RequiredArgsConstructor with constructor-injected
  UserRepository and PasswordEncoder. createUser normalizes email,
  checks duplicates via existsByEmail, throws EmailAlreadyExistsException,
  hashes password with BCrypt, saves. findByEmail returns Optional<User>.

- EmailAlreadyExistsException: custom RuntimeException for duplicate
  registration attempts. ControllerAdvice handler deferred to auth ticket.

Verification: ./gradlew test passes (Flyway + H2 context loads).
docker compose up -d succeeds, Flyway applies V1 against PostgreSQL 16.
\d users confirms all columns, constraints, defaults, and indexes.
2026-05-01 02:06:24 +02:00

102 lines
2.4 KiB
Java

package se.bilhalsning.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "users")
public class User {
@Id
@Column(name = "id", columnDefinition = "uuid", nullable = false, updatable = false)
private UUID id;
@Column(name = "email", nullable = false, unique = true, length = 255)
private String email;
@Column(name = "password_hash", nullable = false, length = 255)
private String passwordHash;
@Enumerated(EnumType.STRING)
@Column(name = "subscription", nullable = false, length = 20)
private Subscription subscription = Subscription.NONE;
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@PrePersist
void onCreate() {
if (this.id == null) {
this.id = UUID.randomUUID();
}
Instant now = Instant.now();
if (this.createdAt == null) {
this.createdAt = now;
}
this.updatedAt = now;
}
@PreUpdate
void onUpdate() {
this.updatedAt = Instant.now();
}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email != null ? email.toLowerCase().trim() : null;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public Subscription getSubscription() {
return subscription;
}
public void setSubscription(Subscription subscription) {
this.subscription = subscription;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}