Skip to content

Commit ae2c67c

Browse files
committed
add
2 parents 0047805 + 6e83919 commit ae2c67c

10 files changed

Lines changed: 632 additions & 12 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ build/
3535
### Environment Variables ###
3636
.env
3737
.env.local
38+
39+
backups

src/main/java/com/crimeLink/analyzer/config/SecurityConfig.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class SecurityConfig {
4040
@Bean
4141
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
4242
http
43+
<<<<<<< HEAD
4344
.csrf(csrf -> csrf.disable())
4445
// CRITICAL FIX: Enable CORS using the bean configuration
4546
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
@@ -95,6 +96,77 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
9596
)
9697
.authenticationProvider(authenticationProvider())
9798
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
99+
=======
100+
.csrf(csrf -> csrf.disable())
101+
// CRITICAL FIX: Enable CORS using the bean configuration
102+
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
103+
.authorizeHttpRequests(auth -> auth
104+
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
105+
.requestMatchers("/api/auth/**").permitAll()
106+
.requestMatchers("/api/health").permitAll()
107+
.requestMatchers("/api/admin/health").permitAll()
108+
.requestMatchers("/api/facial/health").permitAll() // ML service health check
109+
.requestMatchers("/api/call-analysis/health").permitAll() // ML service health check
110+
111+
// ML Service endpoints
112+
.requestMatchers("/api/call-analysis/**").hasRole("Investigator")
113+
.requestMatchers("/api/facial/register").hasAnyRole("Investigator", "OIC")
114+
.requestMatchers("/api/facial/criminals").hasAnyRole("Investigator", "OIC")
115+
.requestMatchers("/api/facial/**").hasRole("Investigator")
116+
117+
// Criminal CRUD (direct DB, no Python)
118+
.requestMatchers("/api/criminals/**").hasAnyRole("Investigator", "OIC")
119+
.requestMatchers("/api/criminals").hasAnyRole("Investigator", "OIC")
120+
121+
.requestMatchers("/api/database/**").permitAll()
122+
.requestMatchers("/api/test").permitAll()
123+
.requestMatchers("/api/debug/**").permitAll() // 🔍 Debug endpoints
124+
.requestMatchers("/error").permitAll() // Allow error page without auth
125+
126+
// Public endpoints
127+
.requestMatchers("/api/vehicle**").permitAll()
128+
.requestMatchers("/api/mobile/auth/**").permitAll()
129+
.requestMatchers("/api/duties/**").permitAll()
130+
.requestMatchers("/api/crime-reports/map").permitAll()
131+
.requestMatchers(HttpMethod.GET, "/api/crime-reports").permitAll()
132+
.requestMatchers("/api/crime-reports/upload-evidence").authenticated()
133+
.requestMatchers("/api/crime-reports/**").hasAnyRole("OIC", "Admin")
134+
135+
// Field Officer routes
136+
.requestMatchers("/api/officers/me/**").hasRole("FieldOfficer")
137+
.requestMatchers("/api/mobile/**").hasRole("FieldOfficer")
138+
.requestMatchers("/api/leaves/**").permitAll()
139+
140+
// OIC-only routes
141+
.requestMatchers("/api/duty-schedules/**").hasRole("OIC")
142+
.requestMatchers("/api/weapon/**").hasRole("OIC")
143+
.requestMatchers("/api/weapon-issue/**").hasRole("OIC")
144+
145+
// Admin/OIC/Investigator routes (officer data, locations, users)
146+
.requestMatchers("/api/users/field-officers").hasAnyRole("Admin", "OIC", "Investigator")
147+
.requestMatchers("/api/admin/officers/*/locations/**").hasAnyRole("Admin", "OIC", "Investigator")
148+
149+
// Admin-only: backup, restore, settings (must come before general /api/admin/**)
150+
.requestMatchers("/api/admin/backup").hasRole("Admin")
151+
.requestMatchers("/api/admin/restore").hasRole("Admin")
152+
.requestMatchers("/api/admin/backups").hasRole("Admin")
153+
.requestMatchers("/api/admin/settings").hasRole("Admin")
154+
155+
.requestMatchers("/api/admin/**").hasAnyRole("OIC", "Admin")
156+
157+
.anyRequest().authenticated())
158+
.exceptionHandling(exception -> exception
159+
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
160+
.accessDeniedHandler((request, response, accessDeniedException) -> {
161+
response.setStatus(HttpStatus.FORBIDDEN.value());
162+
response.setContentType("application/json");
163+
response.getWriter().write("{\"message\":\"Access denied\"}");
164+
}))
165+
.sessionManagement(session -> session
166+
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
167+
.authenticationProvider(authenticationProvider())
168+
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
169+
>>>>>>> 6e83919d1833fd060bc9d9871099b940c90958eb
98170

99171
return http.build();
100172
}

src/main/java/com/crimeLink/analyzer/controller/AdminController.java

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package com.crimeLink.analyzer.controller;
22

33
import com.crimeLink.analyzer.dto.AuditLogDTO;
4+
import com.crimeLink.analyzer.entity.BackupMetadata;
45
import com.crimeLink.analyzer.entity.LoginAudit;
56
import com.crimeLink.analyzer.entity.User;
67
import com.crimeLink.analyzer.repository.LoginAuditRepository;
78
import com.crimeLink.analyzer.repository.UserRepository;
9+
import com.crimeLink.analyzer.service.BackupService;
10+
import com.crimeLink.analyzer.service.SystemSettingsService;
811
import lombok.RequiredArgsConstructor;
912
import org.springframework.data.domain.PageRequest;
1013
import org.springframework.data.domain.Sort;
1114
import org.springframework.http.ResponseEntity;
15+
import org.springframework.security.access.prepost.PreAuthorize;
16+
import org.springframework.security.core.Authentication;
1217
import org.springframework.security.crypto.password.PasswordEncoder;
1318
import org.springframework.web.bind.annotation.*;
1419

@@ -24,6 +29,12 @@ public class AdminController {
2429
private final UserRepository userRepo;
2530
private final LoginAuditRepository auditRepo;
2631
private final PasswordEncoder passwordEncoder;
32+
private final BackupService backupService;
33+
private final SystemSettingsService settingsService;
34+
35+
// ════════════════════════════════════════════════════════════════
36+
// USER MANAGEMENT (Admin + OIC)
37+
// ════════════════════════════════════════════════════════════════
2738

2839
/**
2940
* Get all users or filter by role/status
@@ -124,6 +135,10 @@ public ResponseEntity<?> deactivateUser(@PathVariable Integer id) {
124135
.orElse(ResponseEntity.notFound().build());
125136
}
126137

138+
// ════════════════════════════════════════════════════════════════
139+
// AUDIT LOGS (Admin + OIC)
140+
// ════════════════════════════════════════════════════════════════
141+
127142
/**
128143
* Get audit logs
129144
* GET /api/admin/audit-logs?limit=100&offset=0
@@ -180,23 +195,26 @@ public ResponseEntity<List<AuditLogDTO>> getAuditLogs(
180195
return ResponseEntity.ok(dtoList);
181196
}
182197

198+
// ════════════════════════════════════════════════════════════════
199+
// BACKUP & RESTORE (Admin only)
200+
// ════════════════════════════════════════════════════════════════
201+
183202
/**
184203
* Trigger database backup
185204
* POST /api/admin/backup
186205
*/
187206
@PostMapping("/backup")
188-
public ResponseEntity<?> triggerBackup() {
207+
@PreAuthorize("hasRole('Admin')")
208+
public ResponseEntity<?> triggerBackup(Authentication authentication) {
189209
try {
190-
String timestamp = java.time.LocalDateTime.now()
191-
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
192-
String filename = "backup_" + timestamp + ".sql";
193-
194-
// TODO: Implement actual backup logic
195-
// For Railway PostgreSQL, use pg_dump or Spring's backup mechanisms
210+
String userEmail = authentication.getName();
211+
BackupMetadata metadata = backupService.createBackup(userEmail);
196212

197213
return ResponseEntity.ok(Map.of(
198214
"message", "Backup created successfully",
199-
"file", filename
215+
"file", metadata.getFilename(),
216+
"sizeBytes", metadata.getSizeBytes(),
217+
"createdAt", metadata.getCreatedAt().toString()
200218
));
201219
} catch (Exception e) {
202220
return ResponseEntity.status(500).body(
@@ -210,27 +228,86 @@ public ResponseEntity<?> triggerBackup() {
210228
* POST /api/admin/restore
211229
*/
212230
@PostMapping("/restore")
213-
public ResponseEntity<?> restoreBackup(@RequestBody Map<String, String> request) {
231+
@PreAuthorize("hasRole('Admin')")
232+
public ResponseEntity<?> restoreBackup(
233+
@RequestBody Map<String, String> request,
234+
Authentication authentication) {
214235
try {
215236
String filename = request.get("filename");
216-
if (filename == null || filename.isEmpty()) {
237+
if (filename == null || filename.isBlank()) {
217238
return ResponseEntity.badRequest()
218239
.body(Map.of("message", "Filename is required"));
219240
}
220241

221-
// TODO: Implement actual restore logic
222-
// For Railway PostgreSQL, use psql or Spring's restore mechanisms
242+
String userEmail = authentication.getName();
243+
backupService.restoreBackup(filename, userEmail);
223244

224245
return ResponseEntity.ok(Map.of(
225246
"message", "Database restored successfully from " + filename
226247
));
248+
} catch (IllegalArgumentException e) {
249+
return ResponseEntity.badRequest().body(
250+
Map.of("message", e.getMessage())
251+
);
227252
} catch (Exception e) {
228253
return ResponseEntity.status(500).body(
229254
Map.of("message", "Restore failed: " + e.getMessage())
230255
);
231256
}
232257
}
233258

259+
/**
260+
* List all available backups
261+
* GET /api/admin/backups
262+
*/
263+
@GetMapping("/backups")
264+
@PreAuthorize("hasRole('Admin')")
265+
public ResponseEntity<List<BackupMetadata>> listBackups() {
266+
return ResponseEntity.ok(backupService.listBackups());
267+
}
268+
269+
// ════════════════════════════════════════════════════════════════
270+
// SYSTEM SETTINGS (Admin only)
271+
// ════════════════════════════════════════════════════════════════
272+
273+
/**
274+
* Get all system settings
275+
* GET /api/admin/settings
276+
*/
277+
@GetMapping("/settings")
278+
@PreAuthorize("hasRole('Admin')")
279+
public ResponseEntity<Map<String, String>> getSettings() {
280+
return ResponseEntity.ok(settingsService.getAllSettings());
281+
}
282+
283+
/**
284+
* Update system settings
285+
* PUT /api/admin/settings
286+
*/
287+
@PutMapping("/settings")
288+
@PreAuthorize("hasRole('Admin')")
289+
public ResponseEntity<?> updateSettings(@RequestBody Map<String, String> settings) {
290+
try {
291+
Map<String, String> updated = settingsService.updateSettings(settings);
292+
return ResponseEntity.ok(Map.of(
293+
"message", "Settings saved successfully",
294+
"settings", updated
295+
));
296+
} catch (IllegalArgumentException e) {
297+
return ResponseEntity.badRequest().body(
298+
Map.of("message", e.getMessage())
299+
);
300+
} catch (Exception e) {
301+
return ResponseEntity.status(500).body(
302+
Map.of("message", "Failed to save settings: " + e.getMessage())
303+
);
304+
}
305+
}
306+
307+
// ════════════════════════════════════════════════════════════════
308+
// SYSTEM HEALTH (public)
309+
// ════════════════════════════════════════════════════════════════
310+
234311
/**
235312
* Get system health
236313
* GET /api/admin/health
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.crimeLink.analyzer.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Entity
11+
@Table(name = "backup_metadata")
12+
@Data
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
public class BackupMetadata {
16+
17+
@Id
18+
@GeneratedValue(strategy = GenerationType.IDENTITY)
19+
private Long id;
20+
21+
@Column(name = "filename", nullable = false, unique = true, length = 255)
22+
private String filename;
23+
24+
@Column(name = "size_bytes")
25+
private Long sizeBytes;
26+
27+
@Column(name = "created_at", nullable = false)
28+
private LocalDateTime createdAt;
29+
30+
@Column(name = "created_by", length = 100)
31+
private String createdBy;
32+
33+
@Column(name = "status", length = 50)
34+
private String status; // SUCCESS, FAILED
35+
36+
@PrePersist
37+
public void prePersist() {
38+
if (createdAt == null) {
39+
createdAt = LocalDateTime.now();
40+
}
41+
}
42+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.crimeLink.analyzer.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Entity
11+
@Table(name = "system_settings")
12+
@Data
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
public class SystemSetting {
16+
17+
@Id
18+
@GeneratedValue(strategy = GenerationType.IDENTITY)
19+
private Long id;
20+
21+
@Column(name = "setting_key", nullable = false, unique = true, length = 100)
22+
private String settingKey;
23+
24+
@Column(name = "setting_value", nullable = false, length = 500)
25+
private String settingValue;
26+
27+
@Column(name = "updated_at")
28+
private LocalDateTime updatedAt;
29+
30+
@PrePersist
31+
@PreUpdate
32+
public void onUpdate() {
33+
updatedAt = LocalDateTime.now();
34+
}
35+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.crimeLink.analyzer.repository;
2+
3+
import com.crimeLink.analyzer.entity.BackupMetadata;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.List;
8+
import java.util.Optional;
9+
10+
@Repository
11+
public interface BackupMetadataRepository extends JpaRepository<BackupMetadata, Long> {
12+
13+
List<BackupMetadata> findAllByOrderByCreatedAtDesc();
14+
15+
Optional<BackupMetadata> findByFilename(String filename);
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.crimeLink.analyzer.repository;
2+
3+
import com.crimeLink.analyzer.entity.SystemSetting;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.Optional;
8+
9+
@Repository
10+
public interface SystemSettingRepository extends JpaRepository<SystemSetting, Long> {
11+
12+
Optional<SystemSetting> findBySettingKey(String settingKey);
13+
}

0 commit comments

Comments
 (0)