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
-
+
+
+
+
+ Change Access Token for Selected Repositories
+
+
+ You are about to change the access token for {{ selectedRepos.length }} selected repositories.
+
+
+
+
+
+
+ Cancel
+
+ Change Token
+
+
\ 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
+
+
+ Token
+
Upload SBOM for SCA
-
-
+
+
+
+ No branches found in database. Scan will use default branch.
+
{{ sbomError }}
@@ -205,6 +218,25 @@ Upload SBOM for SCA
+
+
+ Change access token
+
+
+
+
+
+ {{ tokenError }}
+
+
+ Cancel
+
+
+ Save
+
+
+
+
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