From 7e8a04d83cab54cfa4f699fe0826df58ede4816f Mon Sep 17 00:00:00 2001 From: Lobo Date: Tue, 31 Mar 2026 19:48:23 +0200 Subject: [PATCH] Login should work now, tests are missing --- .gitea/workflows/publish-auth.yml | 71 ++++++++++++++++ Dockerfile | 35 ++++++++ pom.xml | 22 +++-- .../auth_service/AuthServiceApplication.java | 10 +-- .../configuration/ObservabilityConfig.java | 24 ++++++ .../configuration/SecurityConfig.java | 85 +++++++++++++++++++ .../login/controller/LoginController.java | 46 ++++++++++ .../login/controller/dtos/LoginRequest.java | 7 ++ .../login/controller/dtos/LoginResponse.java | 8 ++ .../domain/login/model/UserEntity.java | 30 +++++++ .../login/repository/UserRepository.java | 22 +++++ src/main/resources/application-dev.yaml | 14 +++ src/main/resources/application.yaml | 35 +++++++- 13 files changed, 392 insertions(+), 17 deletions(-) create mode 100644 .gitea/workflows/publish-auth.yml create mode 100644 Dockerfile create mode 100644 src/main/java/auth_service/configuration/ObservabilityConfig.java create mode 100644 src/main/java/auth_service/configuration/SecurityConfig.java create mode 100644 src/main/java/auth_service/domain/login/controller/LoginController.java create mode 100644 src/main/java/auth_service/domain/login/controller/dtos/LoginRequest.java create mode 100644 src/main/java/auth_service/domain/login/controller/dtos/LoginResponse.java create mode 100644 src/main/java/auth_service/domain/login/model/UserEntity.java create mode 100644 src/main/java/auth_service/domain/login/repository/UserRepository.java create mode 100644 src/main/resources/application-dev.yaml diff --git a/.gitea/workflows/publish-auth.yml b/.gitea/workflows/publish-auth.yml new file mode 100644 index 0000000..a347f31 --- /dev/null +++ b/.gitea/workflows/publish-auth.yml @@ -0,0 +1,71 @@ +name: Build, Test & Push Auth Service + +# Trigger the workflow +on: + push: + branches: + - main + +jobs: + # ------------------------------------------------------------------ + # JOB 1: Code integrity and quality gates (CI) + # ------------------------------------------------------------------ + quality-check: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Java 25 + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + # Automatically cache Maven dependencies to speed up builds + cache: 'maven' + + - name: Build and Test with Maven + # Run all tests (including Spring Boot context loads) to ensure stability + run: ./mvnw clean verify + + # ------------------------------------------------------------------ + # JOB 2: Docker Build & Push (CD) + # Runs only if 'quality-check' is successful and we are on branch main + # ------------------------------------------------------------------ + docker: + needs: quality-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Prepare tag + id: prep + shell: bash + run: | + # Sanitize branch name for valid Docker tags + BRANCH="${GITHUB_REF_NAME}" + BRANCH="${BRANCH//\//-}" + BRANCH="$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]')" + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" + echo "sha=${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.andreas-dahm.eu + # Use your Gitea Secrets here + username: ${{ github.repository_owner }} + password: ${{ secrets.TOKEN_GITEA }} + + - name: Build & Push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + git.andreas-dahm.eu/lobo_corp/tbp-auth-service:${{ steps.prep.outputs.branch }} + git.andreas-dahm.eu/lobo_corp/tbp-auth-service:${{ steps.prep.outputs.branch }}-${{ steps.prep.outputs.sha }} + git.andreas-dahm.eu/lobo_corp/tbp-auth-service:latest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4de8f6a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# ------------------------------------------------------------------ +# STAGE 1: Build the application using Maven Wrapper +# ------------------------------------------------------------------ +FROM eclipse-temurin:25-jdk-alpine AS builder + +WORKDIR /app + +# Copy the Maven wrapper and pom.xml first to cache dependencies +COPY .mvn/ .mvn +COPY mvnw pom.xml ./ + +# Make the wrapper executable (just in case) and download dependencies +RUN chmod +x mvnw +RUN ./mvnw dependency:go-offline + +# Copy the actual source code and build the application (skip tests in Docker, +# as they will be run by the Gitea Action before the build) +COPY src ./src +RUN ./mvnw clean package -DskipTests + +# ------------------------------------------------------------------ +# STAGE 2: Create the minimal runtime image +# ------------------------------------------------------------------ +FROM eclipse-temurin:25-jre-alpine + +WORKDIR /app + +# Expose the port your Spring Boot app runs on +EXPOSE 8080 + +# Copy the compiled jar file from the builder stage +COPY --from=builder /app/target/*.jar app.jar + +# Run the application +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index d827043..e709f52 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,19 @@ org.flywaydb flyway-database-postgresql + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-flyway + + + io.micrometer + micrometer-registry-prometheus + runtime + org.springframework.boot spring-boot-docker-compose @@ -84,15 +97,6 @@ - - - ${basedir}/src/main/resources - true - - **/application*.yml - - - org.apache.maven.plugins diff --git a/src/main/java/auth_service/AuthServiceApplication.java b/src/main/java/auth_service/AuthServiceApplication.java index b456ef6..0914914 100644 --- a/src/main/java/auth_service/AuthServiceApplication.java +++ b/src/main/java/auth_service/AuthServiceApplication.java @@ -5,9 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class AuthServiceApplication { - - public static void main(String[] args) { - SpringApplication.run(AuthServiceApplication.class, args); - } - -} + static void main(String[] args) { + SpringApplication.run(AuthServiceApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/auth_service/configuration/ObservabilityConfig.java b/src/main/java/auth_service/configuration/ObservabilityConfig.java new file mode 100644 index 0000000..a7b6701 --- /dev/null +++ b/src/main/java/auth_service/configuration/ObservabilityConfig.java @@ -0,0 +1,24 @@ +package auth_service.configuration; + +import io.micrometer.core.aop.TimedAspect; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for application observability and metrics. + */ +@Configuration +public class ObservabilityConfig { + + /** + * Registers the TimedAspect bean to enable @Timed annotations. + * * @param registry The Micrometer MeterRegistry + * + * @return A new TimedAspect instance + */ + @Bean + public TimedAspect timedAspect(MeterRegistry registry) { + return new TimedAspect(registry); + } +} \ No newline at end of file diff --git a/src/main/java/auth_service/configuration/SecurityConfig.java b/src/main/java/auth_service/configuration/SecurityConfig.java new file mode 100644 index 0000000..0aaeb3f --- /dev/null +++ b/src/main/java/auth_service/configuration/SecurityConfig.java @@ -0,0 +1,85 @@ +package auth_service.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +/** + * Global security configuration for the Authentication Service. + * Implements whitelist routing and environment-specific CORS. + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + /** + * The Whitelist: Endpoints that do NOT require authentication. + */ + private static final String[] WHITE_LIST_URLS = { + "/api/auth/**", // Allow login, registration, etc. + "/actuator/health", // Allow health checks (e.g., for Docker/Traefik) + "/actuator/prometheus", // Allow Prometheus to scrape metrics + "/error" // Default Spring Boot error page routing + }; + /** + * Injects the allowed CORS origins from the active application.yml profile. + */ + @Value("${app.cors.allowed-origins}") + private String[] allowedOrigins; + + /** + * Defines the password encoder used for hashing passwords. + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * Defines the global CORS configuration. + * Using a dedicated Bean ensures it's properly registered in the filter chain. + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // Use OriginPatterns instead of AllowedOrigins to safely support "*" wildcards + configuration.setAllowedOriginPatterns(Arrays.asList(allowedOrigins)); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(false); // Set to true later if we use cookies + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + // Apply this CORS config to all endpoints + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) { + http + .csrf(AbstractHttpConfigurer::disable) + // Tell Spring Security to use the CorsConfigurationSource bean defined above + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers(WHITE_LIST_URLS).permitAll() + .anyRequest().authenticated() + ); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/auth_service/domain/login/controller/LoginController.java b/src/main/java/auth_service/domain/login/controller/LoginController.java new file mode 100644 index 0000000..24cf326 --- /dev/null +++ b/src/main/java/auth_service/domain/login/controller/LoginController.java @@ -0,0 +1,46 @@ +package auth_service.domain.login.controller; + +import auth_service.domain.login.controller.dtos.LoginRequest; +import auth_service.domain.login.controller.dtos.LoginResponse; +import auth_service.domain.login.repository.UserRepository; +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller handling authentication requests. + */ +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class LoginController { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + /** + * Authenticates a user based on email and password. + * * @param request The login credentials + * + * @return HTTP 200 on success, HTTP 401 on failure + */ + @PostMapping("/login") + @Timed(value = "auth.login.requests.time", description = "Time taken to process login requests") + public ResponseEntity login(@RequestBody LoginRequest request) { + + return userRepository.findByEmail(request.email()) + .filter(user -> passwordEncoder.matches(request.password(), user.getPassword())) + .map(user -> ResponseEntity.ok( + new LoginResponse("Login successful", user.getEmail()) + )) + .orElseGet(() -> ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + new LoginResponse("Invalid credentials", null) + )); + } +} \ No newline at end of file diff --git a/src/main/java/auth_service/domain/login/controller/dtos/LoginRequest.java b/src/main/java/auth_service/domain/login/controller/dtos/LoginRequest.java new file mode 100644 index 0000000..e40802c --- /dev/null +++ b/src/main/java/auth_service/domain/login/controller/dtos/LoginRequest.java @@ -0,0 +1,7 @@ +package auth_service.domain.login.controller.dtos; + +/** + * DTO (Data Transfer Object) representing the incoming login request. + */ +public record LoginRequest(String email, String password) { +} \ No newline at end of file diff --git a/src/main/java/auth_service/domain/login/controller/dtos/LoginResponse.java b/src/main/java/auth_service/domain/login/controller/dtos/LoginResponse.java new file mode 100644 index 0000000..96a4d5d --- /dev/null +++ b/src/main/java/auth_service/domain/login/controller/dtos/LoginResponse.java @@ -0,0 +1,8 @@ +package auth_service.domain.login.controller.dtos; + +/** + * DTO representing the outgoing login response. + * The email will be null if the login fails. + */ +public record LoginResponse(String message, String email) { +} \ No newline at end of file diff --git a/src/main/java/auth_service/domain/login/model/UserEntity.java b/src/main/java/auth_service/domain/login/model/UserEntity.java new file mode 100644 index 0000000..edc0434 --- /dev/null +++ b/src/main/java/auth_service/domain/login/model/UserEntity.java @@ -0,0 +1,30 @@ +package auth_service.domain.login.model; + +import jakarta.persistence.*; +import lombok.*; + +/** + * Represents a user entity mapped to the 'users' table in PostgreSQL. + */ +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private boolean enabled; +} \ No newline at end of file diff --git a/src/main/java/auth_service/domain/login/repository/UserRepository.java b/src/main/java/auth_service/domain/login/repository/UserRepository.java new file mode 100644 index 0000000..6e4c5e0 --- /dev/null +++ b/src/main/java/auth_service/domain/login/repository/UserRepository.java @@ -0,0 +1,22 @@ +package auth_service.domain.login.repository; + +import auth_service.domain.login.model.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * Repository for database operations regarding the User entity. + */ +@Repository +public interface UserRepository extends JpaRepository { + + /** + * Finds a user by their email address. + * * @param email The email to search for + * + * @return An Optional containing the user if found + */ + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..d807033 --- /dev/null +++ b/src/main/resources/application-dev.yaml @@ -0,0 +1,14 @@ +# ───────────────────────────────────────────── +# DEVELOPMENT CONFIGURATION +# ───────────────────────────────────────────── +# Overrides properties from application.yml when the 'dev' profile is active + +app: + cors: + # Local development: allow all origins to prevent CORS issues + allowed-origins: "*" + +# Optional: Make SQL logs visible for debugging +spring: + jpa: + show-sql: true \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1775ea2..0077dc8 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,5 +1,36 @@ +# ───────────────────────────────────────────── +# BASE CONFIGURATION (Production Defaults) +# ───────────────────────────────────────────── spring: application: - name: auth-service + name: tbp-auth-service + profiles: + active: dev # Default profile for local startup + flyway: + # Force Flyway to be enabled + enabled: true + # Create the schema history table if it does not exist + baseline-on-migrate: true + # Explicitly state where to look for scripts + locations: classpath:db/migration + +# Actuator & Micrometer Settings +management: + endpoints: + web: + exposure: + # Expose health and metrics to the web + include: health, prometheus, metrics + endpoint: + health: + show-details: always + prometheus: + metrics: + export: + enabled: true + +# Custom properties for CORS app: - version: '@project.version@' \ No newline at end of file + cors: + # Production default: strictly allow only the frontend domain + allowed-origins: "https://tbp.andreas-dahm.eu" \ No newline at end of file