chatwoot admin panel integration
All checks were successful
Deploy to VPS / deploy (push) Successful in 1m23s

This commit is contained in:
Tihon
2026-03-16 17:00:55 +02:00
parent bd260497f9
commit 284fd07bea
6 changed files with 73 additions and 7 deletions

View File

@@ -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")

View File

@@ -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"
));
}
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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<String, Object> 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<String, Object> claims, String subject) {
return Jwts.builder()
.claims(claims)

View File

@@ -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)