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

This commit is contained in:
Tihon
2026-03-16 18:48:46 +02:00
parent 2779e7a1c1
commit 0c0bb5a5bc
8 changed files with 43 additions and 43 deletions

View File

@@ -104,13 +104,13 @@ public class AdminSecurityConfig {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/login", "/api/admin/chatwoot-session").permitAll() .requestMatchers("/api/admin/login", "/api/admin/chatwoot-session").permitAll()
.requestMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "GAME_ADMIN", "TICKETS_SUPPORT") .requestMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "SUPPORT")
.requestMatchers("/api/admin/payments/**").hasAnyRole("ADMIN", "GAME_ADMIN") .requestMatchers("/api/admin/payments/**").hasAnyRole("ADMIN", "SUPPORT")
.requestMatchers("/api/admin/payouts/**").hasAnyRole("ADMIN", "PAYOUT_SUPPORT", "GAME_ADMIN") .requestMatchers("/api/admin/payouts/**").hasAnyRole("ADMIN", "SUPPORT")
.requestMatchers("/api/admin/rooms/**").hasAnyRole("ADMIN", "GAME_ADMIN") .requestMatchers("/api/admin/rooms/**").hasAnyRole("ADMIN")
.requestMatchers("/api/admin/configurations/**").hasAnyRole("ADMIN", "GAME_ADMIN") .requestMatchers("/api/admin/configurations/**").hasAnyRole("ADMIN")
.requestMatchers("/api/admin/tickets/**").hasAnyRole("ADMIN", "TICKETS_SUPPORT", "GAME_ADMIN") .requestMatchers("/api/admin/tickets/**").hasAnyRole("ADMIN")
.requestMatchers("/api/admin/quick-answers/**").hasAnyRole("ADMIN", "TICKETS_SUPPORT", "GAME_ADMIN") .requestMatchers("/api/admin/quick-answers/**").hasAnyRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().denyAll() .anyRequest().denyAll()
) )

View File

@@ -57,7 +57,7 @@ public class AdminLoginController {
} }
/** /**
* Exchanges a Chatwoot integration API key for an admin JWT with ROLE_TICKETS_SUPPORT. * Exchanges a Chatwoot integration API key for an admin JWT with ROLE_SUPPORT.
* Used when the admin panel is embedded in Chatwoot as a Dashboard App iframe. * Used when the admin panel is embedded in Chatwoot as a Dashboard App iframe.
* API key must match CHATWOOT_INTEGRATION_SECRET on the server. * API key must match CHATWOOT_INTEGRATION_SECRET on the server.
*/ */
@@ -73,11 +73,11 @@ public class AdminLoginController {
if (!chatwootIntegrationSecret.equals(request.getApiKey().trim())) { if (!chatwootIntegrationSecret.equals(request.getApiKey().trim())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid apiKey"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid apiKey");
} }
String token = jwtUtil.generateTokenWithRole("__chatwoot__", "ROLE_TICKETS_SUPPORT"); String token = jwtUtil.generateTokenWithRole("__chatwoot__", "ROLE_SUPPORT");
return ResponseEntity.ok(new AdminLoginResponse( return ResponseEntity.ok(new AdminLoginResponse(
token, token,
"__chatwoot__", "__chatwoot__",
"ROLE_TICKETS_SUPPORT" "ROLE_SUPPORT"
)); ));
} }
} }

View File

@@ -7,8 +7,6 @@ import com.honey.honey.repository.PaymentRepository;
import com.honey.honey.repository.UserARepository; import com.honey.honey.repository.UserARepository;
import com.honey.honey.repository.UserDRepository; import com.honey.honey.repository.UserDRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -16,6 +14,8 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@@ -31,18 +31,18 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/admin/payments") @RequestMapping("/api/admin/payments")
@RequiredArgsConstructor @RequiredArgsConstructor
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public class AdminPaymentController { public class AdminPaymentController {
private final PaymentRepository paymentRepository; private final PaymentRepository paymentRepository;
private final UserARepository userARepository; private final UserARepository userARepository;
private final UserDRepository userDRepository; private final UserDRepository userDRepository;
private boolean isGameAdmin() { private static boolean isSupport() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || auth.getAuthorities() == null) return false; if (auth == null || auth.getAuthorities() == null) return false;
return auth.getAuthorities().stream() return auth.getAuthorities().stream()
.anyMatch(a -> "ROLE_GAME_ADMIN".equals(a.getAuthority())); .anyMatch(a -> "ROLE_SUPPORT".equals(a.getAuthority()));
} }
@GetMapping @GetMapping
@@ -70,7 +70,7 @@ public class AdminPaymentController {
Pageable pageable = PageRequest.of(page, size, sort); Pageable pageable = PageRequest.of(page, size, sort);
List<Integer> masterIds = isGameAdmin() ? userDRepository.findMasterUserIds() : List.of(); List<Integer> masterIds = isSupport() ? userDRepository.findMasterUserIds() : List.of();
// Build specification // Build specification
Specification<Payment> spec = (root, query, cb) -> { Specification<Payment> spec = (root, query, cb) -> {

View File

@@ -35,7 +35,7 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/admin/payouts") @RequestMapping("/api/admin/payouts")
@RequiredArgsConstructor @RequiredArgsConstructor
@PreAuthorize("hasAnyRole('ADMIN', 'PAYOUT_SUPPORT', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public class AdminPayoutController { public class AdminPayoutController {
private final PayoutRepository payoutRepository; private final PayoutRepository payoutRepository;
@@ -45,11 +45,11 @@ public class AdminPayoutController {
private final PayoutService payoutService; private final PayoutService payoutService;
private final LocalizationService localizationService; private final LocalizationService localizationService;
private boolean isGameAdmin() { private static boolean isSupport() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || auth.getAuthorities() == null) return false; if (auth == null || auth.getAuthorities() == null) return false;
return auth.getAuthorities().stream() return auth.getAuthorities().stream()
.anyMatch(a -> "ROLE_GAME_ADMIN".equals(a.getAuthority())); .anyMatch(a -> "ROLE_SUPPORT".equals(a.getAuthority()));
} }
@GetMapping @GetMapping
@@ -74,7 +74,7 @@ public class AdminPayoutController {
Pageable pageable = PageRequest.of(page, size, sort); Pageable pageable = PageRequest.of(page, size, sort);
List<Integer> masterIds = isGameAdmin() ? userDRepository.findMasterUserIds() : List.of(); List<Integer> masterIds = isSupport() ? userDRepository.findMasterUserIds() : List.of();
// Build specification // Build specification
Specification<Payout> spec = (root, query, cb) -> { Specification<Payout> spec = (root, query, cb) -> {
@@ -156,7 +156,7 @@ public class AdminPayoutController {
} }
@PostMapping("/{id}/complete") @PostMapping("/{id}/complete")
@PreAuthorize("hasAnyRole('ADMIN', 'PAYOUT_SUPPORT')") @PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> completePayout(@PathVariable Long id) { public ResponseEntity<?> completePayout(@PathVariable Long id) {
try { try {
Optional<Payout> payoutOpt = payoutRepository.findById(id); Optional<Payout> payoutOpt = payoutRepository.findById(id);
@@ -179,7 +179,7 @@ public class AdminPayoutController {
} }
@PostMapping("/{id}/cancel") @PostMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('ADMIN', 'PAYOUT_SUPPORT')") @PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> cancelPayout(@PathVariable Long id) { public ResponseEntity<?> cancelPayout(@PathVariable Long id) {
try { try {
Optional<Payout> payoutOpt = payoutRepository.findById(id); Optional<Payout> payoutOpt = payoutRepository.findById(id);

View File

@@ -35,7 +35,7 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/admin/tickets") @RequestMapping("/api/admin/tickets")
@RequiredArgsConstructor @RequiredArgsConstructor
@PreAuthorize("hasAnyRole('ADMIN', 'TICKETS_SUPPORT', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN')")
public class AdminSupportTicketController { public class AdminSupportTicketController {
private final SupportTicketRepository supportTicketRepository; private final SupportTicketRepository supportTicketRepository;

View File

@@ -25,6 +25,13 @@ import java.util.Set;
@RequiredArgsConstructor @RequiredArgsConstructor
public class AdminUserController { public class AdminUserController {
private static boolean isSupport() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || auth.getAuthorities() == null) return false;
return auth.getAuthorities().stream()
.anyMatch(a -> "ROLE_SUPPORT".equals(a.getAuthority()));
}
/** Sortable fields: UserA properties plus UserB/UserD (handled via custom query in service). */ /** Sortable fields: UserA properties plus UserB/UserD (handled via custom query in service). */
private static final Set<String> SORTABLE_FIELDS = Set.of( private static final Set<String> SORTABLE_FIELDS = Set.of(
"id", "screenName", "telegramId", "telegramName", "isPremium", "id", "screenName", "telegramId", "telegramName", "isPremium",
@@ -37,15 +44,8 @@ public class AdminUserController {
private final AdminUserService adminUserService; private final AdminUserService adminUserService;
private final UserService userService; private final UserService userService;
private boolean isGameAdmin() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || auth.getAuthorities() == null) return false;
return auth.getAuthorities().stream()
.anyMatch(a -> "ROLE_GAME_ADMIN".equals(a.getAuthority()));
}
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<Map<String, Object>> getUsers( public ResponseEntity<Map<String, Object>> getUsers(
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size, @RequestParam(defaultValue = "50") int size,
@@ -85,7 +85,7 @@ public class AdminUserController {
Long balanceMinBigint = balanceMin != null ? balanceMin * 1000000L : null; Long balanceMinBigint = balanceMin != null ? balanceMin * 1000000L : null;
Long balanceMaxBigint = balanceMax != null ? balanceMax * 1000000L : null; Long balanceMaxBigint = balanceMax != null ? balanceMax * 1000000L : null;
boolean excludeMasters = isGameAdmin(); boolean excludeMasters = isSupport();
Page<AdminUserDto> dtoPage = adminUserService.getUsers( Page<AdminUserDto> dtoPage = adminUserService.getUsers(
pageable, pageable,
search, search,
@@ -124,7 +124,7 @@ public class AdminUserController {
* Chatwoot (Telegram channel) often uses Telegram user ID as contact identifier. * Chatwoot (Telegram channel) often uses Telegram user ID as contact identifier.
*/ */
@GetMapping("/by-telegram-id") @GetMapping("/by-telegram-id")
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN', 'TICKETS_SUPPORT')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<Map<String, Integer>> getUserByTelegramId(@RequestParam("telegram_id") Long telegramId) { public ResponseEntity<Map<String, Integer>> getUserByTelegramId(@RequestParam("telegram_id") Long telegramId) {
if (telegramId == null) { if (telegramId == null) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
@@ -135,9 +135,9 @@ public class AdminUserController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN', 'TICKETS_SUPPORT')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<AdminUserDetailDto> getUserDetail(@PathVariable Integer id) { public ResponseEntity<AdminUserDetailDto> getUserDetail(@PathVariable Integer id) {
AdminUserDetailDto userDetail = adminUserService.getUserDetail(id, isGameAdmin()); AdminUserDetailDto userDetail = adminUserService.getUserDetail(id, isSupport());
if (userDetail == null) { if (userDetail == null) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
@@ -145,7 +145,7 @@ public class AdminUserController {
} }
@GetMapping("/{id}/transactions") @GetMapping("/{id}/transactions")
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<Map<String, Object>> getUserTransactions( public ResponseEntity<Map<String, Object>> getUserTransactions(
@PathVariable Integer id, @PathVariable Integer id,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@@ -167,7 +167,7 @@ public class AdminUserController {
} }
@GetMapping("/{id}/payments") @GetMapping("/{id}/payments")
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<Map<String, Object>> getUserPayments( public ResponseEntity<Map<String, Object>> getUserPayments(
@PathVariable Integer id, @PathVariable Integer id,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@@ -190,7 +190,7 @@ public class AdminUserController {
} }
@GetMapping("/{id}/payouts") @GetMapping("/{id}/payouts")
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<Map<String, Object>> getUserPayouts( public ResponseEntity<Map<String, Object>> getUserPayouts(
@PathVariable Integer id, @PathVariable Integer id,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@@ -213,14 +213,14 @@ public class AdminUserController {
} }
@GetMapping("/{id}/tasks") @GetMapping("/{id}/tasks")
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<Map<String, Object>> getUserTasks(@PathVariable Integer id) { public ResponseEntity<Map<String, Object>> getUserTasks(@PathVariable Integer id) {
Map<String, Object> tasks = adminUserService.getUserTasks(id); Map<String, Object> tasks = adminUserService.getUserTasks(id);
return ResponseEntity.ok(tasks); return ResponseEntity.ok(tasks);
} }
@PatchMapping("/{id}/ban") @PatchMapping("/{id}/ban")
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<?> setUserBanned( public ResponseEntity<?> setUserBanned(
@PathVariable Integer id, @PathVariable Integer id,
@RequestBody Map<String, Boolean> body) { @RequestBody Map<String, Boolean> body) {
@@ -237,7 +237,7 @@ public class AdminUserController {
} }
@PatchMapping("/{id}/withdrawals-enabled") @PatchMapping("/{id}/withdrawals-enabled")
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
public ResponseEntity<?> setWithdrawalsEnabled( public ResponseEntity<?> setWithdrawalsEnabled(
@PathVariable Integer id, @PathVariable Integer id,
@RequestBody Map<String, Boolean> body) { @RequestBody Map<String, Boolean> body) {

View File

@@ -21,7 +21,7 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/admin/quick-answers") @RequestMapping("/api/admin/quick-answers")
@RequiredArgsConstructor @RequiredArgsConstructor
@PreAuthorize("hasAnyRole('ADMIN', 'TICKETS_SUPPORT', 'GAME_ADMIN')") @PreAuthorize("hasAnyRole('ADMIN')")
public class QuickAnswerController { public class QuickAnswerController {
private final QuickAnswerRepository quickAnswerRepository; private final QuickAnswerRepository quickAnswerRepository;

View File

@@ -88,7 +88,7 @@ public interface UserDRepository extends JpaRepository<UserD, Integer> {
List<UserD> findAllMasters(); List<UserD> findAllMasters();
/** /**
* IDs of users who are Masters (id = master_id and master_id > 0). Used to exclude them from GAME_ADMIN views. * IDs of users who are Masters (id = master_id and master_id > 0).
*/ */
@Query("SELECT d.id FROM UserD d WHERE d.id = d.masterId AND d.masterId > 0") @Query("SELECT d.id FROM UserD d WHERE d.id = d.masterId AND d.masterId > 0")
List<Integer> findMasterUserIds(); List<Integer> findMasterUserIds();