Skip to content
Merged
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 @@ -35,3 +35,5 @@ build/
### Environment Variables ###
.env
.env.local

backups
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
// Admin/OIC/Investigator routes (officer data, locations, users)
.requestMatchers("/api/users/field-officers").hasAnyRole("Admin", "OIC", "Investigator")
.requestMatchers("/api/admin/officers/*/locations/**").hasAnyRole("Admin", "OIC", "Investigator")

// Admin-only: backup, restore, settings (must come before general /api/admin/**)
.requestMatchers("/api/admin/backup").hasRole("Admin")
.requestMatchers("/api/admin/restore").hasRole("Admin")
.requestMatchers("/api/admin/backups").hasRole("Admin")
.requestMatchers("/api/admin/settings").hasRole("Admin")

.requestMatchers("/api/admin/**").hasAnyRole("OIC", "Admin")

.anyRequest().authenticated())
Expand Down
101 changes: 89 additions & 12 deletions src/main/java/com/crimeLink/analyzer/controller/AdminController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.crimeLink.analyzer.controller;

import com.crimeLink.analyzer.dto.AuditLogDTO;
import com.crimeLink.analyzer.entity.BackupMetadata;
import com.crimeLink.analyzer.entity.LoginAudit;
import com.crimeLink.analyzer.entity.User;
import com.crimeLink.analyzer.repository.LoginAuditRepository;
import com.crimeLink.analyzer.repository.UserRepository;
import com.crimeLink.analyzer.service.BackupService;
import com.crimeLink.analyzer.service.SystemSettingsService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

Expand All @@ -24,6 +29,12 @@ public class AdminController {
private final UserRepository userRepo;
private final LoginAuditRepository auditRepo;
private final PasswordEncoder passwordEncoder;
private final BackupService backupService;
private final SystemSettingsService settingsService;

// ════════════════════════════════════════════════════════════════
// USER MANAGEMENT (Admin + OIC)
// ════════════════════════════════════════════════════════════════

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

// ════════════════════════════════════════════════════════════════
// AUDIT LOGS (Admin + OIC)
// ════════════════════════════════════════════════════════════════

/**
* Get audit logs
* GET /api/admin/audit-logs?limit=100&offset=0
Expand Down Expand Up @@ -180,23 +195,26 @@ public ResponseEntity<List<AuditLogDTO>> getAuditLogs(
return ResponseEntity.ok(dtoList);
}

// ════════════════════════════════════════════════════════════════
// BACKUP & RESTORE (Admin only)
// ════════════════════════════════════════════════════════════════

/**
* Trigger database backup
* POST /api/admin/backup
*/
@PostMapping("/backup")
public ResponseEntity<?> triggerBackup() {
@PreAuthorize("hasRole('Admin')")
public ResponseEntity<?> triggerBackup(Authentication authentication) {
try {
String timestamp = java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
String filename = "backup_" + timestamp + ".sql";

// TODO: Implement actual backup logic
// For Railway PostgreSQL, use pg_dump or Spring's backup mechanisms
String userEmail = authentication.getName();
BackupMetadata metadata = backupService.createBackup(userEmail);

return ResponseEntity.ok(Map.of(
"message", "Backup created successfully",
"file", filename
"file", metadata.getFilename(),
"sizeBytes", metadata.getSizeBytes(),
"createdAt", metadata.getCreatedAt().toString()
));
} catch (Exception e) {
return ResponseEntity.status(500).body(
Expand All @@ -210,27 +228,86 @@ public ResponseEntity<?> triggerBackup() {
* POST /api/admin/restore
*/
@PostMapping("/restore")
public ResponseEntity<?> restoreBackup(@RequestBody Map<String, String> request) {
@PreAuthorize("hasRole('Admin')")
public ResponseEntity<?> restoreBackup(
@RequestBody Map<String, String> request,
Authentication authentication) {
try {
String filename = request.get("filename");
if (filename == null || filename.isEmpty()) {
if (filename == null || filename.isBlank()) {
return ResponseEntity.badRequest()
.body(Map.of("message", "Filename is required"));
}

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

return ResponseEntity.ok(Map.of(
"message", "Database restored successfully from " + filename
));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(
Map.of("message", e.getMessage())
);
} catch (Exception e) {
return ResponseEntity.status(500).body(
Map.of("message", "Restore failed: " + e.getMessage())
);
}
}

/**
* List all available backups
* GET /api/admin/backups
*/
@GetMapping("/backups")
@PreAuthorize("hasRole('Admin')")
public ResponseEntity<List<BackupMetadata>> listBackups() {
return ResponseEntity.ok(backupService.listBackups());
}

// ════════════════════════════════════════════════════════════════
// SYSTEM SETTINGS (Admin only)
// ════════════════════════════════════════════════════════════════

/**
* Get all system settings
* GET /api/admin/settings
*/
@GetMapping("/settings")
@PreAuthorize("hasRole('Admin')")
public ResponseEntity<Map<String, String>> getSettings() {
return ResponseEntity.ok(settingsService.getAllSettings());
}

/**
* Update system settings
* PUT /api/admin/settings
*/
@PutMapping("/settings")
@PreAuthorize("hasRole('Admin')")
public ResponseEntity<?> updateSettings(@RequestBody Map<String, String> settings) {
try {
Map<String, String> updated = settingsService.updateSettings(settings);
return ResponseEntity.ok(Map.of(
"message", "Settings saved successfully",
"settings", updated
));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(
Map.of("message", e.getMessage())
);
} catch (Exception e) {
return ResponseEntity.status(500).body(
Map.of("message", "Failed to save settings: " + e.getMessage())
);
}
}

// ════════════════════════════════════════════════════════════════
// SYSTEM HEALTH (public)
// ════════════════════════════════════════════════════════════════

/**
* Get system health
* GET /api/admin/health
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/com/crimeLink/analyzer/entity/BackupMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.crimeLink.analyzer.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(name = "backup_metadata")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BackupMetadata {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "filename", nullable = false, unique = true, length = 255)
private String filename;

@Column(name = "size_bytes")
private Long sizeBytes;

@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

@Column(name = "created_by", length = 100)
private String createdBy;

@Column(name = "status", length = 50)
private String status; // SUCCESS, FAILED

@PrePersist
public void prePersist() {
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/crimeLink/analyzer/entity/SystemSetting.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.crimeLink.analyzer.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(name = "system_settings")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SystemSetting {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "setting_key", nullable = false, unique = true, length = 100)
private String settingKey;

@Column(name = "setting_value", nullable = false, length = 500)
private String settingValue;

@Column(name = "updated_at")
private LocalDateTime updatedAt;

@PrePersist
@PreUpdate
public void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.crimeLink.analyzer.repository;

import com.crimeLink.analyzer.entity.BackupMetadata;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface BackupMetadataRepository extends JpaRepository<BackupMetadata, Long> {

List<BackupMetadata> findAllByOrderByCreatedAtDesc();

Optional<BackupMetadata> findByFilename(String filename);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.crimeLink.analyzer.repository;

import com.crimeLink.analyzer.entity.SystemSetting;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface SystemSettingRepository extends JpaRepository<SystemSetting, Long> {

Optional<SystemSetting> findBySettingKey(String settingKey);
}
Loading
Loading