API Dashboard added
This commit is contained in:
@@ -82,6 +82,8 @@ public class SecurityConfig {
|
||||
.requestMatchers(HttpMethod.PUT, "/api/auth/users/*").authenticated()
|
||||
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
|
||||
.requestMatchers(HttpMethod.POST, "/api/orders/*/cancel").authenticated()
|
||||
.requestMatchers("/api/developer-keys/**").authenticated()
|
||||
.requestMatchers("/api/developer/v1/**").permitAll()
|
||||
.requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF")
|
||||
|
||||
// ── STAFF/MANAGER/MASTER: All other management APIs ──
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
package com.rit.canteen.sales.controller;
|
||||
|
||||
import com.rit.canteen.sales.model.*;
|
||||
import com.rit.canteen.sales.repository.*;
|
||||
import com.rit.canteen.sales.service.DeveloperApiKeyService;
|
||||
import com.rit.canteen.sales.service.TokenService;
|
||||
import com.rit.canteen.sales.service.SystemNotificationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/developer/v1")
|
||||
public class DeveloperApiController {
|
||||
|
||||
@Autowired
|
||||
private DeveloperApiKeyService keyService;
|
||||
|
||||
@Autowired
|
||||
private StallRepository stallRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private TokenTransactionRepository transactionRepository;
|
||||
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
@Autowired
|
||||
private SystemNotificationService notificationService;
|
||||
|
||||
@Autowired
|
||||
private StockUpdateController stockUpdateController;
|
||||
|
||||
// ── Helper to authenticate key ─────────────────────────────────────────
|
||||
private DeveloperApiKey authenticate(String headerKey) {
|
||||
if (headerKey == null || headerKey.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return keyService.validateAndUseKey(headerKey).orElse(null);
|
||||
}
|
||||
|
||||
private boolean checkPermission(DeveloperApiKey key, String requiredScope) {
|
||||
if (key == null) return false;
|
||||
|
||||
// If the operation requires write access but the key doesn't have it, block immediately.
|
||||
if (requiredScope.startsWith("WRITE_") && !key.isWriteAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it's a customer key, they only get default read-only scopes.
|
||||
if (!"SYSTEM".equalsIgnoreCase(key.getUserType())) {
|
||||
return requiredScope.startsWith("READ_");
|
||||
}
|
||||
|
||||
// System users get custom scopes.
|
||||
String scopes = key.getPermissions();
|
||||
if (scopes == null || scopes.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Arrays.stream(scopes.split(","))
|
||||
.map(String::trim)
|
||||
.anyMatch(scope -> scope.equalsIgnoreCase(requiredScope));
|
||||
}
|
||||
|
||||
// ── 1. Validate API Key ────────────────────────────────────────────────
|
||||
@GetMapping("/validate")
|
||||
public ResponseEntity<?> validateKey(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", "VALID",
|
||||
"name", key.getName(),
|
||||
"ownerType", key.getUserType(),
|
||||
"createdAt", key.getCreatedAt(),
|
||||
"readOnly", !key.isWriteAccess(),
|
||||
"permissions", key.getPermissions() != null ? Arrays.asList(key.getPermissions().split(",")) : Collections.emptyList()
|
||||
));
|
||||
}
|
||||
|
||||
// ── 2. Get Stalls (Read-only, non-financial) ───────────────────────────
|
||||
@GetMapping("/stalls")
|
||||
public ResponseEntity<?> getStalls(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "READ_STALLS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_STALLS permission required"));
|
||||
}
|
||||
|
||||
List<Stall> stalls = stallRepository.findAll();
|
||||
// Map to DTO to avoid circular references and hide sensitive fields
|
||||
List<Map<String, Object>> result = stalls.stream().map(s -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("id", s.getId());
|
||||
map.put("name", s.getName());
|
||||
map.put("description", s.getDescription());
|
||||
map.put("active", s.isActive());
|
||||
map.put("temporarilyClosed", s.isTemporarilyClosed());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 3. Get Products (Read-only, non-financial) ─────────────────────────
|
||||
@GetMapping("/products")
|
||||
public ResponseEntity<?> getProducts(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "READ_PRODUCTS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_PRODUCTS permission required"));
|
||||
}
|
||||
|
||||
List<Product> products = productRepository.findAll();
|
||||
List<Map<String, Object>> result = products.stream().map(p -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("id", p.getId());
|
||||
map.put("productId", p.getProductId());
|
||||
map.put("name", p.getName());
|
||||
map.put("category", p.getCategory());
|
||||
map.put("price", p.getPrice()); // List price is fine for catalog, not a financial report
|
||||
map.put("stock", p.getStock());
|
||||
map.put("active", p.isActive());
|
||||
map.put("isDraft", p.isDraft());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/wallet")
|
||||
public ResponseEntity<?> getWallet(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@RequestParam(required = false) String mobileNumber,
|
||||
@RequestParam(required = false) Long userId) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "READ_WALLETS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_WALLETS permission required"));
|
||||
}
|
||||
|
||||
// ── Check if checking balance for another user by mobileNumber or userId ──
|
||||
if (mobileNumber != null || userId != null) {
|
||||
Optional<User> targetUserOpt = Optional.empty();
|
||||
if (userId != null) {
|
||||
targetUserOpt = userRepository.findById(userId);
|
||||
} else {
|
||||
targetUserOpt = userRepository.findByMobileNumber(mobileNumber);
|
||||
}
|
||||
|
||||
if (targetUserOpt.isPresent()) {
|
||||
User user = targetUserOpt.get();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"userMobile", user.getMobileNumber(),
|
||||
"userName", user.getName() != null ? user.getName() : "Customer",
|
||||
"ritzTokenBalance", user.getRitzTokenBalance(),
|
||||
"currency", "Ritz Token",
|
||||
"queryScope", "SPECIFIC_USER"
|
||||
));
|
||||
} else {
|
||||
return ResponseEntity.status(404).body(Map.of("error", "Target user not found"));
|
||||
}
|
||||
}
|
||||
|
||||
if ("CUSTOMER".equals(key.getUserType())) {
|
||||
// Customer key: return their specific wallet balance
|
||||
Optional<User> userOpt = userRepository.findById(key.getUserId());
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"userMobile", user.getMobileNumber(),
|
||||
"userName", user.getName() != null ? user.getName() : "Customer",
|
||||
"ritzTokenBalance", user.getRitzTokenBalance(),
|
||||
"currency", "Ritz Token",
|
||||
"queryScope", "OWNER"
|
||||
));
|
||||
}
|
||||
return ResponseEntity.status(404).body(Map.of("error", "Owner user not found"));
|
||||
} else {
|
||||
// System key: return overall token circulation stats, but NO individual user accounts or detailed ledgers
|
||||
List<User> allUsers = userRepository.findAll();
|
||||
BigDecimal totalCirculation = allUsers.stream()
|
||||
.map(User::getRitzTokenBalance)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"scope", "SYSTEM_CIRCULATION",
|
||||
"activeWalletsCount", allUsers.size(),
|
||||
"totalCirculationBalance", totalCirculation,
|
||||
"currency", "Ritz Token",
|
||||
"queryScope", "SYSTEM"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5. Get Orders (requires READ_ORDERS) ────────────────────────────────
|
||||
@GetMapping("/orders")
|
||||
public ResponseEntity<?> getOrders(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "READ_ORDERS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_ORDERS permission required"));
|
||||
}
|
||||
|
||||
List<Order> orders;
|
||||
if ("CUSTOMER".equals(key.getUserType())) {
|
||||
orders = orderRepository.findByUserIdOrderByCreatedAtDesc(key.getUserId());
|
||||
} else {
|
||||
orders = orderRepository.findAll();
|
||||
}
|
||||
|
||||
// Map to DTO. Financial details (totalAmount, paymentMethod, item prices) are only exposed for staff/SYSTEM keys.
|
||||
List<Map<String, Object>> result = orders.stream().map(o -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("id", o.getId());
|
||||
map.put("orderNumber", o.getOrderNumber());
|
||||
map.put("displayOrderId", o.getDisplayOrderId());
|
||||
map.put("status", o.getStatus());
|
||||
map.put("createdAt", o.getCreatedAt());
|
||||
map.put("orderType", o.getOrderType());
|
||||
|
||||
if ("SYSTEM".equalsIgnoreCase(key.getUserType())) {
|
||||
map.put("totalAmount", o.getTotalAmount());
|
||||
map.put("paymentMethod", o.getPaymentMethod());
|
||||
}
|
||||
|
||||
List<Map<String, Object>> itemsList = o.getItems().stream().map(item -> {
|
||||
Map<String, Object> itemMap = new HashMap<>();
|
||||
itemMap.put("productName", item.getProductName());
|
||||
itemMap.put("quantity", item.getQuantity());
|
||||
itemMap.put("stallName", item.getStallName());
|
||||
if ("SYSTEM".equalsIgnoreCase(key.getUserType())) {
|
||||
itemMap.put("price", item.getPrice());
|
||||
}
|
||||
return itemMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
map.put("items", itemsList);
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 5a. Get Wallet Transactions (requires READ_WALLETS & SYSTEM key) ─────
|
||||
@GetMapping("/wallet/transactions")
|
||||
public ResponseEntity<?> getWalletTransactions(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!"SYSTEM".equalsIgnoreCase(key.getUserType())) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: Only staff keys can view general transaction records"));
|
||||
}
|
||||
if (!checkPermission(key, "READ_WALLETS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_WALLETS permission required"));
|
||||
}
|
||||
return ResponseEntity.ok(tokenService.getAllTransactions());
|
||||
}
|
||||
|
||||
// ── 5b. Get Wallet Circulation Stats (requires READ_WALLETS & SYSTEM key) ──
|
||||
@GetMapping("/wallet/stats")
|
||||
public ResponseEntity<?> getWalletStats(@RequestHeader(value = "X-Developer-Key", required = false) String headerKey) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!"SYSTEM".equalsIgnoreCase(key.getUserType())) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: Only staff keys can view wallet statistics"));
|
||||
}
|
||||
if (!checkPermission(key, "READ_WALLETS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: READ_WALLETS permission required"));
|
||||
}
|
||||
return ResponseEntity.ok(tokenService.getGlobalStats());
|
||||
}
|
||||
|
||||
// ── 6. Create Stall (requires WRITE_STALLS) ───────────────────────────
|
||||
@PostMapping("/stalls")
|
||||
public ResponseEntity<?> createStall(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@RequestBody Stall stall) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_STALLS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_STALLS permission required"));
|
||||
}
|
||||
Stall saved = stallRepository.save(stall);
|
||||
return ResponseEntity.status(201).body(saved);
|
||||
}
|
||||
|
||||
// ── 7. Update Stall (requires WRITE_STALLS) ───────────────────────────
|
||||
@PutMapping("/stalls/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateStall(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@PathVariable Long id,
|
||||
@RequestBody Stall updatedStall) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_STALLS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_STALLS permission required"));
|
||||
}
|
||||
return stallRepository.findById(id).map(stall -> {
|
||||
stall.setName(updatedStall.getName());
|
||||
stall.setDescription(updatedStall.getDescription());
|
||||
stall.setActive(updatedStall.isActive());
|
||||
stall.setTemporarilyClosed(updatedStall.isTemporarilyClosed());
|
||||
stall.setSessionOptional(updatedStall.isSessionOptional());
|
||||
return ResponseEntity.ok(stallRepository.save(stall));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── 8. Delete Stall (requires WRITE_STALLS) ───────────────────────────
|
||||
@DeleteMapping("/stalls/{id}")
|
||||
public ResponseEntity<?> deleteStall(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@PathVariable Long id) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_STALLS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_STALLS permission required"));
|
||||
}
|
||||
return stallRepository.findById(id).map(stall -> {
|
||||
stallRepository.delete(stall);
|
||||
return ResponseEntity.ok(Map.of("success", true, "message", "Stall deleted successfully"));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── 9. Create Product (requires WRITE_PRODUCTS) ───────────────────────
|
||||
@PostMapping("/products")
|
||||
@Transactional
|
||||
public ResponseEntity<?> createProduct(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@RequestBody Product product) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_PRODUCTS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_PRODUCTS permission required"));
|
||||
}
|
||||
Product saved = productRepository.save(product);
|
||||
if (saved.isDraft()) {
|
||||
notificationService.createNotification(
|
||||
"Draft Product Created via API",
|
||||
"A product draft '" + saved.getName() + "' was created via developer API key.",
|
||||
"PRODUCT",
|
||||
"/inventory/products"
|
||||
);
|
||||
}
|
||||
return ResponseEntity.status(201).body(saved);
|
||||
}
|
||||
|
||||
// ── 10. Update Product (requires WRITE_PRODUCTS) ───────────────────────
|
||||
@PutMapping("/products/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateProduct(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@PathVariable Long id,
|
||||
@RequestBody Product details) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_PRODUCTS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_PRODUCTS permission required"));
|
||||
}
|
||||
return productRepository.findById(id).map(p -> {
|
||||
p.setProductId(details.getProductId());
|
||||
p.setName(details.getName());
|
||||
p.setCategory(details.getCategory());
|
||||
p.setDescription(details.getDescription());
|
||||
p.setPrice(details.getPrice());
|
||||
p.setStock(details.getStock());
|
||||
p.setActive(details.isActive());
|
||||
p.setVeg(details.isVeg());
|
||||
|
||||
Product updated = productRepository.save(p);
|
||||
stockUpdateController.broadcastStockUpdate(updated.getId(), updated.getStock());
|
||||
return ResponseEntity.ok(updated);
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── 11. Delete Product (requires WRITE_PRODUCTS) ──────────────────────
|
||||
@DeleteMapping("/products/{id}")
|
||||
public ResponseEntity<?> deleteProduct(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@PathVariable Long id) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_PRODUCTS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_PRODUCTS permission required"));
|
||||
}
|
||||
return productRepository.findById(id).map(p -> {
|
||||
productRepository.delete(p);
|
||||
return ResponseEntity.ok(Map.of("success", true, "message", "Product deleted successfully"));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── 12. Update Order Status (requires WRITE_ORDERS) ───────────────────
|
||||
@PatchMapping("/orders/{id}/status")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateOrderStatus(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, String> body) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_ORDERS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_ORDERS permission required"));
|
||||
}
|
||||
String newStatus = body.get("status");
|
||||
if (newStatus == null || newStatus.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Status field is required"));
|
||||
}
|
||||
return orderRepository.findById(id).map(order -> {
|
||||
String oldStatus = order.getStatus();
|
||||
String nextStatus = newStatus.toUpperCase();
|
||||
if ("CANCELLED".equals(nextStatus) && !"CANCELLED".equals(oldStatus)) {
|
||||
if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
|
||||
tokenService.refund(order.getUserId(), "ORD-" + order.getDisplayOrderId(),
|
||||
order.getTotalAmount(), "Status changed to CANCELLED via API");
|
||||
}
|
||||
}
|
||||
order.setStatus(nextStatus);
|
||||
orderRepository.save(order);
|
||||
return ResponseEntity.ok(Map.of("success", true, "message", "Order status updated to " + nextStatus));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── 13. Wallet Topup (requires WRITE_WALLETS) ──────────────────────────
|
||||
@PostMapping("/wallet/topup")
|
||||
@Transactional
|
||||
public ResponseEntity<?> walletTopup(
|
||||
@RequestHeader(value = "X-Developer-Key", required = false) String headerKey,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
DeveloperApiKey key = authenticate(headerKey);
|
||||
if (key == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Invalid or missing X-Developer-Key header"));
|
||||
}
|
||||
if (!checkPermission(key, "WRITE_WALLETS")) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Access Denied: WRITE_WALLETS permission required"));
|
||||
}
|
||||
|
||||
try {
|
||||
Long targetUserId = null;
|
||||
if (body.containsKey("userId") && body.get("userId") != null) {
|
||||
targetUserId = Long.valueOf(body.get("userId").toString());
|
||||
} else if (body.containsKey("mobileNumber") && body.get("mobileNumber") != null) {
|
||||
String mobile = body.get("mobileNumber").toString();
|
||||
User targetUser = userRepository.findByMobileNumber(mobile).orElse(null);
|
||||
if (targetUser != null) {
|
||||
targetUserId = targetUser.getId();
|
||||
} else {
|
||||
return ResponseEntity.status(404).body(Map.of("error", "User with mobile number " + mobile + " not found"));
|
||||
}
|
||||
} else {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "userId or mobileNumber is required"));
|
||||
}
|
||||
|
||||
BigDecimal amount = new BigDecimal(body.get("amount").toString());
|
||||
if (amount.compareTo(new BigDecimal("50")) < 0) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Minimum top up amount is 50 tokens"));
|
||||
}
|
||||
if (amount.compareTo(new BigDecimal("5000")) > 0) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Single transaction limit exceeded (Max: 5000 tokens)"));
|
||||
}
|
||||
|
||||
String ref = body.getOrDefault("referenceId", "API-TOPUP-" + System.currentTimeMillis()).toString();
|
||||
User updatedUser = tokenService.topUp(targetUserId, amount, ref);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"newBalance", updatedUser.getRitzTokenBalance(),
|
||||
"message", "Successfully added " + amount + " Ritz Tokens via API Key"
|
||||
));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.rit.canteen.sales.controller;
|
||||
|
||||
import com.rit.canteen.sales.model.DeveloperApiKey;
|
||||
import com.rit.canteen.sales.service.DeveloperApiKeyService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/developer-keys")
|
||||
public class DeveloperApiKeyController {
|
||||
|
||||
@Autowired
|
||||
private DeveloperApiKeyService keyService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> listKeys() {
|
||||
UserContext context = getUserContext();
|
||||
if (context == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
|
||||
}
|
||||
List<DeveloperApiKey> keys = keyService.getKeysForUser(context.userId, context.userType);
|
||||
return ResponseEntity.ok(keys);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> createKey(@RequestBody Map<String, Object> body) {
|
||||
UserContext context = getUserContext();
|
||||
if (context == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
|
||||
}
|
||||
String name = body.containsKey("name") && body.get("name") != null
|
||||
? body.get("name").toString()
|
||||
: "My API Key";
|
||||
|
||||
boolean writeAccess = false;
|
||||
String permissions = "";
|
||||
|
||||
if ("SYSTEM".equalsIgnoreCase(context.userType)) {
|
||||
Object writeVal = body.get("writeAccess");
|
||||
if (writeVal instanceof Boolean) {
|
||||
writeAccess = (Boolean) writeVal;
|
||||
} else if (writeVal != null) {
|
||||
writeAccess = Boolean.parseBoolean(writeVal.toString());
|
||||
}
|
||||
permissions = body.containsKey("permissions") && body.get("permissions") != null
|
||||
? body.get("permissions").toString()
|
||||
: "";
|
||||
} else {
|
||||
permissions = "READ_PRODUCTS,READ_STALLS,READ_ORDERS,READ_WALLETS";
|
||||
}
|
||||
|
||||
try {
|
||||
DeveloperApiKey newKey = keyService.createKey(context.userId, context.userType, context.identifier, name, writeAccess, permissions);
|
||||
return ResponseEntity.status(201).body(newKey);
|
||||
} catch (IllegalStateException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteKey(@PathVariable Long id) {
|
||||
UserContext context = getUserContext();
|
||||
if (context == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
|
||||
}
|
||||
try {
|
||||
keyService.deleteKey(id, context.userId, context.userType);
|
||||
return ResponseEntity.ok(Map.of("success", true, "message", "API Key revoked successfully"));
|
||||
} catch (SecurityException e) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helper UserContext parser ──────────────────────────────────────────
|
||||
|
||||
private UserContext getUserContext() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || !auth.isAuthenticated()) return null;
|
||||
|
||||
if (auth.getDetails() instanceof Claims claims) {
|
||||
Long userId;
|
||||
Object uid = claims.get("userId");
|
||||
if (uid instanceof Integer) {
|
||||
userId = ((Integer) uid).longValue();
|
||||
} else if (uid instanceof Long) {
|
||||
userId = (Long) uid;
|
||||
} else if (uid != null) {
|
||||
userId = Long.valueOf(uid.toString());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
String type = (String) claims.get("type");
|
||||
String userType = "customer".equals(type) ? "CUSTOMER" : "SYSTEM";
|
||||
String identifier = claims.getSubject();
|
||||
|
||||
return new UserContext(userId, userType, identifier);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class UserContext {
|
||||
final Long userId;
|
||||
final String userType;
|
||||
final String identifier;
|
||||
|
||||
UserContext(Long userId, String userType, String identifier) {
|
||||
this.userId = userId;
|
||||
this.userType = userType;
|
||||
this.identifier = identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.rit.canteen.sales.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "developer_api_keys")
|
||||
public class DeveloperApiKey {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
private String apiKey;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String userType; // "SYSTEM" or "CUSTOMER"
|
||||
|
||||
@Column(nullable = false)
|
||||
private String ownerIdentifier; // Email or Mobile Number
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean active = true;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||
private boolean writeAccess = false;
|
||||
|
||||
@Column(nullable = true, length = 1000)
|
||||
private String permissions; // comma-separated scopes (e.g. READ_PRODUCTS,WRITE_PRODUCTS)
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime lastUsedAt;
|
||||
|
||||
public DeveloperApiKey() {}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserType() {
|
||||
return userType;
|
||||
}
|
||||
|
||||
public void setUserType(String userType) {
|
||||
this.userType = userType;
|
||||
}
|
||||
|
||||
public String getOwnerIdentifier() {
|
||||
return ownerIdentifier;
|
||||
}
|
||||
|
||||
public void setOwnerIdentifier(String ownerIdentifier) {
|
||||
this.ownerIdentifier = ownerIdentifier;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastUsedAt() {
|
||||
return lastUsedAt;
|
||||
}
|
||||
|
||||
public void setLastUsedAt(LocalDateTime lastUsedAt) {
|
||||
this.lastUsedAt = lastUsedAt;
|
||||
}
|
||||
|
||||
public boolean isWriteAccess() {
|
||||
return writeAccess;
|
||||
}
|
||||
|
||||
public void setWriteAccess(boolean writeAccess) {
|
||||
this.writeAccess = writeAccess;
|
||||
}
|
||||
|
||||
public String getPermissions() {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public void setPermissions(String permissions) {
|
||||
this.permissions = permissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.rit.canteen.sales.repository;
|
||||
|
||||
import com.rit.canteen.sales.model.DeveloperApiKey;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface DeveloperApiKeyRepository extends JpaRepository<DeveloperApiKey, Long> {
|
||||
Optional<DeveloperApiKey> findByApiKey(String apiKey);
|
||||
List<DeveloperApiKey> findByUserIdAndUserType(Long userId, String userType);
|
||||
long countByUserIdAndUserType(Long userId, String userType);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.rit.canteen.sales.service;
|
||||
|
||||
import com.rit.canteen.sales.model.DeveloperApiKey;
|
||||
import com.rit.canteen.sales.repository.DeveloperApiKeyRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class DeveloperApiKeyService {
|
||||
|
||||
@Autowired
|
||||
private DeveloperApiKeyRepository repository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<DeveloperApiKey> getKeysForUser(Long userId, String userType) {
|
||||
return repository.findByUserIdAndUserType(userId, userType);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DeveloperApiKey createKey(Long userId, String userType, String ownerIdentifier, String name, boolean writeAccess, String permissions) {
|
||||
// Enforce the 3 key limit only for regular CUSTOMER users. SYSTEM users (admin/managers) get unlimited keys.
|
||||
if (!"SYSTEM".equalsIgnoreCase(userType)) {
|
||||
long count = repository.countByUserIdAndUserType(userId, userType);
|
||||
if (count >= 3) {
|
||||
throw new IllegalStateException("Maximum limit of 3 API keys reached");
|
||||
}
|
||||
}
|
||||
|
||||
DeveloperApiKey key = new DeveloperApiKey();
|
||||
key.setUserId(userId);
|
||||
key.setUserType(userType);
|
||||
key.setOwnerIdentifier(ownerIdentifier);
|
||||
key.setName(name);
|
||||
key.setWriteAccess(writeAccess);
|
||||
key.setPermissions(permissions);
|
||||
|
||||
// Generate a secure API key
|
||||
String prefix = writeAccess ? "DEV-W-" : "DEV-";
|
||||
String rawKey = prefix + UUID.randomUUID().toString().replace("-", "").toUpperCase();
|
||||
key.setApiKey(rawKey);
|
||||
|
||||
return repository.save(key);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteKey(Long keyId, Long userId, String userType) {
|
||||
Optional<DeveloperApiKey> keyOpt = repository.findById(keyId);
|
||||
if (keyOpt.isPresent()) {
|
||||
DeveloperApiKey key = keyOpt.get();
|
||||
if (key.getUserId().equals(userId) && key.getUserType().equals(userType)) {
|
||||
repository.delete(key);
|
||||
} else {
|
||||
throw new SecurityException("Unauthorized to delete this API key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<DeveloperApiKey> validateAndUseKey(String apiKey) {
|
||||
Optional<DeveloperApiKey> keyOpt = repository.findByApiKey(apiKey);
|
||||
if (keyOpt.isPresent() && keyOpt.get().isActive()) {
|
||||
DeveloperApiKey key = keyOpt.get();
|
||||
key.setLastUsedAt(LocalDateTime.now());
|
||||
return Optional.of(repository.save(key));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user