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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.mixeway.mixewayflowapi.api.coderepo.service.CodeRepoApiService;
import io.mixeway.mixewayflowapi.db.entity.CodeRepo;
import io.mixeway.mixewayflowapi.domain.coderepo.CreateCodeRepoService;
import io.mixeway.mixewayflowapi.domain.coderepo.DeleteCodeRepoService;
import io.mixeway.mixewayflowapi.exceptions.CodeRepoNotFoundException;
import io.mixeway.mixewayflowapi.exceptions.TeamNotFoundException;
import io.mixeway.mixewayflowapi.exceptions.UnauthorizedException;
Expand All @@ -27,6 +28,7 @@
public class CodeRepoController {
private final CreateCodeRepoService createCodeRepoService;
private final CodeRepoApiService codeRepoApiService;
private final DeleteCodeRepoService deleteCodeRepoService;

@PreAuthorize("hasAuthority('USER')")
@PostMapping(value= "/api/v1/coderepo/create/gitlab")
Expand Down Expand Up @@ -160,4 +162,22 @@ public ResponseEntity<StatusDTO> renameCodeRepo(
return new ResponseEntity<>(new StatusDTO(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}

@PreAuthorize("hasAuthority('ADMIN')")
@DeleteMapping(value = "/api/v1/coderepo/{id}")
public ResponseEntity<StatusDTO> deleteCodeRepo(@PathVariable("id") Long id, Principal principal) {
try {
deleteCodeRepoService.deleteRepo(id, principal);
return ResponseEntity.ok(new StatusDTO("Repository deleted."));
} catch (UnauthorizedException e) {
log.error("[CodeRepo] Unauthorized delete attempt for {} by {}", id, principal.getName());
return new ResponseEntity<>(new StatusDTO("Unauthorized"), HttpStatus.FORBIDDEN);
} catch (CodeRepoNotFoundException e) {
log.error("[CodeRepo] Repository not found for delete {} by {}", id, principal.getName());
return new ResponseEntity<>(new StatusDTO(e.getMessage()), HttpStatus.NOT_FOUND);
} catch (Exception e) {
log.error("[CodeRepo] Delete failed for id {} by {}: {}", id, principal.getName(), e.getMessage());
return new ResponseEntity<>(new StatusDTO("Internal server error"), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.mixeway.mixewayflowapi.api.threatintel.controller;

import io.mixeway.mixewayflowapi.api.teamfindings.controller.FindingsByTeamController;
import io.mixeway.mixewayflowapi.api.teamfindings.service.FindingsByTeamService;
import io.mixeway.mixewayflowapi.api.threatintel.dto.ItemListResponse;
import io.mixeway.mixewayflowapi.api.threatintel.dto.RemovedVulnerabilityDTO;
import io.mixeway.mixewayflowapi.api.threatintel.dto.ReviewedVulnerabilityDTO;
Expand All @@ -15,6 +17,7 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;
Expand All @@ -27,6 +30,7 @@
public class ThreatIntelController {

private final ThreatIntelService threatIntelService;
private final FindingsByTeamService findingsByTeamService;

@PreAuthorize("hasAuthority('USER')")
@GetMapping(value= "/api/v1/threat-intel/findings")
Expand All @@ -36,8 +40,11 @@ public ResponseEntity<ItemListResponse> getThreats(Principal principal){

@PreAuthorize("hasAuthority('USER')")
@GetMapping(value= "/api/v1/threat-intel/findings/{remoteId}")
public ResponseEntity<ItemListResponse> getThreatsForTeam(Principal principal, @PathVariable("remoteId") String remoteId){
public ResponseEntity<ItemListResponse> getThreatsForTeam(@RequestHeader("X-API-KEY") String apiKey, Principal principal, @PathVariable("remoteId") String remoteId){
try {
if (!findingsByTeamService.isValidApiKey(apiKey)) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
return threatIntelService.getThreatsForTeam(principal, remoteId);
} catch (TeamNotFoundException e){
log.warn(e.getLocalizedMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,4 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,6 @@ List<DailyFindings> findETAFindingsBetweenDates(@Param("startDate") LocalDate st
*/
List<CodeRepoFindingStats> findByCodeRepoOrderByDateInsertedDesc(CodeRepo codeRepo);

void deleteByCodeRepo(CodeRepo codeRepo);

}
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ Finding findFirstByCodeRepoAndVulnerabilityAndLocationAndStatus(CodeRepo codeRep
String location,
Finding.Status status);

void deleteByCodeRepo(CodeRepo codeRepo);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update Finding f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public interface ScanInfoRepository extends CrudRepository<ScanInfo, Long> {

Optional<ScanInfo> findByCodeRepoAndCodeRepoBranchAndCommitId(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, String commitId);
List<ScanInfo> findByCodeRepo(CodeRepo repo);
void deleteByCodeRepo(CodeRepo codeRepo);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.mixeway.mixewayflowapi.domain.coderepo;

import io.mixeway.mixewayflowapi.db.entity.CodeRepo;
import io.mixeway.mixewayflowapi.db.repository.AppDataTypeRepository;
import io.mixeway.mixewayflowapi.db.repository.CodeRepoFindingStatsRepository;
import io.mixeway.mixewayflowapi.db.repository.CodeRepoRepository;
import io.mixeway.mixewayflowapi.db.repository.FindingRepository;
import io.mixeway.mixewayflowapi.db.repository.ScanInfoRepository;
import io.mixeway.mixewayflowapi.exceptions.CodeRepoNotFoundException;
import io.mixeway.mixewayflowapi.utils.PermissionFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.Principal;
import java.util.Collections;

@Service
@RequiredArgsConstructor
@Log4j2
public class DeleteCodeRepoService {
private final FindCodeRepoService findCodeRepoService;
private final CodeRepoRepository codeRepoRepository;
private final FindingRepository findingRepository;
private final ScanInfoRepository scanInfoRepository;
private final CodeRepoFindingStatsRepository codeRepoFindingStatsRepository;
private final AppDataTypeRepository appDataTypeRepository;
private final PermissionFactory permissionFactory;

@Transactional
public void deleteRepo(Long repoId, Principal principal) {
CodeRepo repo = findCodeRepoService.findById(repoId)
.orElseThrow(() -> new CodeRepoNotFoundException("Code repository not found"));

permissionFactory.canUserManageTeam(repo.getTeam(), principal);

if (repo.getComponents() != null && !repo.getComponents().isEmpty()) {
repo.setComponents(Collections.emptyList());
}

findingRepository.deleteByCodeRepo(repo);
scanInfoRepository.deleteByCodeRepo(repo);
codeRepoFindingStatsRepository.deleteByCodeRepo(repo);
appDataTypeRepository.deleteAllByCodeRepo(repo);
codeRepoRepository.delete(repo);

log.info("[CodeRepo] Deleted repository {} by {}", repoId, principal.getName());
}
}
6 changes: 6 additions & 0 deletions frontend/src/app/service/RepoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,10 @@ export class RepoService {
{ withCredentials: true }
);
}
deleteRepo(repoId: number): Observable<any> {
return this.http.delete<any>(
`${this.loginUrl}/api/v1/coderepo/${repoId}`,
{ withCredentials: true }
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ <h2 class="mb-0 repo-title">
<svg cIcon name="cil-pencil" class="me-1"></svg>
Rename
</button>
<button *ngIf="userRole === 'ADMIN'"
cButton color="danger" variant="ghost" size="sm"
[cTooltip]="'Delete repository'"
(click)="requestDeleteRepo()">
<svg cIcon name="cil-trash" class="me-1"></svg>
Delete
</button>
</div>
</c-card-header>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class RepositoryInfoComponent implements OnInit {

@Output() runScanEvent = new EventEmitter<void>();
@Output() openChangeTeamModalEvent = new EventEmitter<void>();
@Output() deleteRepoEvent = new EventEmitter<void>();

ngOnInit(): void {
// Enhance chart options with better defaults
Expand Down Expand Up @@ -124,6 +125,9 @@ export class RepositoryInfoComponent implements OnInit {
this.renameForm.name = this.repoData?.name ?? '';
this.renameModalVisible = true;
}
requestDeleteRepo(): void {
this.deleteRepoEvent.emit();
}
confirmRename() {
const id = this.repoData?.id;
if (!id) return;
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/app/views/show-repo/show-repo.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
[options]="options"
(runScanEvent)="runScan()"
(openChangeTeamModalEvent)="openChangeTeamModal()"
(deleteRepoEvent)="openDeleteRepoModal()"
></app-repository-info>

<div class="mt-4">
Expand Down Expand Up @@ -656,6 +657,42 @@ <h5 cModalTitle>Change Team</h5>
</c-modal-footer>
</c-modal>

<c-modal size="lg"
id="deleteRepoConfirmationModal"
alignment="center"
[visible]="deleteRepoConfirmationVisible"
(visibleChange)="deleteRepoConfirmationVisible = $event">
<c-modal-header>
<h5 cModalTitle>Confirm Repository Deletion</h5>
</c-modal-header>
<c-modal-body>
<div class="alert alert-warning">
<h4 class="alert-heading">Warning!</h4>
<p>You are about to delete this repository. This action cannot be undone.</p>
<p>Please type "accept" to confirm this change:</p>
</div>
<div class="mb-3">
<input type="text"
class="form-control"
[(ngModel)]="deleteConfirmationText"
placeholder="Type 'accept' to confirm">
</div>
</c-modal-body>
<c-modal-footer>
<button (click)="closeDeleteRepoModal()"
cButton
color="secondary">
Cancel
</button>
<button (click)="executeDeleteRepo()"
cButton
color="danger"
[disabled]="deleteConfirmationText !== 'accept'">
Delete Repository
</button>
</c-modal-footer>
</c-modal>

<c-modal size="lg"
id="confirmationModal"
alignment="center"
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/app/views/show-repo/show-repo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ export class ShowRepoComponent implements OnInit, AfterViewInit {
availableTeams: Team[] = [];
selectedNewTeamId: number | null = null;

// Delete repo confirmation modal
deleteRepoConfirmationVisible: boolean = false;
deleteConfirmationText: string = '';

// Comment properties
newComment: string = '';
isAddingComment: boolean = false;
Expand Down Expand Up @@ -1031,6 +1035,37 @@ export class ShowRepoComponent implements OnInit, AfterViewInit {
});
}

openDeleteRepoModal(): void {
this.deleteConfirmationText = '';
this.deleteRepoConfirmationVisible = true;
}

closeDeleteRepoModal(): void {
this.deleteRepoConfirmationVisible = false;
this.deleteConfirmationText = '';
}

executeDeleteRepo(): void {
const repoId = this.repoData?.id || (this.repoId ? +this.repoId : null);
if (!repoId || this.deleteConfirmationText !== 'accept') {
return;
}
this.repoService.deleteRepo(repoId).subscribe({
next: () => {
this.toastStatus = 'success';
this.toastMessage = 'Repository deleted successfully';
this.toggleToast();
this.closeDeleteRepoModal();
this.router.navigate(['/dashboard']);
},
error: (error: any) => {
this.toastStatus = 'danger';
this.toastMessage = error?.error?.message || 'Error deleting repository';
this.toggleToast();
}
});
}

toggleBulkAction() {
this.bulkActionMode = !this.bulkActionMode;
if (!this.bulkActionMode) {
Expand Down