Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.
Merged
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
7 changes: 3 additions & 4 deletions .github/workflows/buildtest.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
name: Build and Test Admin Service

on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
- main
- dev
- devOps

jobs:
build-test:
Expand Down
27 changes: 27 additions & 0 deletions admin-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
Expand All @@ -54,6 +58,29 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!-- JWT dependencies -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.techtorque.admin_service.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Value("${jwt.secret:mysecretkey}")
private String jwtSecret;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String authHeader = request.getHeader("Authorization");

if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);

try {
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));

Claims claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();

String username = claims.getSubject();
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) claims.get("roles");

if (username != null && roles != null) {
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(role -> {
String roleUpper = role.trim().toUpperCase();
// Treat SUPER_ADMIN as ADMIN for authorization purposes
if ("SUPER_ADMIN".equals(roleUpper)) {
// Add both SUPER_ADMIN and ADMIN roles
return Arrays.asList(
new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"),
new SimpleGrantedAuthority("ROLE_ADMIN")
);
}
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + roleUpper));
})
.flatMap(List::stream)
.collect(Collectors.toList());

UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);

SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.warn("JWT token validation failed: " + e.getMessage());
}
}

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.techtorque.admin_service.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
Expand All @@ -12,8 +13,11 @@
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

// A more comprehensive whitelist for Swagger/OpenAPI, based on the auth-service config.
private static final String[] SWAGGER_WHITELIST = {
"/v3/api-docs/**",
Expand All @@ -24,6 +28,11 @@ public class SecurityConfig {
"/api-docs/**"
};

// Public endpoints accessible by all authenticated users (including CUSTOMER role)
private static final String[] PUBLIC_ENDPOINTS = {
"/public/**"
};

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
Expand All @@ -42,12 +51,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Permit all requests to the Swagger UI and API docs paths
.requestMatchers(SWAGGER_WHITELIST).permitAll()

// Allow authenticated users to access public endpoints
.requestMatchers(PUBLIC_ENDPOINTS).authenticated()

// All other requests must be authenticated
.anyRequest().authenticated()
)

// Add our custom filter to read headers from the Gateway
.addFilterBefore(new GatewayHeaderFilter(), UsernamePasswordAuthenticationFilter.class);
// Add JWT filter first (for direct service-to-service calls with JWT)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

// Add our custom filter to read headers from the Gateway (for gateway-routed calls)
.addFilterAfter(new GatewayHeaderFilter(), JwtAuthenticationFilter.class);

return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import reactor.core.publisher.Mono;
import jakarta.servlet.http.HttpServletRequest;

/**
* Configuration for WebClient to communicate with other microservices
Expand All @@ -31,12 +36,35 @@ public class WebClientConfig {
@Value("${services.vehicle.url:http://localhost:8082}")
private String vehicleServiceUrl;

/**
* Exchange filter function to propagate JWT token from incoming request to outgoing WebClient calls
*/
private ExchangeFilterFunction jwtTokenPropagationFilter() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return Mono.just(
org.springframework.web.reactive.function.client.ClientRequest
.from(clientRequest)
.header(HttpHeaders.AUTHORIZATION, authHeader)
.build()
);
}
}
return Mono.just(clientRequest);
});
}

@Bean(name = "authServiceWebClient")
public WebClient authServiceWebClient(WebClient.Builder webClientBuilder) {
return webClientBuilder
.baseUrl(authServiceUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(jwtTokenPropagationFilter())
.build();
}

Expand All @@ -46,6 +74,7 @@ public WebClient paymentServiceWebClient(WebClient.Builder webClientBuilder) {
.baseUrl(paymentServiceUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(jwtTokenPropagationFilter())
.build();
}

Expand All @@ -55,6 +84,7 @@ public WebClient appointmentServiceWebClient(WebClient.Builder webClientBuilder)
.baseUrl(appointmentServiceUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(jwtTokenPropagationFilter())
.build();
}

Expand All @@ -64,6 +94,7 @@ public WebClient projectServiceWebClient(WebClient.Builder webClientBuilder) {
.baseUrl(projectServiceUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(jwtTokenPropagationFilter())
.build();
}

Expand All @@ -73,6 +104,7 @@ public WebClient timeLoggingServiceWebClient(WebClient.Builder webClientBuilder)
.baseUrl(timeLoggingServiceUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(jwtTokenPropagationFilter())
.build();
}

Expand All @@ -82,6 +114,7 @@ public WebClient vehicleServiceWebClient(WebClient.Builder webClientBuilder) {
.baseUrl(vehicleServiceUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(jwtTokenPropagationFilter())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.techtorque.admin_service.controller;

import com.techtorque.admin_service.dto.response.ServiceTypeResponse;
import com.techtorque.admin_service.service.AdminServiceConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* Public controller for service types - accessible by all authenticated users and other microservices
* This allows customers and other services to read active service types without admin privileges
*/
@RestController
@RequestMapping("/public/service-types")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "Public Service Types", description = "Public API for accessing service types")
public class PublicServiceTypeController {

private final AdminServiceConfigService adminServiceConfigService;

@GetMapping
@Operation(summary = "Get all active service types", description = "Retrieve all active service types available to customers")
public ResponseEntity<List<ServiceTypeResponse>> getActiveServiceTypes() {
log.info("Public request: Fetching all active service types");
List<ServiceTypeResponse> serviceTypes = adminServiceConfigService.getAllServiceTypes(true); // activeOnly = true
log.info("Retrieved {} active service types for public access", serviceTypes.size());
return ResponseEntity.ok(serviceTypes);
}

@GetMapping("/{id}")
@Operation(summary = "Get service type by ID", description = "Retrieve a specific service type by its ID")
public ResponseEntity<ServiceTypeResponse> getServiceTypeById(@PathVariable String id) {
log.info("Public request: Fetching service type with ID: {}", id);
ServiceTypeResponse serviceType = adminServiceConfigService.getServiceTypeById(id);
return ResponseEntity.ok(serviceType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,24 @@ public class UpdateUserRequest {
@Pattern(regexp = "ADMIN|EMPLOYEE|CUSTOMER", message = "Role must be ADMIN, EMPLOYEE, or CUSTOMER")
private String role;

private List<String> roles;

private Boolean active;

// Alternative field name for activation status (frontend compatibility)
private Boolean enabled;

@Size(max = 20, message = "Maximum 20 permissions allowed")
private List<String> permissions;

@Size(max = 100)
private String department;

/**
* Get the activation status, checking both 'active' and 'enabled' fields
*/
public Boolean getActivationStatus() {
// Prioritize 'enabled' if set, otherwise use 'active'
return enabled != null ? enabled : active;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public class ServiceTypeResponse {
private String name;
private String description;
private String category;
private BigDecimal price;
private Integer durationMinutes;
private BigDecimal basePriceLKR; // Changed from 'price' to match frontend
private Integer estimatedDurationMinutes; // Changed from 'durationMinutes' to match frontend
private Boolean active;
private Boolean requiresApproval;
private Integer dailyCapacity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,8 @@ public void deleteServiceType(String id, String deletedBy) {
ServiceType serviceType = serviceTypeRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Service type not found: " + id));

// Soft delete
serviceType.setActive(false);
serviceTypeRepository.save(serviceType);
// Hard delete - actually remove from database
serviceTypeRepository.delete(serviceType);

log.info("Service type deleted successfully: {}", id);
}
Expand All @@ -134,8 +133,8 @@ private ServiceTypeResponse convertToResponse(ServiceType serviceType) {
.name(serviceType.getName())
.description(serviceType.getDescription())
.category(serviceType.getCategory())
.price(serviceType.getPrice())
.durationMinutes(serviceType.getDefaultDurationMinutes())
.basePriceLKR(serviceType.getPrice())
.estimatedDurationMinutes(serviceType.getDefaultDurationMinutes())
.active(serviceType.getActive())
.requiresApproval(serviceType.getRequiresApproval())
.dailyCapacity(serviceType.getDailyCapacity())
Expand Down
Loading