diff --git a/src/main/java/com/honey/honey/config/AdminSecurityConfig.java b/src/main/java/com/honey/honey/config/AdminSecurityConfig.java index c9b9c54..fd43e6e 100644 --- a/src/main/java/com/honey/honey/config/AdminSecurityConfig.java +++ b/src/main/java/com/honey/honey/config/AdminSecurityConfig.java @@ -103,8 +103,8 @@ public class AdminSecurityConfig { .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/admin/login").permitAll() - .requestMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "GAME_ADMIN") + .requestMatchers("/api/admin/login", "/api/admin/chatwoot-session").permitAll() + .requestMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "GAME_ADMIN", "TICKETS_SUPPORT") .requestMatchers("/api/admin/payments/**").hasAnyRole("ADMIN", "GAME_ADMIN") .requestMatchers("/api/admin/payouts/**").hasAnyRole("ADMIN", "PAYOUT_SUPPORT", "GAME_ADMIN") .requestMatchers("/api/admin/rooms/**").hasAnyRole("ADMIN", "GAME_ADMIN") diff --git a/src/main/java/com/honey/honey/controller/AdminLoginController.java b/src/main/java/com/honey/honey/controller/AdminLoginController.java index 61f5ece..b61538d 100644 --- a/src/main/java/com/honey/honey/controller/AdminLoginController.java +++ b/src/main/java/com/honey/honey/controller/AdminLoginController.java @@ -2,8 +2,11 @@ package com.honey.honey.controller; import com.honey.honey.dto.AdminLoginRequest; import com.honey.honey.dto.AdminLoginResponse; +import com.honey.honey.dto.ChatwootSessionRequest; +import com.honey.honey.security.admin.JwtUtil; import com.honey.honey.service.AdminService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -19,6 +22,11 @@ import java.util.Optional; public class AdminLoginController { private final AdminService adminService; + private final JwtUtil jwtUtil; + + /** Shared secret with Chatwoot. Set via env CHATWOOT_INTEGRATION_SECRET (e.g. from VPS secret file). */ + @Value("${CHATWOOT_INTEGRATION_SECRET:}") + private String chatwootIntegrationSecret; @PostMapping("/login") public ResponseEntity login(@RequestBody AdminLoginRequest request) { @@ -47,5 +55,30 @@ public class AdminLoginController { role )); } + + /** + * Exchanges a Chatwoot integration API key for an admin JWT with ROLE_TICKETS_SUPPORT. + * Used when the admin panel is embedded in Chatwoot as a Dashboard App iframe. + * API key must match CHATWOOT_INTEGRATION_SECRET on the server. + */ + @PostMapping("/chatwoot-session") + public ResponseEntity chatwootSession(@RequestBody ChatwootSessionRequest request) { + if (request.getApiKey() == null || request.getApiKey().isBlank()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("apiKey is required"); + } + if (chatwootIntegrationSecret == null || chatwootIntegrationSecret.isBlank()) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Chatwoot integration is not configured (CHATWOOT_INTEGRATION_SECRET)"); + } + if (!chatwootIntegrationSecret.equals(request.getApiKey().trim())) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid apiKey"); + } + String token = jwtUtil.generateTokenWithRole("__chatwoot__", "ROLE_TICKETS_SUPPORT"); + return ResponseEntity.ok(new AdminLoginResponse( + token, + "__chatwoot__", + "ROLE_TICKETS_SUPPORT" + )); + } } diff --git a/src/main/java/com/honey/honey/dto/ChatwootSessionRequest.java b/src/main/java/com/honey/honey/dto/ChatwootSessionRequest.java new file mode 100644 index 0000000..0af4c27 --- /dev/null +++ b/src/main/java/com/honey/honey/dto/ChatwootSessionRequest.java @@ -0,0 +1,9 @@ +package com.honey.honey.dto; + +import lombok.Data; + +@Data +public class ChatwootSessionRequest { + /** API key shared with Chatwoot (Dashboard App). Must match CHATWOOT_INTEGRATION_SECRET on server. */ + private String apiKey; +} diff --git a/src/main/java/com/honey/honey/security/admin/JwtAuthenticationFilter.java b/src/main/java/com/honey/honey/security/admin/JwtAuthenticationFilter.java index 3b6c581..0d6f97e 100644 --- a/src/main/java/com/honey/honey/security/admin/JwtAuthenticationFilter.java +++ b/src/main/java/com/honey/honey/security/admin/JwtAuthenticationFilter.java @@ -43,11 +43,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (jwtUtil.validateToken(jwt, username)) { - // Get admin from database to retrieve actual role - String role = adminRepository.findByUsername(username) - .map(Admin::getRole) - .orElse("ROLE_ADMIN"); // Fallback to ROLE_ADMIN if not found - + // If token has explicit role (e.g. Chatwoot integration), use it; otherwise load from DB + String role = jwtUtil.getRoleFromToken(jwt); + if (role == null || role.isBlank()) { + role = adminRepository.findByUsername(username) + .map(Admin::getRole) + .orElse("ROLE_ADMIN"); + } UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( username, null, diff --git a/src/main/java/com/honey/honey/security/admin/JwtUtil.java b/src/main/java/com/honey/honey/security/admin/JwtUtil.java index 0a7892b..4d7eefa 100644 --- a/src/main/java/com/honey/honey/security/admin/JwtUtil.java +++ b/src/main/java/com/honey/honey/security/admin/JwtUtil.java @@ -33,6 +33,27 @@ public class JwtUtil { return createToken(claims, username); } + /** + * Generates a token with an explicit role (e.g. for Chatwoot integration). + * Used when the subject is not a DB admin; the filter will use this role from the token. + */ + public String generateTokenWithRole(String subject, String role) { + Map claims = new HashMap<>(); + claims.put("role", role); + return createToken(claims, subject); + } + + /** + * Returns the role from token claims, or null if not present. + */ + public String getRoleFromToken(String token) { + try { + return getClaimFromToken(token, claims -> claims.get("role", String.class)); + } catch (Exception e) { + return null; + } + } + private String createToken(Map claims, String subject) { return Jwts.builder() .claims(claims) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4d27811..deaadf5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -121,6 +121,7 @@ app: secret: ${APP_ADMIN_JWT_SECRET:change-this-to-a-secure-random-string-in-production-min-32-characters} # JWT expiration time in milliseconds (default: 24 hours) expiration: ${APP_ADMIN_JWT_EXPIRATION:86400000} + chatwoot-integration-secret: ${CHATWOOT_INTEGRATION_SECRET:} # GeoIP configuration # Set GEOIP_DB_PATH environment variable to use external file (recommended for production)