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