Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated code lifecycle: Let users generate personal access tokens #9182

Merged
merged 65 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
b28f062
personal acces tokens
SimonEntholzer Aug 3, 2024
b6a8bb4
add tests
SimonEntholzer Aug 3, 2024
c4c0a2e
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 3, 2024
5e05397
removed accidentally inserted backslash
SimonEntholzer Aug 3, 2024
b886e9e
Update src/main/webapp/app/shared/user-settings/vcs-access-tokens-set…
SimonEntholzer Aug 4, 2024
949b9a7
Update src/main/webapp/i18n/de/userSettings.json
SimonEntholzer Aug 4, 2024
800a5c1
Update src/main/webapp/i18n/de/userSettings.json
SimonEntholzer Aug 4, 2024
b9c72a8
Update src/main/webapp/i18n/de/userSettings.json
SimonEntholzer Aug 4, 2024
0cd4174
Update src/main/webapp/i18n/en/userSettings.json
SimonEntholzer Aug 4, 2024
652d128
Update src/main/webapp/i18n/en/userSettings.json
SimonEntholzer Aug 4, 2024
b2fcb2e
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 4, 2024
75288d3
fixed typescript
SimonEntholzer Aug 4, 2024
48e81be
indent
SimonEntholzer Aug 4, 2024
791ce68
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 5, 2024
189924d
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 12, 2024
a5fc83c
added kebab case test
SimonEntholzer Aug 12, 2024
cb6c135
removed architecture tests
SimonEntholzer Aug 12, 2024
6a6f044
fix comment
SimonEntholzer Aug 12, 2024
14e6c8d
remove test
SimonEntholzer Aug 12, 2024
166cb5c
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 14, 2024
59833d7
improved token creation UI
SimonEntholzer Aug 14, 2024
d1f2e64
improved token creation UI
SimonEntholzer Aug 14, 2024
d613ba0
added ts tests
SimonEntholzer Aug 14, 2024
741adbb
removed debugging string
SimonEntholzer Aug 15, 2024
487f275
fixed comment and tried to indent
SimonEntholzer Aug 15, 2024
d1385e6
adjustment to show token link for instructors if token exists
SimonEntholzer Aug 15, 2024
183dce8
fixed issues
SimonEntholzer Aug 16, 2024
fa2c3c2
improved html
SimonEntholzer Aug 16, 2024
713ffa3
fixed test and html formatting
SimonEntholzer Aug 16, 2024
7a66d71
removed unused profileservice
SimonEntholzer Aug 16, 2024
4521df3
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 17, 2024
b4c9feb
minor code improvement
SimonEntholzer Aug 17, 2024
5d226fc
fix test
SimonEntholzer Aug 18, 2024
f43c6a5
use user tokens by default, and participation tokens only when explic…
SimonEntholzer Aug 18, 2024
0dc6aa0
fix pacing and error handling
SimonEntholzer Aug 18, 2024
b66f65a
fix test
SimonEntholzer Aug 18, 2024
992818e
fix jest tests
SimonEntholzer Aug 18, 2024
81211ac
improve coverage
SimonEntholzer Aug 18, 2024
a94f979
participation-vcs-access-token test 1
SimonEntholzer Aug 19, 2024
57ccd09
user-vcs-access-token integration tests
SimonEntholzer Aug 19, 2024
d79705e
test bad request with invalid expiry date
SimonEntholzer Aug 19, 2024
e2a9b5a
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 19, 2024
41c4d0b
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 20, 2024
a5a82e3
improved code button UI
SimonEntholzer Aug 21, 2024
802ea07
fixed tests
SimonEntholzer Aug 21, 2024
789c9b6
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 21, 2024
e44f66b
improved translation and added date check
SimonEntholzer Aug 21, 2024
391f0da
fix typo
SimonEntholzer Aug 21, 2024
0e7c9f0
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 21, 2024
dfc86e0
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 21, 2024
30ed25d
fix using correct link for settings
SimonEntholzer Aug 21, 2024
2ebc64a
added additional tip for expired token
SimonEntholzer Aug 21, 2024
430ae33
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 24, 2024
1056551
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 24, 2024
644126d
improve client tests
SimonEntholzer Aug 25, 2024
ffb76ed
removed commented code
SimonEntholzer Aug 25, 2024
086110f
added maximum lifetime of tokens
SimonEntholzer Aug 25, 2024
daa5691
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 26, 2024
4679e93
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 26, 2024
9ce0eaa
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 27, 2024
d9df667
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 27, 2024
48fb74e
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 28, 2024
c56aa2d
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 30, 2024
3888c32
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 30, 2024
4273165
Merge branch 'develop' into feature/add-vcs-access-user-token
SimonEntholzer Aug 31, 2024
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 @@ -765,6 +765,17 @@ default Page<User> findAllWithGroupsByIsDeletedIsFalse(Pageable pageable) {
""")
void updateUserSshPublicKeyHash(@Param("userId") long userId, @Param("sshPublicKeyHash") String sshPublicKeyHash, @Param("sshPublicKey") String sshPublicKey);

@Modifying
@Transactional // ok because of modifying query
@Query("""
UPDATE User user
SET user.vcsAccessToken = :vcsAccessToken,
user.vcsAccessTokenExpiryDate = :vcsAccessTokenExpiryDate
WHERE user.id = :userId
""")
void updateUserVcsAccessToken(@Param("userId") long userId, @Param("vcsAccessToken") String vcsAccessToken,
@Param("vcsAccessTokenExpiryDate") ZonedDateTime vcsAccessTokenExpiryDate);
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

@Modifying
@Transactional // ok because of modifying query
@Query("""
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/service/dto/UserDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public class UserDTO extends AuditingEntityDTO {

private String vcsAccessToken;

private ZonedDateTime vcsAccessTokenExpiryDate;

private String sshPublicKey;

private ZonedDateTime irisAccepted;
Expand Down Expand Up @@ -250,6 +252,14 @@ public void setVcsAccessToken(String vcsAccessToken) {
this.vcsAccessToken = vcsAccessToken;
}

public void setVcsAccessTokenExpiryDate(ZonedDateTime zoneDateTime) {
this.vcsAccessTokenExpiryDate = zoneDateTime;
}

public ZonedDateTime getVcsAccessTokenExpiryDate() {
return vcsAccessTokenExpiryDate;
}

public String getSshPublicKey() {
return sshPublicKey;
}
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/web/rest/UserResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,41 @@ public ResponseEntity<Void> deleteSshPublicKey() {
log.debug("Successfully deleted SSH key of user {}", user.getLogin());
return ResponseEntity.ok().build();
}

/**
* PUT users/vcsAccessToken : creates a vcsAccessToken for a user
*
* @return the ResponseEntity with a userDTO containing the token: with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@PutMapping("users/vcsAccessToken")
@EnforceAtLeastStudent
public ResponseEntity<UserDTO> createVcsAccessToken() {
User user = userRepository.getUser();
log.debug("REST request to create a new VCS access token for user {}", user.getLogin());

// TODO add token generation here -> depends on https://github.com/ls1intum/Artemis/pull/8929
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
userRepository.updateUserVcsAccessToken(user.getId(), "vcpat-nsevT9FJUOK4uTbLFqC6DZMtWv9OZzVAU2Wbtzbt4WYO", ZonedDateTime.now().plusMonths(3));
log.debug("Successfully created a VCS access token for user {}", user.getLogin());
user = userRepository.getUser();
UserDTO userDTO = new UserDTO();
userDTO.setLogin(user.getLogin());
userDTO.setVcsAccessToken(user.getVcsAccessToken() + "abcde");
userDTO.setVcsAccessTokenExpiryDate(user.getVcsAccessTokenExpiryDate());
return ResponseEntity.ok(userDTO);
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

/**
* DELETE users/vcsAccessToken : deletes the vcsAccessToken of a user
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@DeleteMapping("users/vcsAccessToken")
@EnforceAtLeastStudent
public ResponseEntity<Void> deleteVcsAccessToken() {
User user = userRepository.getUser();
log.debug("REST request to remove VCS access token key of user {}", user.getLogin());
userRepository.updateUserVcsAccessToken(user.getId(), null, null);
log.debug("Successfully deleted VCS access token of user {}", user.getLogin());
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ public ResponseEntity<UserDTO> getAccount() {
UserDTO userDTO = new UserDTO(user);
// we set this value on purpose here: the user can only fetch their own information, make the token available for constructing the token-based clone-URL
userDTO.setVcsAccessToken(user.getVcsAccessToken());
userDTO.setVcsAccessTokenExpiryDate(user.getVcsAccessTokenExpiryDate());
userDTO.setSshPublicKey(user.getSshPublicKey());
log.info("GET /account {} took {}ms", user.getLogin(), System.currentTimeMillis() - start);
return ResponseEntity.ok(userDTO);
Expand Down
15 changes: 15 additions & 0 deletions src/main/webapp/app/core/auth/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Exercise, getCourseFromExercise } from 'app/entities/exercise.model';
import { Authority } from 'app/shared/constants/authority.constants';
import { TranslateService } from '@ngx-translate/core';
import { ProfileService } from 'app/shared/layouts/profiles/profile.service';
import { EntityResponseType } from 'app/complaints/complaint.service';

export interface IAccountService {
save: (account: any) => Observable<HttpResponse<any>>;
Expand Down Expand Up @@ -348,4 +349,18 @@ export class AccountService implements IAccountService {
}
return this.http.delete<void>('api/users/sshpublickey');
}

/**
* Sends a request to the server to delete the user's current vcsAccessToken
*/
deleteUserVcsAccessToken(): Observable<void> {
return this.http.delete<void>('api/users/vcsAccessToken');
}

/**
* Sends a request to the server to delete the user's current vcsAccessToken
*/
addNewVcsAccessToken(): Observable<EntityResponseType> {
return this.http.put<User>('api/users/vcsAccessToken', null, { observe: 'response' });
}
}
3 changes: 3 additions & 0 deletions src/main/webapp/app/core/user/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class User extends Account {
public visibleRegistrationNumber?: string;
public password?: string;
public vcsAccessToken?: string;
public vcsAccessTokenExpiryDate?: string;
public sshPublicKey?: string;
public irisAccepted?: dayjs.Dayjs;

Expand All @@ -35,6 +36,7 @@ export class User extends Account {
password?: string,
imageUrl?: string,
vcsAccessToken?: string,
vcsAccessTokenExpiryDate?: string,
sshPublicKey?: string,
irisAccepted?: dayjs.Dayjs,
) {
Expand All @@ -48,6 +50,7 @@ export class User extends Account {
this.lastNotificationRead = lastNotificationRead;
this.password = password;
this.vcsAccessToken = vcsAccessToken;
this.vcsAccessTokenExpiryDate = vcsAccessTokenExpiryDate;
this.sshPublicKey = sshPublicKey;
this.irisAccepted = irisAccepted;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@
{{ 'artemisApp.userSettings.scienceSettings' | artemisTranslate }}
</a>
@if (localVCEnabled) {
<a class="list-group-item btn btn-outline-primary" routerLink="sshSettings" routerLinkActive="active">
<!--Account Information-->
{{ 'artemisApp.userSettings.sshSettings' | artemisTranslate }}
<a class="list-group-item btn btn-outline-primary" routerLink="sshSettings" routerLinkActive="active" jhiTranslate="artemisApp.userSettings.sshSettings">
<!--SSH Settings-->
</a>
<a
class="list-group-item btn btn-outline-primary"
routerLink="vcsAccessTokensSettings"
routerLinkActive="active"
jhiTranslate="artemisApp.userSettings.vcsAccessTokenSettings"
>
<!--VCS Access Token Settings-->
</a>
}
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,18 @@ import { userSettingsState } from 'app/shared/user-settings/user-settings.route'
import { ScienceSettingsComponent } from 'app/shared/user-settings/science-settings/science-settings.component';
import { SshUserSettingsComponent } from 'app/shared/user-settings/ssh-settings/ssh-user-settings.component';
import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
import { VcsAccessTokensSettingsComponent } from 'app/shared/user-settings/vcs-access-tokens-settings/vcs-access-tokens-settings.component';
import { ClipboardModule } from '@angular/cdk/clipboard';

@NgModule({
imports: [RouterModule.forChild(userSettingsState), ArtemisSharedModule, ArtemisSharedComponentModule],
declarations: [UserSettingsContainerComponent, AccountInformationComponent, NotificationSettingsComponent, ScienceSettingsComponent, SshUserSettingsComponent],
imports: [RouterModule.forChild(userSettingsState), ArtemisSharedModule, ArtemisSharedComponentModule, ClipboardModule],
declarations: [
UserSettingsContainerComponent,
AccountInformationComponent,
NotificationSettingsComponent,
ScienceSettingsComponent,
SshUserSettingsComponent,
VcsAccessTokensSettingsComponent,
],
})
export class UserSettingsModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'
import { Authority } from 'app/shared/constants/authority.constants';
import { ScienceSettingsComponent } from 'app/shared/user-settings/science-settings/science-settings.component';
import { SshUserSettingsComponent } from 'app/shared/user-settings/ssh-settings/ssh-user-settings.component';
import { VcsAccessTokensSettingsComponent } from 'app/shared/user-settings/vcs-access-tokens-settings/vcs-access-tokens-settings.component';

export const userSettingsState: Routes = [
{
Expand Down Expand Up @@ -50,6 +51,13 @@ export const userSettingsState: Routes = [
pageTitle: 'artemisApp.userSettings.categories.SSH_SETTINGS',
},
},
{
path: 'vcsAccessTokensSettings',
component: VcsAccessTokensSettingsComponent,
data: {
pageTitle: 'artemisApp.userSettings.categories.VCS_ACCESS_TOKENS_SETTINGS',
},
},
],
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
import { User } from 'app/core/user/user.model';
import { AccountService } from 'app/core/auth/account.service';
import { Subject, Subscription, tap } from 'rxjs';
import { ProfileService } from 'app/shared/layouts/profiles/profile.service';
import { PROFILE_LOCALVC } from 'app/app.constants';
import { faCopy, faEdit, faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
import { ButtonSize, ButtonType } from 'app/shared/components/button.component';
import { AlertService } from 'app/core/util/alert.service';

@Component({
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
selector: 'jhi-account-information',
templateUrl: './vcs-access-tokens-settings.html',
styleUrls: ['../user-settings.scss'],
})
export class VcsAccessTokensSettingsComponent implements OnInit, OnDestroy {
currentUser?: User;
localVCEnabled = false;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

readonly faEdit = faEdit;
readonly faSave = faSave;
readonly faTrash = faTrash;
readonly faCopy = faCopy;
private authStateSubscription: Subscription;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
wasCopied = false;

private dialogErrorSource = new Subject<string>();

dialogError$ = this.dialogErrorSource.asObservable();

protected readonly ButtonType = ButtonType;
protected readonly ButtonSize = ButtonSize;

constructor(
private accountService: AccountService,
private profileService: ProfileService,
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
private alertService: AlertService,
) {}

ngOnInit() {
this.profileService.getProfileInfo().subscribe((profileInfo) => {
this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC);
});

this.authStateSubscription = this.accountService
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
.getAuthenticationState()
.pipe(
tap((user: User) => {
this.currentUser = user;
return this.currentUser;
}),
)
.subscribe();
}

ngOnDestroy(): void {
this.authStateSubscription.unsubscribe();
}

deleteVcsAccessToken() {
this.accountService.deleteUserVcsAccessToken().subscribe({
next: () => {
if (this.currentUser) {
this.currentUser.vcsAccessTokenExpiryDate = undefined;
this.currentUser.vcsAccessToken = undefined;
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
this.alertService.success('artemisApp.userSettings.vcsAccessTokensSettingsPage.deleteSuccess');
},
error: () => {
this.alertService.error('artemisApp.userSettings.vcsAccessTokensSettingsPage.deleteFailure');
},
});
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
this.dialogErrorSource.next('');
}

addNewVcsAccessToken() {
this.accountService.addNewVcsAccessToken().subscribe({
next: (res) => {
if (this.currentUser) {
const user = res.body as User;
this.currentUser.vcsAccessToken = user.vcsAccessToken;
this.currentUser.vcsAccessTokenExpiryDate = user.vcsAccessTokenExpiryDate;
}
this.alertService.success('artemisApp.userSettings.vcsAccessTokensSettingsPage.addSuccess');
},
error: () => {
this.alertService.error('artemisApp.userSettings.vcsAccessTokensSettingsPage.addFailure');
},
});
}
/**
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
* set wasCopied for 3 seconds on success
*/
onCopyFinished(successful: boolean) {
if (successful) {
this.wasCopied = true;
setTimeout(() => {
this.wasCopied = false;
}, 3000);
}
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<h1 jhiTranslate="artemisApp.userSettings.vcsAccessTokenSettings">
<!--VCS access token Settings-->
</h1>
@if (currentUser) {
<div class="list-group d-block">
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
<div class="list-group-item">
<dt>
<span jhiTranslate="artemisApp.userSettings.vcsAccessTokensSettingsPage.infoText"></span>
</dt>
</div>
<div class="list-group-item">
<dt>
@if (this.currentUser?.vcsAccessToken) {
<table class="table table-striped flex">
<thead class="thead-dark">
<tr>
<th class="col-2">
<span jhiTranslate="artemisApp.userSettings.vcsAccessTokensSettingsPage.vcsAccessToken"></span>
</th>
<th class="col-1"></th>
<th class="col-4">
<span jhiTranslate="artemisApp.userSettings.vcsAccessTokensSettingsPage.expiryDate"></span>
</th>
<th class="col-4">
<span jhiTranslate="artemisApp.userSettings.vcsAccessTokensSettingsPage.actions"></span>
</th>
</tr>
</thead>

<tbody>
<tr>
<td>***************</td>
<td>
<button
[cdkCopyToClipboard]="currentUser!.vcsAccessToken || ''"
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
(cdkCopyToClipboardCopied)="onCopyFinished($event)"
[class.btn-success]="wasCopied"
class="btn btn-secondary btn-sm me-2"
type="button"
>
<fa-icon [icon]="faCopy"></fa-icon>
</button>
</td>
<td>{{ this.currentUser?.vcsAccessTokenExpiryDate | artemisDate }}</td>
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
<td>
<button
class="btn btn-md flex-grow-1 d-flex align-items-center"
jhiDeleteButton
[renderButtonText]="false"
(delete)="deleteVcsAccessToken()"
deleteQuestion="artemisApp.userSettings.vcsAccessTokensSettingsPage.deleteVcsAccessTokenQuestion"
[dialogError]="dialogError$"
>
<fa-icon [icon]="faTrash" />
<div jhiTranslate="artemisApp.userSettings.vcsAccessTokensSettingsPage.deleteVcsAccessToken" class="ms-2"></div>
</button>
</td>
</tr>
</tbody>
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
</table>
} @else {
<div jhiTranslate="artemisApp.userSettings.vcsAccessTokensSettingsPage.noTokenSet"></div>
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}
</dt>
</div>
@if (!this.currentUser?.vcsAccessToken) {
<div class="list-group-item">
<dt>
<div class="btn-group" role="group" aria-label="Actions">
<jhi-button
class="d-flex"
[btnType]="ButtonType.PRIMARY"
[btnSize]="ButtonSize.SMALL"
[icon]="faEdit"
[title]="'artemisApp.userSettings.vcsAccessTokensSettingsPage.addToken'"
(onClick)="addNewVcsAccessToken()"
/>
</div>
</dt>
</div>
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}
</div>
}
Loading
Loading