Updated API Dashboard

This commit is contained in:
Sidharth Prabhu
2026-07-29 11:52:09 +05:30
parent ba06c1a7a6
commit 28a13d619c
19 changed files with 1386 additions and 136 deletions

View File

@@ -0,0 +1,61 @@
package com.rit.canteen.sales.config;
import com.rit.canteen.sales.model.DeveloperApiKey;
import com.rit.canteen.sales.model.DeveloperApiLog;
import com.rit.canteen.sales.repository.DeveloperApiKeyRepository;
import com.rit.canteen.sales.repository.DeveloperApiLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Optional;
@Component
public class DeveloperApiLogInterceptor implements HandlerInterceptor {
@Autowired
private DeveloperApiKeyRepository keyRepository;
@Autowired
private DeveloperApiLogRepository logRepository;
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String uri = request.getRequestURI();
if (uri.startsWith("/api/developer/v1/")) {
String apiKeyHeader = request.getHeader("X-Developer-Key");
String method = request.getMethod();
int status = response.getStatus();
String clientIp = request.getHeader("X-Forwarded-For");
if (clientIp == null || clientIp.isEmpty()) {
clientIp = request.getRemoteAddr();
}
DeveloperApiLog log = new DeveloperApiLog();
log.setEndpoint(uri);
log.setMethod(method);
log.setStatus(status);
log.setClientIp(clientIp);
String responseBody = (String) request.getAttribute("developer_api_response_body");
if (responseBody != null) {
log.setResponseBody(responseBody);
}
if (apiKeyHeader != null && !apiKeyHeader.trim().isEmpty()) {
log.setApiKey(apiKeyHeader);
Optional<DeveloperApiKey> keyOpt = keyRepository.findByApiKey(apiKeyHeader);
if (keyOpt.isPresent()) {
DeveloperApiKey key = keyOpt.get();
log.setAppId(key.getAppId());
log.setUserId(key.getUserId());
log.setUserType(key.getUserType());
}
}
logRepository.save(log);
}
}
}

View File

@@ -0,0 +1,43 @@
package com.rit.canteen.sales.config;
import com.rit.canteen.sales.controller.DeveloperApiController;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
@ControllerAdvice(assignableTypes = {DeveloperApiController.class})
public class DeveloperApiResponseAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
try {
if (request instanceof ServletServerHttpRequest servletRequest) {
HttpServletRequest httpReq = servletRequest.getServletRequest();
if (body != null) {
String json = objectMapper.writeValueAsString(body);
httpReq.setAttribute("developer_api_response_body", json);
}
}
} catch (Exception e) {
// ignore
}
return body;
}
}

View File

@@ -92,6 +92,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.PUT, "/api/orders/*").authenticated()
.requestMatchers(HttpMethod.POST, "/api/orders/*/cancel").authenticated()
.requestMatchers("/api/developer-keys/**").authenticated()
.requestMatchers("/api/developer-apps/**").authenticated()
.requestMatchers("/api/developer/v1/**").permitAll()
.requestMatchers("/api/counter/**").hasAnyRole("MASTER", "MANAGER", "STAFF")

View File

@@ -1,13 +1,18 @@
package com.rit.canteen.sales.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* WebConfig intentionally left minimal.
* CORS is now fully managed by SecurityConfig.corsConfigurationSource()
* to avoid duplicate/conflicting CORS headers.
*/
@Configuration
public class WebConfig {
// CORS handled by SecurityConfig — do not add CorsRegistry here
public class WebConfig implements WebMvcConfigurer {
@Autowired
private DeveloperApiLogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor).addPathPatterns("/api/developer/v1/**");
}
}

View File

@@ -9,6 +9,9 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import com.rit.canteen.sales.model.DeveloperApiLog;
import com.rit.canteen.sales.repository.DeveloperApiLogRepository;
import java.util.List;
import java.util.Map;
@@ -19,6 +22,30 @@ public class DeveloperApiKeyController {
@Autowired
private DeveloperApiKeyService keyService;
@Autowired
private DeveloperApiLogRepository logRepository;
@GetMapping("/logs")
public ResponseEntity<?> listLogs(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
UserContext context = getUserContext();
if (context == null) {
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
}
org.springframework.data.domain.Pageable pageable = org.springframework.data.domain.PageRequest.of(
page, size, org.springframework.data.domain.Sort.by("timestamp").descending());
org.springframework.data.domain.Page<DeveloperApiLog> logPage = logRepository.findByUserIdAndUserType(
context.userId, context.userType, pageable);
return ResponseEntity.ok(Map.of(
"content", logPage.getContent(),
"currentPage", logPage.getNumber(),
"totalItems", logPage.getTotalElements(),
"totalPages", logPage.getTotalPages(),
"pageSize", logPage.getSize()
));
}
@GetMapping
public ResponseEntity<?> listKeys() {
UserContext context = getUserContext();
@@ -56,8 +83,17 @@ public class DeveloperApiKeyController {
permissions = "READ_PRODUCTS,READ_STALLS,READ_ORDERS,READ_WALLETS";
}
Long appId = null;
if (body.containsKey("appId") && body.get("appId") != null) {
try {
appId = Long.valueOf(body.get("appId").toString());
} catch (NumberFormatException e) {
// ignore
}
}
try {
DeveloperApiKey newKey = keyService.createKey(context.userId, context.userType, context.identifier, name, writeAccess, permissions);
DeveloperApiKey newKey = keyService.createKey(context.userId, context.userType, context.identifier, name, writeAccess, permissions, appId);
return ResponseEntity.status(201).body(newKey);
} catch (IllegalStateException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));

View File

@@ -0,0 +1,129 @@
package com.rit.canteen.sales.controller;
import com.rit.canteen.sales.model.DeveloperApp;
import com.rit.canteen.sales.repository.DeveloperAppRepository;
import com.rit.canteen.sales.repository.DeveloperApiKeyRepository;
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 org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/developer-apps")
public class DeveloperAppController {
@Autowired
private DeveloperAppRepository appRepository;
@Autowired
private DeveloperApiKeyRepository keyRepository;
@GetMapping
public ResponseEntity<?> listApps() {
UserContext context = getUserContext();
if (context == null) {
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
}
List<DeveloperApp> apps = appRepository.findByUserIdAndUserType(context.userId, context.userType);
return ResponseEntity.ok(apps);
}
@PostMapping
public ResponseEntity<?> createApp(@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()
: "";
String description = body.containsKey("description") && body.get("description") != null
? body.get("description").toString()
: "";
if (name.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "App name is required"));
}
DeveloperApp app = new DeveloperApp();
app.setName(name);
app.setDescription(description);
app.setUserId(context.userId);
app.setUserType(context.userType);
app.setOwnerIdentifier(context.identifier);
DeveloperApp saved = appRepository.save(app);
return ResponseEntity.status(201).body(saved);
}
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<?> deleteApp(@PathVariable Long id) {
UserContext context = getUserContext();
if (context == null) {
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
}
Optional<DeveloperApp> appOpt = appRepository.findById(id);
if (appOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
DeveloperApp app = appOpt.get();
if (!app.getUserId().equals(context.userId) || !app.getUserType().equals(context.userType)) {
return ResponseEntity.status(403).body(Map.of("error", "Unauthorized to delete this App"));
}
// Delete keys associated with this App first
keyRepository.deleteByAppId(id);
appRepository.delete(app);
return ResponseEntity.ok(Map.of("success", true, "message", "App and its keys deleted successfully"));
}
// ── 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

@@ -40,6 +40,9 @@ public class DeveloperApiKey {
private LocalDateTime lastUsedAt;
@Column(nullable = true)
private Long appId;
public DeveloperApiKey() {}
@PrePersist
@@ -134,4 +137,12 @@ public class DeveloperApiKey {
public void setPermissions(String permissions) {
this.permissions = permissions;
}
public Long getAppId() {
return appId;
}
public void setAppId(Long appId) {
this.appId = appId;
}
}

View File

@@ -0,0 +1,138 @@
package com.rit.canteen.sales.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "developer_api_logs")
public class DeveloperApiLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String endpoint;
@Column(nullable = false)
private String method;
@Column(nullable = true)
private String apiKey;
@Column(nullable = true)
private Long appId;
@Column(nullable = true)
private Long userId;
@Column(nullable = true)
private String userType;
@Column(nullable = false)
private int status;
@Column(nullable = true)
private String clientIp;
@Column(nullable = false)
private LocalDateTime timestamp;
@Column(columnDefinition = "TEXT")
private String responseBody;
public DeveloperApiLog() {}
@PrePersist
protected void onCreate() {
timestamp = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public Long getAppId() {
return appId;
}
public void setAppId(Long appId) {
this.appId = appId;
}
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 int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getClientIp() {
return clientIp;
}
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public String getResponseBody() {
return responseBody;
}
public void setResponseBody(String responseBody) {
this.responseBody = responseBody;
}
}

View File

@@ -0,0 +1,94 @@
package com.rit.canteen.sales.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "developer_apps")
public class DeveloperApp {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false)
private Long userId;
@Column(nullable = false)
private String userType; // "CUSTOMER"
@Column(nullable = false)
private String ownerIdentifier; // Email or Mobile Number
@Column(nullable = false)
private LocalDateTime createdAt;
public DeveloperApp() {}
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
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 LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -9,4 +9,6 @@ public interface DeveloperApiKeyRepository extends JpaRepository<DeveloperApiKey
Optional<DeveloperApiKey> findByApiKey(String apiKey);
List<DeveloperApiKey> findByUserIdAndUserType(Long userId, String userType);
long countByUserIdAndUserType(Long userId, String userType);
List<DeveloperApiKey> findByAppId(Long appId);
void deleteByAppId(Long appId);
}

View File

@@ -0,0 +1,10 @@
package com.rit.canteen.sales.repository;
import com.rit.canteen.sales.model.DeveloperApiLog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface DeveloperApiLogRepository extends JpaRepository<DeveloperApiLog, Long> {
Page<DeveloperApiLog> findByUserIdAndUserType(Long userId, String userType, Pageable pageable);
}

View File

@@ -0,0 +1,9 @@
package com.rit.canteen.sales.repository;
import com.rit.canteen.sales.model.DeveloperApp;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface DeveloperAppRepository extends JpaRepository<DeveloperApp, Long> {
List<DeveloperApp> findByUserIdAndUserType(Long userId, String userType);
}

View File

@@ -24,12 +24,20 @@ public class DeveloperApiKeyService {
@Transactional
public DeveloperApiKey createKey(Long userId, String userType, String ownerIdentifier, String name, boolean writeAccess, String permissions) {
return createKey(userId, userType, ownerIdentifier, name, writeAccess, permissions, null);
}
@Transactional
public DeveloperApiKey createKey(Long userId, String userType, String ownerIdentifier, String name, boolean writeAccess, String permissions, Long appId) {
// 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");
}
if (appId == null) {
throw new IllegalStateException("App ID is required for Customer keys");
}
}
DeveloperApiKey key = new DeveloperApiKey();
@@ -39,6 +47,7 @@ public class DeveloperApiKeyService {
key.setName(name);
key.setWriteAccess(writeAccess);
key.setPermissions(permissions);
key.setAppId(appId);
// Generate a secure API key
String prefix = writeAccess ? "DEV-W-" : "DEV-";