|
| 1 | +import {Component, OnDestroy, OnInit} from '@angular/core'; |
| 2 | +import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap'; |
| 3 | +import {Subject} from 'rxjs'; |
| 4 | +import {debounceTime, distinctUntilChanged, takeUntil} from 'rxjs/operators'; |
| 5 | +import {UtmToastService} from '../../../shared/alert/utm-toast.service'; |
| 6 | +import { |
| 7 | + ModalConfirmationComponent |
| 8 | +} from '../../../shared/components/utm/util/modal-confirmation/modal-confirmation.component'; |
| 9 | +import {TeamUser, TeamUserPageInfo} from '../../domain/team-user.model'; |
| 10 | +import {FederationTeamService} from '../../services/federation-team.service'; |
| 11 | +import {TeamUserFormModalComponent} from '../team-user-form-modal/team-user-form-modal.component'; |
| 12 | + |
| 13 | +const DEFAULT_PAGE_SIZE = 20; |
| 14 | + |
| 15 | +@Component({ |
| 16 | + selector: 'app-federation-team-management-modal', |
| 17 | + templateUrl: './team-management-modal.component.html', |
| 18 | + styleUrls: ['./team-management-modal.component.scss'] |
| 19 | +}) |
| 20 | +export class TeamManagementModalComponent implements OnInit, OnDestroy { |
| 21 | + users: TeamUser[] = []; |
| 22 | + pageInfo: TeamUserPageInfo | null = null; |
| 23 | + loading = false; |
| 24 | + errorMessage: string | null = null; |
| 25 | + searchTerm = ''; |
| 26 | + pendingActionId: number | null = null; |
| 27 | + |
| 28 | + private currentPage = 1; |
| 29 | + private readonly pageSize = DEFAULT_PAGE_SIZE; |
| 30 | + private searchSubject = new Subject<string>(); |
| 31 | + private destroy$ = new Subject<void>(); |
| 32 | + |
| 33 | + constructor(public activeModal: NgbActiveModal, |
| 34 | + private teamService: FederationTeamService, |
| 35 | + private modalService: NgbModal, |
| 36 | + private toast: UtmToastService) {} |
| 37 | + |
| 38 | + ngOnInit(): void { |
| 39 | + this.searchSubject |
| 40 | + .pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$)) |
| 41 | + .subscribe(() => { |
| 42 | + this.currentPage = 1; |
| 43 | + this.load(); |
| 44 | + }); |
| 45 | + this.load(); |
| 46 | + } |
| 47 | + |
| 48 | + ngOnDestroy(): void { |
| 49 | + this.destroy$.next(); |
| 50 | + this.destroy$.complete(); |
| 51 | + } |
| 52 | + |
| 53 | + onSearchChange(value: string): void { |
| 54 | + this.searchTerm = value; |
| 55 | + this.searchSubject.next(value); |
| 56 | + } |
| 57 | + |
| 58 | + prevPage(): void { |
| 59 | + if (!this.pageInfo || !this.pageInfo.has_prev || this.loading) { |
| 60 | + return; |
| 61 | + } |
| 62 | + this.currentPage = this.pageInfo.page - 1; |
| 63 | + this.load(); |
| 64 | + } |
| 65 | + |
| 66 | + nextPage(): void { |
| 67 | + if (!this.pageInfo || !this.pageInfo.has_next || this.loading) { |
| 68 | + return; |
| 69 | + } |
| 70 | + this.currentPage = this.pageInfo.page + 1; |
| 71 | + this.load(); |
| 72 | + } |
| 73 | + |
| 74 | + openInvite(): void { |
| 75 | + const ref = this.modalService.open(TeamUserFormModalComponent, { |
| 76 | + centered: true, |
| 77 | + backdrop: 'static' |
| 78 | + }); |
| 79 | + ref.componentInstance.saved.subscribe(() => { |
| 80 | + ref.close(); |
| 81 | + this.toast.showSuccessBottom('Invitation sent.'); |
| 82 | + this.currentPage = 1; |
| 83 | + this.load(); |
| 84 | + }); |
| 85 | + } |
| 86 | + |
| 87 | + openEdit(user: TeamUser): void { |
| 88 | + const ref = this.modalService.open(TeamUserFormModalComponent, { |
| 89 | + centered: true, |
| 90 | + backdrop: 'static' |
| 91 | + }); |
| 92 | + ref.componentInstance.user = user; |
| 93 | + ref.componentInstance.saved.subscribe(() => { |
| 94 | + ref.close(); |
| 95 | + this.toast.showSuccessBottom('Team member updated.'); |
| 96 | + this.load(); |
| 97 | + }); |
| 98 | + } |
| 99 | + |
| 100 | + resendInvite(user: TeamUser): void { |
| 101 | + if (this.pendingActionId !== null) { |
| 102 | + return; |
| 103 | + } |
| 104 | + this.pendingActionId = user.id; |
| 105 | + this.teamService.resendInvite(user.id).subscribe({ |
| 106 | + next: () => { |
| 107 | + this.pendingActionId = null; |
| 108 | + this.toast.showSuccessBottom('Invitation re-sent.'); |
| 109 | + }, |
| 110 | + error: err => { |
| 111 | + this.pendingActionId = null; |
| 112 | + this.toast.showError('Resend invite failed', this.extractError(err)); |
| 113 | + } |
| 114 | + }); |
| 115 | + } |
| 116 | + |
| 117 | + disableTfa(user: TeamUser): void { |
| 118 | + if (this.pendingActionId !== null) { |
| 119 | + return; |
| 120 | + } |
| 121 | + const ref = this.modalService.open(ModalConfirmationComponent, { |
| 122 | + backdrop: 'static', |
| 123 | + centered: true |
| 124 | + }); |
| 125 | + ref.componentInstance.header = 'Disable 2FA'; |
| 126 | + ref.componentInstance.message = `Disable 2FA for ${this.displayName(user)}?`; |
| 127 | + ref.componentInstance.confirmBtnText = 'Disable'; |
| 128 | + ref.componentInstance.confirmBtnIcon = 'icon-shield-cross'; |
| 129 | + ref.componentInstance.confirmBtnType = 'delete'; |
| 130 | + ref.componentInstance.textDisplay = |
| 131 | + 'The user will be able to sign in without a 2FA code until they re-enroll.'; |
| 132 | + ref.componentInstance.textType = 'warning'; |
| 133 | + ref.result.then(() => this.runDisableTfa(user), () => undefined); |
| 134 | + } |
| 135 | + |
| 136 | + toggleActivation(user: TeamUser): void { |
| 137 | + if (this.pendingActionId !== null) { |
| 138 | + return; |
| 139 | + } |
| 140 | + if (user.activated) { |
| 141 | + this.confirmAndDeactivate(user); |
| 142 | + } else { |
| 143 | + this.activate(user); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + trackById(_index: number, item: TeamUser): number { |
| 148 | + return item.id; |
| 149 | + } |
| 150 | + |
| 151 | + displayName(user: TeamUser): string { |
| 152 | + const composed = [user.first_name, user.last_name].filter(part => !!part).join(' ').trim(); |
| 153 | + return composed || user.login; |
| 154 | + } |
| 155 | + |
| 156 | + private confirmAndDeactivate(user: TeamUser): void { |
| 157 | + const ref = this.modalService.open(ModalConfirmationComponent, { |
| 158 | + backdrop: 'static', |
| 159 | + centered: true |
| 160 | + }); |
| 161 | + ref.componentInstance.header = 'Deactivate team member'; |
| 162 | + ref.componentInstance.message = `Deactivate ${this.displayName(user)}?`; |
| 163 | + ref.componentInstance.confirmBtnText = 'Deactivate'; |
| 164 | + ref.componentInstance.confirmBtnIcon = 'icon-cancel-circle2'; |
| 165 | + ref.componentInstance.confirmBtnType = 'delete'; |
| 166 | + ref.componentInstance.textDisplay = 'The user will lose access to this federation server.'; |
| 167 | + ref.componentInstance.textType = 'warning'; |
| 168 | + ref.result.then(() => this.runDeactivate(user), () => undefined); |
| 169 | + } |
| 170 | + |
| 171 | + private runDeactivate(user: TeamUser): void { |
| 172 | + this.pendingActionId = user.id; |
| 173 | + this.teamService.deactivate(user.id).subscribe({ |
| 174 | + next: () => { |
| 175 | + this.pendingActionId = null; |
| 176 | + this.toast.showSuccessBottom('Team member deactivated.'); |
| 177 | + this.load(); |
| 178 | + }, |
| 179 | + error: err => { |
| 180 | + this.pendingActionId = null; |
| 181 | + this.toast.showError('Deactivation failed', this.extractError(err)); |
| 182 | + } |
| 183 | + }); |
| 184 | + } |
| 185 | + |
| 186 | + private activate(user: TeamUser): void { |
| 187 | + this.pendingActionId = user.id; |
| 188 | + this.teamService.update(user.id, { |
| 189 | + email: user.email || '', |
| 190 | + first_name: user.first_name || '', |
| 191 | + last_name: user.last_name || '', |
| 192 | + activated: true |
| 193 | + }).subscribe({ |
| 194 | + next: () => { |
| 195 | + this.pendingActionId = null; |
| 196 | + this.toast.showSuccessBottom('Team member activated.'); |
| 197 | + this.load(); |
| 198 | + }, |
| 199 | + error: err => { |
| 200 | + this.pendingActionId = null; |
| 201 | + this.toast.showError('Activation failed', this.extractError(err)); |
| 202 | + } |
| 203 | + }); |
| 204 | + } |
| 205 | + |
| 206 | + private runDisableTfa(user: TeamUser): void { |
| 207 | + this.pendingActionId = user.id; |
| 208 | + this.teamService.disableTfa(user.id).subscribe({ |
| 209 | + next: () => { |
| 210 | + this.pendingActionId = null; |
| 211 | + this.toast.showSuccessBottom('2FA disabled.'); |
| 212 | + this.load(); |
| 213 | + }, |
| 214 | + error: err => { |
| 215 | + this.pendingActionId = null; |
| 216 | + this.toast.showError('Disable 2FA failed', this.extractError(err)); |
| 217 | + } |
| 218 | + }); |
| 219 | + } |
| 220 | + |
| 221 | + private load(): void { |
| 222 | + this.loading = true; |
| 223 | + this.errorMessage = null; |
| 224 | + this.teamService.list({ |
| 225 | + page: this.currentPage, |
| 226 | + page_size: this.pageSize, |
| 227 | + search: this.searchTerm ? this.searchTerm.trim() : undefined |
| 228 | + }).subscribe({ |
| 229 | + next: response => { |
| 230 | + this.loading = false; |
| 231 | + this.users = response.data || []; |
| 232 | + this.pageInfo = response.page_info; |
| 233 | + }, |
| 234 | + error: err => { |
| 235 | + this.loading = false; |
| 236 | + this.users = []; |
| 237 | + this.pageInfo = null; |
| 238 | + this.errorMessage = this.extractError(err); |
| 239 | + } |
| 240 | + }); |
| 241 | + } |
| 242 | + |
| 243 | + private extractError(err: {error?: {message?: string}}): string { |
| 244 | + if (err && err.error && err.error.message) { |
| 245 | + return err.error.message; |
| 246 | + } |
| 247 | + return 'Operation failed. Please try again.'; |
| 248 | + } |
| 249 | +} |
0 commit comments