package com.xuqm.tenant.controller; import com.xuqm.common.model.ApiResponse; import com.xuqm.tenant.dto.CreateAppRequest; import com.xuqm.tenant.entity.AppEntity; import com.xuqm.tenant.entity.FeatureServiceEntity; import com.xuqm.tenant.entity.TenantEntity; import com.xuqm.tenant.repository.TenantRepository; import com.xuqm.tenant.service.AppService; import com.xuqm.tenant.service.AppUserClient; import com.xuqm.tenant.service.EmailService; import com.xuqm.tenant.service.FeatureServiceManager; import com.xuqm.tenant.service.OperationLogService; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/apps") public class AppController { private final AppService appService; private final EmailService emailService; private final OperationLogService operationLogService; private final TenantRepository tenantRepository; private final FeatureServiceManager featureServiceManager; private final AppUserClient appUserClient; public AppController(AppService appService, EmailService emailService, OperationLogService operationLogService, TenantRepository tenantRepository, FeatureServiceManager featureServiceManager, AppUserClient appUserClient) { this.appService = appService; this.emailService = emailService; this.operationLogService = operationLogService; this.tenantRepository = tenantRepository; this.featureServiceManager = featureServiceManager; this.appUserClient = appUserClient; } @GetMapping public ResponseEntity>> list(@AuthenticationPrincipal String tenantId) { return ResponseEntity.ok(ApiResponse.success(appService.listByTenant(tenantId))); } @GetMapping("/{appKey}") public ResponseEntity> get(@PathVariable String appKey, @AuthenticationPrincipal String tenantId) { return ResponseEntity.ok(ApiResponse.success(appService.getByAppKey(appKey, tenantId))); } @PostMapping public ResponseEntity> create(@Valid @RequestBody CreateAppRequest req, @AuthenticationPrincipal String tenantId) { return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req))); } @PutMapping("/{appKey}") public ResponseEntity> update(@PathVariable String appKey, @Valid @RequestBody CreateAppRequest req, @AuthenticationPrincipal String tenantId) { return ResponseEntity.ok(ApiResponse.success(appService.update(appKey, tenantId, req))); } @DeleteMapping("/{appKey}") public ResponseEntity> delete(@PathVariable String appKey, @AuthenticationPrincipal String tenantId) { appService.delete(appKey, tenantId); return ResponseEntity.ok(ApiResponse.ok()); } /** Step 1: send email verification code for secret reveal or reset. */ @PostMapping("/{appKey}/request-secret-verify") public ResponseEntity> requestSecretVerify( @PathVariable String appKey, @RequestParam String purpose, @AuthenticationPrincipal String tenantId) { appService.getByAppKey(appKey, tenantId); TenantEntity tenant = tenantRepository.findById(tenantId) .orElseThrow(() -> new RuntimeException("Tenant not found")); emailService.sendVerificationCode(tenant.getEmail(), purpose); operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REQUEST_SECRET_VERIFY", Map.of( "purpose", purpose )); return ResponseEntity.ok(ApiResponse.ok()); } /** Step 2a: verify code and return the full appSecret. */ @PostMapping("/{appKey}/reveal-secret") public ResponseEntity>> revealSecret( @PathVariable String appKey, @RequestBody Map body, @AuthenticationPrincipal String tenantId) { AppEntity app = appService.getByAppKey(appKey, tenantId); TenantEntity tenant = tenantRepository.findById(tenantId) .orElseThrow(() -> new RuntimeException("Tenant not found")); emailService.verify(tenant.getEmail(), body.get("code"), "REVEAL_SECRET"); operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REVEAL_APP_SECRET", Map.of( "appKey", app.getAppKey() )); return ResponseEntity.ok(ApiResponse.success(Map.of("appSecret", app.getAppSecret()))); } /** Step 2b: verify code and regenerate appSecret (old one invalidated immediately). */ @PostMapping("/{appKey}/reset-secret") public ResponseEntity>> resetSecret( @PathVariable String appKey, @RequestBody Map body, @AuthenticationPrincipal String tenantId) { AppEntity app = appService.getByAppKey(appKey, tenantId); TenantEntity tenant = tenantRepository.findById(tenantId) .orElseThrow(() -> new RuntimeException("Tenant not found")); emailService.verify(tenant.getEmail(), body.get("code"), "RESET_SECRET"); String newSecret = appService.resetSecret(appKey, tenantId); return ResponseEntity.ok(ApiResponse.success(Map.of("appSecret", newSecret))); } /** * List users of an app. Queries IM accounts when IM is enabled, Push accounts otherwise. * The ?source=IM|PUSH param overrides auto-detection. */ @GetMapping("/{appKey}/users") public ResponseEntity>> listUsers( @PathVariable String appKey, @RequestParam(required = false, defaultValue = "") String keyword, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String source, @AuthenticationPrincipal String tenantId) { appService.getByAppKey(appKey, tenantId); List services = featureServiceManager.listByApp(appKey); boolean imEnabled = services.stream().anyMatch(s -> s.getServiceType() == FeatureServiceEntity.ServiceType.IM && s.isEnabled()); boolean pushEnabled = services.stream().anyMatch(s -> s.getServiceType() == FeatureServiceEntity.ServiceType.PUSH && s.isEnabled()); String effectiveSource = (source != null && !source.isBlank()) ? source.trim().toUpperCase() : (imEnabled ? "IM" : "PUSH"); Map result = "PUSH".equals(effectiveSource) ? appUserClient.listPushUsers(appKey, keyword, page, size) : appUserClient.listImUsers(appKey, keyword, page, size); return ResponseEntity.ok(ApiResponse.success(result)); } }