Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ coverage/
nlp-orchestrator/.env

*.json
!docs/**/*.json
!**/package.json
input_videos/
output_frames/

Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,15 +381,22 @@ For environment variables, copy `.env.example` to `.env` and fill in your values
| [Setup Guide](./docs/setup.md) | Full database setup, environment variables, and Docker configuration |
| [Architecture Overview](./docs/architecture/overview.md) | System design, component diagrams, and data flow |

### API Reference
- **Interactive Swagger UI**: `http://localhost:8080/swagger-ui.html` (auto-available in development mode)
- **OpenAPI 3.0 JSON Specification**: [docs/api/openapi.json](./docs/api/openapi.json)
- **Role-Based Postman Collection**: [docs/postman/NyaySetu_RoleBased_Flows.postman_collection.json](./docs/postman/NyaySetu_RoleBased_Flows.postman_collection.json)

### API Documentation & Integration
| Document | Description |
|---|---|
| [OpenAPI/Swagger Spec](./openapi.yaml) | Complete API specification in OpenAPI 3.0 format |
| [Swagger UI Endpoint](http://localhost:8080/swagger-ui.html) | Interactive Swagger UI API console (Dev mode) |
| [OpenAPI JSON Spec](./docs/api/openapi.json) | Complete OpenAPI 3.0 JSON specification for all backend services |
| [Role-Based Postman Collection](./docs/postman/NyaySetu_RoleBased_Flows.postman_collection.json) | Postman collection with litigant, lawyer, judge, police, admin flows |
| [OpenAPI/Swagger Spec](./openapi.yaml) | Complete API specification in OpenAPI 3.0 YAML format |
| [API Testing Guide](./API_TESTING_GUIDE.md) | Comprehensive guide for testing APIs with Postman, cURL, Python, JavaScript |
| [API Endpoints Reference](./API_ENDPOINTS_COMPREHENSIVE.md) | Detailed documentation of all 100+ endpoints with request/response schemas |
| [API Quick Reference](./API_QUICK_REFERENCE.md) | Quick lookup table for endpoints by user role and service |
| [API Integration Checklist](./API_INTEGRATION_CHECKLIST.md) | Step-by-step checklist for integrating APIs into applications |
| [Postman Collection](./Nyay_Setu_API_Collection.postman_collection.json) | Ready-to-import Postman collection with all endpoints and examples |

### Additional Resources
| Document | Description |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class OpenApiConfig {
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("nyaysetu-backend")
.packagesToScan("com.nyaysetu.backend.controller")
.packagesToScan("com.nyaysetu.backend")
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
import java.util.Map;
import jakarta.validation.Valid;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.util.regex.Pattern;

@Tag(name = "Authentication", description = "Register, login, password reset and face login")
Expand All @@ -44,6 +47,11 @@ public class AuthController {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;

@Operation(summary = "Register user", description = "Register a new litigant user account")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Registration successful"),
@ApiResponse(responseCode = "400", description = "Validation failed or user already exists")
})
@SecurityRequirements
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest req) {
Expand Down Expand Up @@ -84,11 +92,18 @@ public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest req) {
}
}

@Operation(summary = "Ping auth service", description = "Health check endpoint for authentication service")
@GetMapping("/ping")
public ResponseEntity<String> ping() {
return ResponseEntity.ok("pong");
}

@Operation(summary = "Login user", description = "Authenticate user with email and password to obtain JWT access and refresh tokens")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Login successful"),
@ApiResponse(responseCode = "401", description = "Invalid credentials"),
@ApiResponse(responseCode = "400", description = "Bad request or Google Sign-In user")
})
@SecurityRequirements
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest req) {
Expand Down Expand Up @@ -131,6 +146,11 @@ public ResponseEntity<?> login(@Valid @RequestBody LoginRequest req) {
}
}

@Operation(summary = "Refresh JWT access token", description = "Generate a new access token using a valid refresh token")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Token refreshed successfully"),
@ApiResponse(responseCode = "401", description = "Invalid or expired refresh token")
})
@SecurityRequirements
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@Valid @RequestBody RefreshTokenRequest req) {
Expand Down Expand Up @@ -166,6 +186,10 @@ public ResponseEntity<?> refreshToken(@Valid @RequestBody RefreshTokenRequest re
private static final String PASSWORD_RESET_GENERIC_MESSAGE =
"If an account with that email exists, a password reset link has been sent.";

@Operation(summary = "Forgot password", description = "Send a password reset link to user email")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Password reset email sent if account exists")
})
@SecurityRequirements
@PostMapping("/forgot-password")
public ResponseEntity<?> forgotPassword(@Valid @RequestBody ForgotPasswordRequest req) {
Expand All @@ -182,6 +206,11 @@ public ResponseEntity<?> forgotPassword(@Valid @RequestBody ForgotPasswordReques
));
}

@Operation(summary = "Verify password reset token", description = "Check if password reset token is valid and not expired")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Token valid"),
@ApiResponse(responseCode = "400", description = "Token invalid or expired")
})
@GetMapping("/verify-reset-token")
public ResponseEntity<?> verifyResetToken(@RequestParam String token) {
try {
Expand All @@ -203,6 +232,11 @@ public ResponseEntity<?> verifyResetToken(@RequestParam String token) {
}
}

@Operation(summary = "Reset password", description = "Set a new password using a valid reset token")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Password reset successful"),
@ApiResponse(responseCode = "400", description = "Token invalid or password policy mismatch")
})
@PostMapping("/reset-password")
public ResponseEntity<?> resetPassword(@Valid @RequestBody ResetPasswordRequest req) {
try {
Expand Down Expand Up @@ -242,6 +276,11 @@ public ResponseEntity<?> resetPassword(@Valid @RequestBody ResetPasswordRequest

// ==================== FACE LOGIN ENDPOINTS ====================

@Operation(summary = "Enroll face descriptor", description = "Register face biometric vector for authenticated user")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Face enrolled successfully"),
@ApiResponse(responseCode = "400", description = "Invalid face descriptor")
})
@PostMapping("/face/enroll")
public ResponseEntity<?> enrollFace(@Valid @RequestBody FaceEnrollRequest req, Authentication auth) {
try {
Expand All @@ -254,6 +293,11 @@ public ResponseEntity<?> enrollFace(@Valid @RequestBody FaceEnrollRequest req, A
}
}

@Operation(summary = "Login with face recognition", description = "Authenticate using face biometric descriptor")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Face verification successful"),
@ApiResponse(responseCode = "401", description = "Face verification failed")
})
@PostMapping("/face/login")
public ResponseEntity<?> loginWithFace(@Valid @RequestBody FaceLoginRequest req) {
try {
Expand All @@ -279,6 +323,11 @@ public ResponseEntity<?> loginWithFace(@Valid @RequestBody FaceLoginRequest req)
}
}

@Operation(summary = "Disable face login", description = "Remove enrolled face recognition biometrics")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Face login disabled"),
@ApiResponse(responseCode = "400", description = "Error disabling face login")
})
@DeleteMapping("/face/disable")
public ResponseEntity<?> disableFaceLogin(Authentication auth) {
try {
Expand All @@ -290,6 +339,10 @@ public ResponseEntity<?> disableFaceLogin(Authentication auth) {
}
}

@Operation(summary = "Get face login enrollment status", description = "Check whether authenticated user has enrolled face biometrics")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Status retrieved successfully")
})
@GetMapping("/face/status")
public ResponseEntity<?> getFaceLoginStatus(Authentication auth) {
try {
Expand All @@ -301,6 +354,27 @@ public ResponseEntity<?> getFaceLoginStatus(Authentication auth) {
}
}

@Operation(summary = "Update language preference", description = "Persist user preferred UI/AI language (en, hi, mr, ta, te)")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Language preference saved successfully"),
@ApiResponse(responseCode = "400", description = "Invalid language code")
})
@PutMapping("/language-preference")
public ResponseEntity<?> updateLanguagePreference(Authentication auth, @RequestBody Map<String, String> body) {
try {
String lang = body.getOrDefault("language", "en");
if (!java.util.List.of("en", "hi", "mr", "ta", "te", "gu", "kn", "bn", "ml", "pa").contains(lang)) {
return ResponseEntity.badRequest().body(Map.of("message", "Unsupported language code"));
}
User user = authService.findByEmail(auth.getName());
user.setPreferredLanguage(lang);
userRepository.save(user);
return ResponseEntity.ok(Map.of("message", "Language preference saved", "preferredLanguage", lang));
} catch (Exception e) {
return ResponseEntity.status(400).body(Map.of("message", e.getMessage()));
}
}

@GetMapping("/test")
public ResponseEntity<String> test() {
return ResponseEntity.ok("ok");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,50 @@
import com.nyaysetu.backend.entity.CaseEntity;
import com.nyaysetu.backend.entity.CaseStatus;
import com.nyaysetu.backend.service.CaseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@Tag(name = "Case Management", description = "Endpoints for creating, fetching, updating cases and managing appeals")
@RestController
@RequestMapping("/api/cases")
@RequiredArgsConstructor
public class CaseController {

private final CaseService caseService;

@Operation(summary = "Create a new legal case", description = "Allows LITIGANT, LAWYER, or ADMIN to file a new case")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "Case created successfully"),
@ApiResponse(responseCode = "403", description = "Access denied")
})
@PreAuthorize("hasAnyRole('LAWYER', 'LITIGANT', 'ADMIN')")
@PostMapping
public ResponseEntity<CaseEntity> createCase(@RequestBody CreateCaseRequest dto) {
return new ResponseEntity<>(caseService.createCase(dto), HttpStatus.CREATED);
}

@Operation(summary = "Get case details by ID", description = "Retrieve a specific case by its unique UUID")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Case retrieved successfully"),
@ApiResponse(responseCode = "404", description = "Case not found")
})
@GetMapping("/{id}")
public ResponseEntity<CaseEntity> getCase(@PathVariable UUID id) {
return ResponseEntity.ok(caseService.getCase(id));
}

// 🛠️ PAGINATED ENDPOINT FOR ISSUE #828
@Operation(summary = "Get all cases (paginated)", description = "Fetch a paginated list of cases")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Page of cases retrieved successfully")
})
@GetMapping
public ResponseEntity<Page<CaseEntity>> getAllCases(
@RequestParam(defaultValue = "0") int page,
Expand All @@ -43,6 +59,11 @@ public ResponseEntity<Page<CaseEntity>> getAllCases(
return ResponseEntity.ok(casesPage);
}

@Operation(summary = "Update case status", description = "Allows JUDGE, SUPER_JUDGE, or ADMIN to update the status of a case")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Status updated successfully"),
@ApiResponse(responseCode = "403", description = "Forbidden for current user role")
})
@PreAuthorize("hasAnyRole('JUDGE', 'SUPER_JUDGE', 'ADMIN')")
@PutMapping("/{id}/status")
public ResponseEntity<CaseEntity> updateStatus(
Expand All @@ -52,6 +73,10 @@ public ResponseEntity<CaseEntity> updateStatus(
return ResponseEntity.ok(caseService.updateStatus(id, status));
}

@Operation(summary = "File an appeal for a case", description = "Allows LITIGANT or ADMIN to submit an appeal for a case")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "Appeal created successfully")
})
@PreAuthorize("hasAnyRole('LITIGANT', 'ADMIN')")
@PostMapping("/{caseId}/appeal")
public ResponseEntity<CaseEntity> createAppeal(
Expand All @@ -62,13 +87,21 @@ public ResponseEntity<CaseEntity> createAppeal(
.body(caseService.createAppeal(caseId, reason));
}

@Operation(summary = "Get appeals for a case", description = "Retrieve list of appeals associated with a case")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Appeals list retrieved successfully")
})
@GetMapping("/{caseId}/appeals")
public ResponseEntity<List<CaseEntity>> getAppeals(
@PathVariable UUID caseId
) {
return ResponseEntity.ok(caseService.getAppeals(caseId));
}

@Operation(summary = "Update appeal status", description = "Allows JUDGE, SUPER_JUDGE, or ADMIN to approve/reject an appeal")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Appeal status updated successfully")
})
@PreAuthorize("hasAnyRole('JUDGE', 'SUPER_JUDGE', 'ADMIN')")
@PutMapping("/appeals/{appealId}/status")
public ResponseEntity<CaseEntity> updateAppealStatus(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,54 @@ public ResponseEntity<Map<String, Object>> updateRespondentDetails(
"message", "Respondent details updated successfully"
));
}

@org.springframework.beans.factory.annotation.Autowired(required = false)
private com.nyaysetu.backend.service.DocumentManagementService documentManagementService;

@Operation(summary = "Upload supporting document for case", description = "Upload supporting documents (PDF, JPG, PNG up to 10MB) attached to a specific case")
@PostMapping("/{id}/documents")
public ResponseEntity<?> uploadCaseDocument(
@PathVariable UUID id,
@RequestParam("file") org.springframework.web.multipart.MultipartFile file,
@RequestParam(value = "category", defaultValue = "CASE_DOCUMENT") String category,
@RequestParam(value = "description", required = false, defaultValue = "") String description,
Authentication authentication,
jakarta.servlet.http.HttpServletRequest request
) {
try {
User user = authService.findByEmail(authentication.getName());
caseAccessService.requireCaseAccess(id, user);

if (file.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Uploaded file cannot be empty"));
}

if (file.getSize() > 10 * 1024 * 1024) {
return ResponseEntity.badRequest().body(Map.of("error", "File size exceeds maximum limit of 10MB"));
}

String contentType = file.getContentType();
String fileName = file.getOriginalFilename() != null ? file.getOriginalFilename().toLowerCase() : "";
if (contentType != null && !contentType.equals("application/pdf") &&
!contentType.startsWith("image/") && !fileName.endsWith(".pdf") &&
!fileName.endsWith(".jpg") && !fileName.endsWith(".jpeg") && !fileName.endsWith(".png")) {
return ResponseEntity.badRequest().body(Map.of("error", "Only PDF, JPG, and PNG file formats are supported"));
}

String uploadIp = request.getHeader("X-Forwarded-For");
if (uploadIp == null || uploadIp.isEmpty()) uploadIp = request.getRemoteAddr();

com.nyaysetu.backend.dto.UploadDocumentRequest uploadRequest = com.nyaysetu.backend.dto.UploadDocumentRequest.builder()
.category(category)
.description(description)
.caseId(id)
.build();

com.nyaysetu.backend.dto.DocumentDto document = documentManagementService.uploadDocument(file, uploadRequest, user, uploadIp);
return ResponseEntity.ok(document);
} catch (Exception e) {
log.error("Failed to upload document for case {}", id, e);
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
}
}
}
Loading
Loading