diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/controller/CodeRepoController.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/controller/CodeRepoController.java index 0db9c016..929f5a8f 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/controller/CodeRepoController.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/controller/CodeRepoController.java @@ -37,6 +37,22 @@ public class CodeRepoController { private final CodeRepoApiService codeRepoApiService; private final DeleteCodeRepoService deleteCodeRepoService; + private ResponseEntity buildCreateRepoErrorResponse(Exception e) { + Throwable rootCause = e; + while (rootCause.getCause() != null) { + rootCause = rootCause.getCause(); + } + String message = rootCause.getMessage(); + boolean repoAlreadyExists = message != null && message.toLowerCase().contains("already exists"); + if (repoAlreadyExists) { + return new ResponseEntity<>(new StatusDTO("Repository already exists."), HttpStatus.CONFLICT); + } + return new ResponseEntity<>( + new StatusDTO("Error during repository import. Please contact administrator."), + HttpStatus.BAD_REQUEST + ); + } + @PreAuthorize("hasAuthority('USER')") @PostMapping(value= "/api/v1/coderepo/create/gitlab") public ResponseEntity createCodeRepoGitlab(@Valid @RequestBody CreateCodeRepoRequestDto createCodeRepoRequestDto, Principal principal){ @@ -44,9 +60,8 @@ public ResponseEntity createCodeRepoGitlab(@Valid @RequestBody Create createCodeRepoService.createCodeRepo(createCodeRepoRequestDto, CodeRepo.RepoType.GITLAB).block(); return new ResponseEntity<>(new StatusDTO("ok"), HttpStatus.CREATED); } catch (Exception e){ - e.printStackTrace(); - log.error("[CodeRepo] Error Creating CodeRepo {} by {}", createCodeRepoRequestDto.getName(), principal.getName()); - return new ResponseEntity<>(new StatusDTO("Not ok"), HttpStatus.BAD_REQUEST); + log.error("[CodeRepo] Error creating CodeRepo {} by {}: {}", createCodeRepoRequestDto.getName(), principal.getName(), e.getMessage()); + return buildCreateRepoErrorResponse(e); } } @PreAuthorize("hasAuthority('USER')") @@ -56,9 +71,8 @@ public ResponseEntity createCodeRepoGitHub(@Valid @RequestBody Create createCodeRepoService.createCodeRepo(createCodeRepoRequestDto, CodeRepo.RepoType.GITHUB).block(); return new ResponseEntity<>(new StatusDTO("ok"), HttpStatus.CREATED); } catch (Exception e){ - e.printStackTrace(); - log.error("[CodeRepo] Error Creating CodeRepo {} by {}", createCodeRepoRequestDto.getName(), principal.getName()); - return new ResponseEntity<>(new StatusDTO("Not ok"), HttpStatus.BAD_REQUEST); + log.error("[CodeRepo] Error creating CodeRepo {} by {}: {}", createCodeRepoRequestDto.getName(), principal.getName(), e.getMessage()); + return buildCreateRepoErrorResponse(e); } } @@ -69,9 +83,8 @@ public ResponseEntity createCodeRepoGitea(@Valid @RequestBody CreateC createCodeRepoService.createCodeRepo(createCodeRepoRequestDto, CodeRepo.RepoType.GITEA).block(); return new ResponseEntity<>(new StatusDTO("ok"), HttpStatus.CREATED); } catch (Exception e){ - e.printStackTrace(); - log.error("[CodeRepo] Error Creating CodeRepo {} by {}", createCodeRepoRequestDto.getName(), principal.getName()); - return new ResponseEntity<>(new StatusDTO("Not ok"), HttpStatus.BAD_REQUEST); + log.error("[CodeRepo] Error creating CodeRepo {} by {}: {}", createCodeRepoRequestDto.getName(), principal.getName(), e.getMessage()); + return buildCreateRepoErrorResponse(e); } } @@ -82,9 +95,8 @@ public ResponseEntity createCodeRepoBitbucket(@Valid @RequestBody Cre createCodeRepoService.createCodeRepo(createCodeRepoRequestDto, CodeRepo.RepoType.BITBUCKET).block(); return new ResponseEntity<>(new StatusDTO("ok"), HttpStatus.CREATED); } catch (Exception e){ - e.printStackTrace(); - log.error("[CodeRepo] Error Creating CodeRepo {} by {}", createCodeRepoRequestDto.getName(), principal.getName()); - return new ResponseEntity<>(new StatusDTO("Not ok"), HttpStatus.BAD_REQUEST); + log.error("[CodeRepo] Error creating CodeRepo {} by {}: {}", createCodeRepoRequestDto.getName(), principal.getName(), e.getMessage()); + return buildCreateRepoErrorResponse(e); } } @@ -228,15 +240,30 @@ public ResponseEntity bulkChangeTeam(@Valid @RequestBody BulkChangeTe } } - @PreAuthorize("hasAuthority('ADMIN')") - @PutMapping(value = "/api/v1/coderepo/bulk/change-token") + @PreAuthorize("hasAnyAuthority('ADMIN','TEAM_MANAGER')") + @PostMapping(value = "/api/v1/coderepo/bulk/change-token") public ResponseEntity bulkChangeToken(@Valid @RequestBody BulkChangeTokenRequestDto request, Principal principal) { try { - codeRepoApiService.bulkChangeAccessToken(request.getRepositoryIds(), request.getAccessToken()); + codeRepoApiService.bulkChangeAccessToken(request.getRepositoryIds(), request.getAccessToken(), principal); return ResponseEntity.ok(new StatusDTO("Access token updated for selected repositories.")); } catch (Exception e) { log.error("[CodeRepo] Error during bulk token change by {}: {}", principal.getName(), e.getMessage()); - return new ResponseEntity<>(new StatusDTO(e.getMessage()), HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(new StatusDTO("Error during access token update."), HttpStatus.BAD_REQUEST); + } + } + + @PreAuthorize("hasAnyAuthority('ADMIN','TEAM_MANAGER')") + @PostMapping(value = "/api/v1/coderepo/{id}/change-token") + public ResponseEntity changeToken( + @PathVariable("id") Long id, + @Valid @RequestBody ChangeAccessTokenRequestDto request, + Principal principal) { + try { + codeRepoApiService.changeAccessToken(id, request.getAccessToken(), principal); + return ResponseEntity.ok(new StatusDTO("Access token updated for repository.")); + } catch (Exception e) { + log.error("[CodeRepo] Error during token change for repo {} by {}: {}", id, principal.getName(), e.getMessage()); + return new ResponseEntity<>(new StatusDTO("Error during access token update."), HttpStatus.BAD_REQUEST); } } diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/dto/ChangeAccessTokenRequestDto.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/dto/ChangeAccessTokenRequestDto.java new file mode 100644 index 00000000..ea9b97bf --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/dto/ChangeAccessTokenRequestDto.java @@ -0,0 +1,13 @@ +package io.mixeway.mixewayflowapi.api.coderepo.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ChangeAccessTokenRequestDto { + + @NotBlank(message = "Access token cannot be empty.") + private String accessToken; +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/service/CodeRepoApiService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/service/CodeRepoApiService.java index 85f8f629..314c68c6 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/service/CodeRepoApiService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/coderepo/service/CodeRepoApiService.java @@ -202,7 +202,19 @@ public void bulkChangeTeam(List repositoryIds, Long newTeamId, Principal p updateCodeRepoService.bulkChangeTeam(repositoryIds, newTeam); } - public void bulkChangeAccessToken(List repositoryIds, String accessToken) { + public void changeAccessToken(Long repoId, String accessToken, Principal principal) { + CodeRepo repo = findCodeRepoService.findById(repoId) + .orElseThrow(() -> new CodeRepoNotFoundException("Repository not found")); + permissionFactory.canUserManageTeam(repo.getTeam(), principal); + updateCodeRepoService.changeAccessToken(repo, accessToken); + } + + public void bulkChangeAccessToken(List repositoryIds, String accessToken, Principal principal) { + for (Long repositoryId : repositoryIds) { + CodeRepo repository = findCodeRepoService.findById(repositoryId) + .orElseThrow(() -> new CodeRepoNotFoundException("Repository not found: " + repositoryId)); + permissionFactory.canUserManageTeam(repository.getTeam(), principal); + } updateCodeRepoService.bulkChangeAccessToken(repositoryIds, accessToken); } diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/CodeRepoRepository.java b/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/CodeRepoRepository.java index 3ec3082f..40c343ef 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/CodeRepoRepository.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/CodeRepoRepository.java @@ -98,6 +98,10 @@ void updateRepositoryMetadata(@Param("repoId") Long repoId, @Query("UPDATE CodeRepo c SET c.accessToken = :accessToken WHERE c.id IN :repositoryIds") void updateAccessTokenForRepositories(@Param("repositoryIds") List repositoryIds, @Param("accessToken") String accessToken); + @Modifying + @Query("UPDATE CodeRepo c SET c.accessToken = :accessToken WHERE c.id = :repositoryId") + void updateAccessTokenForRepository(@Param("repositoryId") Long repositoryId, @Param("accessToken") String accessToken); + Optional findByRemoteIdAndRepourl(Long id, String repoUrl); boolean existsByTeamAndNameIgnoreCase(Team team, String trimmed); diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/ScanInfoRepository.java b/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/ScanInfoRepository.java index feb7ad3e..ea0c7bf2 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/ScanInfoRepository.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/db/repository/ScanInfoRepository.java @@ -13,6 +13,7 @@ public interface ScanInfoRepository extends CrudRepository { Optional findByCodeRepoAndCodeRepoBranchAndCommitId(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, String commitId); + List findAllByCodeRepoAndCodeRepoBranchAndCommitIdOrderByInsertedDateDesc(CodeRepo codeRepo, CodeRepoBranch codeRepoBranch, String commitId); List findByCodeRepo(CodeRepo repo); boolean existsByCodeRepoBranch(CodeRepoBranch codeRepoBranch); void deleteByCodeRepo(CodeRepo codeRepo); diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/CreateCodeRepoService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/CreateCodeRepoService.java index ac645c57..65a93f7a 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/CreateCodeRepoService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/CreateCodeRepoService.java @@ -89,7 +89,7 @@ public Mono createCodeRepo(CreateCodeRepoRequestDto createCodeRepoRequestD .forEach(finalCodeRepo::upsertLanguage); finalCodeRepo = codeRepoRepository.save(finalCodeRepo); - scaService.createDtrackProject(finalCodeRepo); + // scaService.createDtrackProject(finalCodeRepo); log.info("[CodeRepoService] Creating initial scan for {} default branch {}", codeRepo.getRepourl(), codeRepoBranch.getName()); scanManagerService.scanRepository(finalCodeRepo, finalCodeRepo.getDefaultBranch(), null, null); @@ -103,9 +103,11 @@ public Mono createCodeRepo(CreateCodeRepoRequestDto createCodeRepoRequestD } } else { - // FIX: Uncommented this block to throw an exception when the team is not found or the repo already exists. - log.warn("[CodeRepoService] Trying to add repository that exsits"); - //throw new TeamNotFoundException("[CreateCodeRepoService] Team " + createCodeRepoRequestDto.getTeam() + " not found or repo already exists."); + if (team.isEmpty()) { + throw new TeamNotFoundException("[CreateCodeRepoService] Team " + createCodeRepoRequestDto.getTeam() + " not found."); + } + log.warn("[CodeRepoService] Trying to add repository that already exists: {}", repoResponse.getWebUrl()); + throw new IllegalArgumentException("Repository already exists."); } }) // Schedule the execution of the blocking code on the 'boundedElastic' scheduler. diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java index 2bda478a..b6057d92 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/coderepo/UpdateCodeRepoService.java @@ -8,7 +8,6 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.checkerframework.checker.units.qual.C; import org.springframework.data.jpa.repository.Modifying; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -344,6 +343,17 @@ public void bulkChangeAccessToken(List repositoryIds, String accessToken) ); } + @Modifying + @Transactional + public void changeAccessToken(CodeRepo codeRepo, String accessToken) { + if (!StringUtils.hasText(accessToken)) { + throw new IllegalArgumentException("Access token cannot be empty."); + } + String sanitizedToken = accessToken.trim(); + codeRepoRepository.updateAccessTokenForRepository(codeRepo.getId(), sanitizedToken); + log.info("Changed access token for repository {}", codeRepo.getRepourl()); + } + @Transactional public void renameCodeRepo(CodeRepo codeRepo, String newName) { if (!StringUtils.hasText(newName)) { diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/scaninfo/CreateScanInfoService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/scaninfo/CreateScanInfoService.java index 7cd8b788..675e2648 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/domain/scaninfo/CreateScanInfoService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/domain/scaninfo/CreateScanInfoService.java @@ -5,10 +5,11 @@ import io.mixeway.mixewayflowapi.db.entity.ScanInfo; import io.mixeway.mixewayflowapi.db.repository.ScanInfoRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.Optional; +import java.util.List; /** * Service class for creating or updating immutable {@link ScanInfo} entities. @@ -20,6 +21,7 @@ */ @Service @RequiredArgsConstructor +@Log4j2 public class CreateScanInfoService { private final ScanInfoRepository scanInfoRepository; @@ -54,12 +56,16 @@ public ScanInfo createOrUpdateScanInfo(CodeRepo codeRepo, CodeRepoBranch codeRep int iacHigh, int iacCritical, int secretsHigh, int secretsCritical, int gitlabHigh, int gitlabCritical, int dastHigh, int dastCritical) { - Optional existingScanInfoOpt = scanInfoRepository.findByCodeRepoAndCodeRepoBranchAndCommitId(codeRepo, codeRepoBranch, commitId); - ScanInfo scanInfo; + List existingScanInfos = scanInfoRepository + .findAllByCodeRepoAndCodeRepoBranchAndCommitIdOrderByInsertedDateDesc(codeRepo, codeRepoBranch, commitId); - if (existingScanInfoOpt.isPresent()) { - scanInfo = existingScanInfoOpt.get(); + if (!existingScanInfos.isEmpty()) { + if (existingScanInfos.size() > 1) { + log.warn("[ScanInfo] Found {} duplicate scan_info rows for repoId={}, branchId={}, commitId={}. Updating latest row only.", + existingScanInfos.size(), codeRepo.getId(), codeRepoBranch.getId(), commitId); + } + scanInfo = existingScanInfos.get(0); scanInfo.updateScanInfo(scaScanStatus, sastScanStatus, iacScanStatus, secretsScanStatus, gitlabScanStatus, scaHigh, scaCritical, sastHigh, sastCritical, iacHigh, iacCritical, secretsHigh, secretsCritical, gitlabHigh, gitlabCritical); } else { diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/repo/service/RepositoryMetadataSyncService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/repo/service/RepositoryMetadataSyncService.java index 2bac188f..18e73956 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/repo/service/RepositoryMetadataSyncService.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/integrations/repo/service/RepositoryMetadataSyncService.java @@ -10,6 +10,7 @@ import io.mixeway.mixewayflowapi.integrations.repo.apiclient.GitLabApiClientService; import io.mixeway.mixewayflowapi.integrations.repo.apiclient.GiteaApiClientService; import io.mixeway.mixewayflowapi.integrations.repo.dto.ImportCodeRepoResponseDto; +import io.mixeway.mixewayflowapi.scanmanager.service.ScanManagerService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; @@ -31,6 +32,7 @@ public class RepositoryMetadataSyncService { private final GitHubApiClientService gitHubApiClientService; private final GiteaApiClientService giteaApiClientService; private final BitbucketApiClientService bitbucketApiClientService; + private final ScanManagerService scanManagerService; public void syncAllRepositoriesMetadata() { codeRepoRepository.findAll().forEach(this::syncRepositoryMetadata); @@ -38,6 +40,7 @@ public void syncAllRepositoriesMetadata() { private void syncRepositoryMetadata(CodeRepo codeRepo) { try { + CodeRepo currentCodeRepo = codeRepo; RepoMetadata metadata = fetchRepositoryMetadata(codeRepo); if (metadata == null || !hasText(metadata.name()) || !hasText(metadata.webUrl()) || !hasText(metadata.defaultBranch())) { return; @@ -52,9 +55,10 @@ private void syncRepositoryMetadata(CodeRepo codeRepo) { if (nameChanged || urlChanged || defaultBranchChanged) { codeRepoRepository.updateRepositoryMetadata(codeRepo.getId(), normalizedName, metadata.webUrl(), defaultBranch); + currentCodeRepo = codeRepoRepository.findById(codeRepo.getId()).orElse(codeRepo); } - BranchSyncResult branchSyncResult = syncRepositoryBranches(codeRepo, metadata.defaultBranch(), metadata.webUrl()); + BranchSyncResult branchSyncResult = syncRepositoryBranches(currentCodeRepo, metadata.defaultBranch(), metadata.webUrl()); if (nameChanged || urlChanged || defaultBranchChanged || branchSyncResult.hasChanges()) { log.info( "Repository metadata sync updated repoId={} remoteId={} [nameChanged={}, urlChanged={}, defaultBranchChanged={}, branchesAdded={}, branchesMarkedExisting={}, branchesMarkedMissing={}]", @@ -68,6 +72,12 @@ private void syncRepositoryMetadata(CodeRepo codeRepo) { branchSyncResult.branchesMarkedMissing() ); } + + if (defaultBranchChanged) { + log.info("Default branch changed for repoId={} (remoteId={}). Triggering scan on branch {}.", + currentCodeRepo.getId(), currentCodeRepo.getRemoteId(), currentCodeRepo.getDefaultBranch().getName()); + scanManagerService.scanRepository(currentCodeRepo, currentCodeRepo.getDefaultBranch(), null, null); + } } catch (Exception e) { log.warn("Failed to sync metadata for repo {} (id={}): {}", codeRepo.getName(), codeRepo.getId(), e.getMessage()); } diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/utils/PermissionFactory.java b/backend/src/main/java/io/mixeway/mixewayflowapi/utils/PermissionFactory.java index d362810f..6585f9df 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/utils/PermissionFactory.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/utils/PermissionFactory.java @@ -72,19 +72,19 @@ public Optional findById(Long teamId, Principal principal) { public void canUserManageTeam(Team team, Principal principal) { UserInfo userInfo = findUserService.findUser(principal.getName()); UserRole adminRole = findRoleService.findUserRole("ADMIN"); + boolean isAdmin = userInfo.getRoles().contains(adminRole); + boolean isTeamMember = userInfo.getTeams().contains(team); - // In SaaS mode, check organization boundaries - if (appConfigService.isSaasMode()) { - if (!userInfo.getRoles().contains(adminRole) || - (team.getOrganization() != null && - !userInfo.getOrganizations().contains(team.getOrganization()))) { - throw new UnauthorizedException(""); - } - } else { - // STANDALONE mode - existing logic - if (!userInfo.getRoles().contains(adminRole) && !userInfo.getTeams().contains(team)) { - throw new UnauthorizedException(""); - } + // Non-admins (including TEAM_MANAGER) can manage only their own teams. + if (!isAdmin && !isTeamMember) { + throw new UnauthorizedException(""); + } + + // In SaaS mode, also enforce organization boundary for both admin and team members. + if (appConfigService.isSaasMode() + && team.getOrganization() != null + && !userInfo.getOrganizations().contains(team.getOrganization())) { + throw new UnauthorizedException(""); } } public void canUserAccessTeam(Team team, Principal principal) { diff --git a/frontend/src/app/service/DashboardService.ts b/frontend/src/app/service/DashboardService.ts index 972d307d..b4af353b 100644 --- a/frontend/src/app/service/DashboardService.ts +++ b/frontend/src/app/service/DashboardService.ts @@ -65,4 +65,12 @@ export class DashboardService { return this.http.put(this.loginUrl + '/api/v1/coderepo/bulk/change-team', {"repositoryIds":repoIds,"newTeamId":newTeamId},{ withCredentials: true }); } + + changeAccessTokenForRepos(repoIds: number[], accessToken: string): Observable { + return this.http.post( + `${this.loginUrl}/api/v1/coderepo/bulk/change-token`, + { repositoryIds: repoIds, accessToken }, + { withCredentials: true } + ); + } } diff --git a/frontend/src/app/service/RepoService.ts b/frontend/src/app/service/RepoService.ts index c8a062db..b9177f8b 100644 --- a/frontend/src/app/service/RepoService.ts +++ b/frontend/src/app/service/RepoService.ts @@ -86,6 +86,13 @@ export class RepoService { { withCredentials: true } ); } + changeAccessToken(repoId: number, accessToken: string): Observable { + return this.http.post( + `${this.loginUrl}/api/v1/coderepo/${repoId}/change-token`, + { accessToken }, + { withCredentials: true } + ); + } rename(repoId: number, newName: string) { return this.http.put( `${this.loginUrl}/api/v1/coderepo/${repoId}/rename`, @@ -100,7 +107,7 @@ export class RepoService { ); } changeAccessTokenForRepos(repoIds: number[], accessToken: string): Observable { - return this.http.put( + return this.http.post( `${this.loginUrl}/api/v1/coderepo/bulk/change-token`, { repositoryIds: repoIds, accessToken }, { withCredentials: true } diff --git a/frontend/src/app/views/dashboard/dashboard.component.html b/frontend/src/app/views/dashboard/dashboard.component.html index a87dc0be..211af7b5 100644 --- a/frontend/src/app/views/dashboard/dashboard.component.html +++ b/frontend/src/app/views/dashboard/dashboard.component.html @@ -224,9 +224,13 @@
Connect Repository Provider - + @@ -1342,4 +1346,33 @@
Change Team for Selected Repositories
Change Team + + + + +
Change Access Token for Selected Repositories
+
+ +

You are about to change the access token for {{ selectedRepos.length }} selected repositories.

+ +
+ Repositories: +
    +
  • {{ repo.target }}
  • +
+
+ +
+
+ + +
+
+
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/views/dashboard/dashboard.component.ts b/frontend/src/app/views/dashboard/dashboard.component.ts index bf7ae8a7..d3f71589 100644 --- a/frontend/src/app/views/dashboard/dashboard.component.ts +++ b/frontend/src/app/views/dashboard/dashboard.component.ts @@ -160,6 +160,7 @@ export class DashboardComponent implements OnInit { public visibleConnectProvider = false; connectProviderForm: FormGroup; isAdmin = false; + isTeamManager = false; providerRows: any[] = []; providerColumns: any[] = [ { prop: 'providerType', name: 'Provider' }, @@ -171,6 +172,8 @@ export class DashboardComponent implements OnInit { selectedRepos: any[] = []; visibleChangeTeamModal = false; changeTeamForm: FormGroup; + visibleChangeTokenModal = false; + changeTokenForm: FormGroup; selectionType = SelectionType; public rowIdentity = (row: any): any => { return row.id; @@ -385,6 +388,9 @@ export class DashboardComponent implements OnInit { this.changeTeamForm = this.fb.group({ newTeamId: ['', Validators.required] }); + this.changeTokenForm = this.fb.group({ + accessToken: ['', Validators.required] + }); this.#destroyRef.onDestroy(() => { if (this.repoSearchDebounceTimer) { clearTimeout(this.repoSearchDebounceTimer); @@ -418,6 +424,14 @@ export class DashboardComponent implements OnInit { this.visibleChangeTeamModal = event; } + openChangeTokenModal() { + this.visibleChangeTokenModal = true; + } + + handleChangeTokenModalChange(event: boolean) { + this.visibleChangeTokenModal = event; + } + onSubmitChangeTeam() { if (this.changeTeamForm.invalid) { this.showToast('danger', 'Please select a new team.'); @@ -444,6 +458,37 @@ export class DashboardComponent implements OnInit { }); } + onSubmitChangeToken() { + if (this.changeTokenForm.invalid) { + this.showToast('danger', 'Please provide a new access token.'); + return; + } + if (this.selectedRepos.length === 0) { + this.showToast('danger', 'No repositories selected.'); + return; + } + + const repoIds = this.selectedRepos.map(repo => repo.id); + const accessToken = (this.changeTokenForm.value.accessToken || '').trim(); + if (!accessToken) { + this.showToast('danger', 'Access token cannot be empty.'); + return; + } + + this.dashboardService.changeAccessTokenForRepos(repoIds, accessToken).subscribe({ + next: () => { + this.showToast('success', 'Access token updated for selected repositories.'); + this.visibleChangeTokenModal = false; + this.selectedRepos = []; + this.changeTokenForm.reset(); + this.loadCodeRepos(this.repoCurrentPage); + }, + error: (err) => { + this.showToast('danger', `Error changing access token: ${err.error?.message || 'Please try again.'}`); + } + }); + } + // Add methods to handle the new modal connectProviderModal() { this.ensureTeamsLoaded(); @@ -484,6 +529,7 @@ export class DashboardComponent implements OnInit { let userRole = localStorage.getItem('userRole'); this.role = userRole; this.isAdmin = userRole === 'ADMIN'; + this.isTeamManager = userRole === 'TEAM_MANAGER'; this.authService.hc().subscribe({ next: (response) => { if (!userRole) { @@ -1348,6 +1394,7 @@ export class DashboardComponent implements OnInit { this.visibleNewTeam = false; this.visibleSingleRepoModal = false; this.visibleConnectProvider = false; // Add this line + this.visibleChangeTokenModal = false; } closeNewTeamModal() { @@ -1424,7 +1471,7 @@ export class DashboardComponent implements OnInit { }, error: (error) => { this.toastStatus = "danger" - this.toastMessage = "Problem during repo import. If it will keep occurring contact system administrator." + this.toastMessage = this.resolveRepoImportErrorMessage(error) this.toggleToast(); } }); @@ -1524,8 +1571,8 @@ export class DashboardComponent implements OnInit { this.loadSecurityData(); // Reload security data after adding a repository this.visibleSingleRepoModal = false; }, - error: () => { - this.showToast("danger", "Problem during repo import. If it will keep occurring contact system administrator."); + error: (error) => { + this.showToast("danger", this.resolveRepoImportErrorMessage(error)); }, }); }, @@ -1560,8 +1607,8 @@ export class DashboardComponent implements OnInit { this.loadSecurityData(); // Reload security data after adding a repository this.visibleSingleRepoModal = false; }, - error: () => { - this.showToast("danger", "Problem during repo import. If it will keep occurring contact system administrator."); + error: (error) => { + this.showToast("danger", this.resolveRepoImportErrorMessage(error)); }, }); }, @@ -1594,8 +1641,8 @@ export class DashboardComponent implements OnInit { this.loadSecurityData(); this.visibleSingleRepoModal = false; }, - error: () => { - this.showToast("danger", "Problem during repo import. If it will keep occurring contact system administrator."); + error: (error) => { + this.showToast("danger", this.resolveRepoImportErrorMessage(error)); }, }); }, @@ -1627,8 +1674,8 @@ export class DashboardComponent implements OnInit { this.loadSecurityData(); this.visibleSingleRepoModal = false; }, - error: () => { - this.showToast("danger", "Problem during repo import. If it will keep occurring contact system administrator."); + error: (error) => { + this.showToast("danger", this.resolveRepoImportErrorMessage(error)); }, }); }, @@ -1646,6 +1693,14 @@ export class DashboardComponent implements OnInit { this.toggleToast(); } + private resolveRepoImportErrorMessage(error: any): string { + const apiMessage = error?.error?.status || error?.error?.message; + if (typeof apiMessage === 'string' && apiMessage.trim().length > 0) { + return apiMessage; + } + return "Problem during repo import. If it will keep occurring contact system administrator."; + } + // Helper method to get the base URL private getBaseUrl(url: string): string { try { diff --git a/frontend/src/app/views/show-repo/repository-info/repository-info.component.html b/frontend/src/app/views/show-repo/repository-info/repository-info.component.html index 3116f9d4..b9d935dc 100644 --- a/frontend/src/app/views/show-repo/repository-info/repository-info.component.html +++ b/frontend/src/app/views/show-repo/repository-info/repository-info.component.html @@ -79,6 +79,13 @@

Rename +

- - + + + + No branches found in database. Scan will use default branch. +
{{ sbomError }}
@@ -205,6 +218,25 @@
Upload SBOM for SCA
+ + +
Change access token
+ +
+ + + +
{{ tokenError }}
+
+ + + + +
+
Rename repository
diff --git a/frontend/src/app/views/show-repo/repository-info/repository-info.component.ts b/frontend/src/app/views/show-repo/repository-info/repository-info.component.ts index fc4f7d6a..02e2b8df 100644 --- a/frontend/src/app/views/show-repo/repository-info/repository-info.component.ts +++ b/frontend/src/app/views/show-repo/repository-info/repository-info.component.ts @@ -83,6 +83,10 @@ export class RepositoryInfoComponent implements OnInit { renameSaving = false; renameError: string | null = null; renameForm = { name: '' }; + tokenModalVisible = false; + tokenSaving = false; + tokenError: string | null = null; + tokenForm = { accessToken: '' }; scanDropdownOpen = false; branchScanModalVisible = false; @@ -95,6 +99,7 @@ export class RepositoryInfoComponent implements OnInit { sbomBranch = ''; sbomError: string | null = null; sbomFile: File | null = null; + sbomAvailableBranches: string[] = []; @Output() runScanEvent = new EventEmitter(); @Output() runScanBranchEvent = new EventEmitter(); @@ -164,7 +169,11 @@ export class RepositoryInfoComponent implements OnInit { this.sbomModalVisible = true; this.sbomError = null; this.sbomFile = null; + this.sbomAvailableBranches = this.getDbBranchNames(); this.sbomBranch = this.repoData?.defaultBranch?.name ?? ''; + if (!this.sbomAvailableBranches.includes(this.sbomBranch)) { + this.sbomBranch = this.sbomAvailableBranches[0] ?? ''; + } } onSbomFileChange(event: Event): void { @@ -192,6 +201,24 @@ export class RepositoryInfoComponent implements OnInit { }); } + private getDbBranchNames(): string[] { + const names = new Set(); + const defaultBranchName = this.repoData?.defaultBranch?.name; + if (defaultBranchName) { + names.add(defaultBranchName); + } + + const branches = this.repoData?.branches ?? []; + for (const branch of branches) { + const branchName = branch?.name; + if (branchName) { + names.add(branchName); + } + } + + return Array.from(names).sort((a, b) => a.localeCompare(b)); + } + constructor(private codeService: RepoService) {} @@ -204,6 +231,11 @@ export class RepositoryInfoComponent implements OnInit { this.renameForm.name = this.repoData?.name ?? ''; this.renameModalVisible = true; } + openTokenModal() { + this.tokenError = null; + this.tokenForm.accessToken = ''; + this.tokenModalVisible = true; + } requestDeleteRepo(): void { this.deleteRepoEvent.emit(); } @@ -237,4 +269,27 @@ export class RepositoryInfoComponent implements OnInit { } }); } + confirmTokenChange() { + const id = this.repoData?.id; + if (!id) return; + + const trimmed = (this.tokenForm.accessToken || '').trim(); + if (!trimmed) { + this.tokenError = 'Access token cannot be empty.'; + return; + } + + this.tokenSaving = true; + this.codeService.changeAccessToken(id, trimmed).subscribe({ + next: () => { + this.tokenSaving = false; + this.tokenModalVisible = false; + this.tokenForm.accessToken = ''; + }, + error: (err) => { + this.tokenSaving = false; + this.tokenError = err?.error?.message || 'Token update failed.'; + } + }); + } } \ No newline at end of file