API Dashboard added

This commit is contained in:
Sidharth Prabhu
2026-07-24 10:27:30 +05:30
parent 2c42aeb4ee
commit 93e375dc7d
27 changed files with 6448 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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