chatwoot admin panel integration
All checks were successful
Deploy to VPS / deploy (push) Successful in 1m23s
All checks were successful
Deploy to VPS / deploy (push) Successful in 1m23s
This commit is contained in:
@@ -103,8 +103,8 @@ public class AdminSecurityConfig {
|
|||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/api/admin/login").permitAll()
|
.requestMatchers("/api/admin/login", "/api/admin/chatwoot-session").permitAll()
|
||||||
.requestMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
.requestMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "GAME_ADMIN", "TICKETS_SUPPORT")
|
||||||
.requestMatchers("/api/admin/payments/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
.requestMatchers("/api/admin/payments/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
||||||
.requestMatchers("/api/admin/payouts/**").hasAnyRole("ADMIN", "PAYOUT_SUPPORT", "GAME_ADMIN")
|
.requestMatchers("/api/admin/payouts/**").hasAnyRole("ADMIN", "PAYOUT_SUPPORT", "GAME_ADMIN")
|
||||||
.requestMatchers("/api/admin/rooms/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
.requestMatchers("/api/admin/rooms/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package com.honey.honey.controller;
|
|||||||
|
|
||||||
import com.honey.honey.dto.AdminLoginRequest;
|
import com.honey.honey.dto.AdminLoginRequest;
|
||||||
import com.honey.honey.dto.AdminLoginResponse;
|
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 com.honey.honey.service.AdminService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -19,6 +22,11 @@ import java.util.Optional;
|
|||||||
public class AdminLoginController {
|
public class AdminLoginController {
|
||||||
|
|
||||||
private final AdminService adminService;
|
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")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<?> login(@RequestBody AdminLoginRequest request) {
|
public ResponseEntity<?> login(@RequestBody AdminLoginRequest request) {
|
||||||
@@ -47,5 +55,30 @@ public class AdminLoginController {
|
|||||||
role
|
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"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -43,11 +43,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
if (jwtUtil.validateToken(jwt, username)) {
|
if (jwtUtil.validateToken(jwt, username)) {
|
||||||
// Get admin from database to retrieve actual role
|
// If token has explicit role (e.g. Chatwoot integration), use it; otherwise load from DB
|
||||||
String role = adminRepository.findByUsername(username)
|
String role = jwtUtil.getRoleFromToken(jwt);
|
||||||
.map(Admin::getRole)
|
if (role == null || role.isBlank()) {
|
||||||
.orElse("ROLE_ADMIN"); // Fallback to ROLE_ADMIN if not found
|
role = adminRepository.findByUsername(username)
|
||||||
|
.map(Admin::getRole)
|
||||||
|
.orElse("ROLE_ADMIN");
|
||||||
|
}
|
||||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||||
username,
|
username,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -33,6 +33,27 @@ public class JwtUtil {
|
|||||||
return createToken(claims, username);
|
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) {
|
private String createToken(Map<String, Object> claims, String subject) {
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.claims(claims)
|
.claims(claims)
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ app:
|
|||||||
secret: ${APP_ADMIN_JWT_SECRET:change-this-to-a-secure-random-string-in-production-min-32-characters}
|
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)
|
# JWT expiration time in milliseconds (default: 24 hours)
|
||||||
expiration: ${APP_ADMIN_JWT_EXPIRATION:86400000}
|
expiration: ${APP_ADMIN_JWT_EXPIRATION:86400000}
|
||||||
|
chatwoot-integration-secret: ${CHATWOOT_INTEGRATION_SECRET:}
|
||||||
|
|
||||||
# GeoIP configuration
|
# GeoIP configuration
|
||||||
# Set GEOIP_DB_PATH environment variable to use external file (recommended for production)
|
# Set GEOIP_DB_PATH environment variable to use external file (recommended for production)
|
||||||
|
|||||||
Reference in New Issue
Block a user