Include auth token and generate auth token

This commit is contained in:
shamoon
2023-11-23 00:15:18 -08:00
parent f948113c0f
commit f90bd4722c
12 changed files with 185 additions and 31 deletions

View File

@@ -27,6 +27,27 @@
</div>
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
<div class="mb-3">
<label class="form-label" i18n>API Auth Token</label>
<div class="position-relative">
<div class="input-group">
<input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
<svg class="buttonicon-sm" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
</svg><span class="visually-hidden" i18n>Copy</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
</svg>
</button>
</div>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
</div>
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@@ -3,3 +3,7 @@
margin: 0 !important; // hack-ish, for animation
}
}
.copied-badge {
right: 8em;
}

View File

@@ -1,4 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
import { ProfileService } from 'src/app/services/profile.service'
@@ -6,9 +11,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import {
NgbAccordionModule,
NgbActiveModal,
NgbModal,
NgbModalModule,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap'
import { HttpClientModule } from '@angular/common/http'
import { TextComponent } from '../input/text/text.component'
@@ -16,12 +19,14 @@ import { PasswordComponent } from '../input/password/password.component'
import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { By } from '@angular/platform-browser'
import { Clipboard } from '@angular/cdk/clipboard'
const profile = {
email: 'foo@bar.com',
password: '*********',
first_name: 'foo',
last_name: 'bar',
auth_token: '123456789abcdef',
}
describe('ProfileEditDialogComponent', () => {
@@ -29,6 +34,7 @@ describe('ProfileEditDialogComponent', () => {
let fixture: ComponentFixture<ProfileEditDialogComponent>
let profileService: ProfileService
let toastService: ToastService
let clipboard: Clipboard
beforeEach(() => {
TestBed.configureTestingModule({
@@ -48,6 +54,7 @@ describe('ProfileEditDialogComponent', () => {
})
profileService = TestBed.inject(ProfileService)
toastService = TestBed.inject(ToastService)
clipboard = TestBed.inject(Clipboard)
fixture = TestBed.createComponent(ProfileEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
@@ -68,6 +75,7 @@ describe('ProfileEditDialogComponent', () => {
password: profile.password,
first_name: 'foo2',
last_name: profile.last_name,
auth_token: profile.auth_token,
}
const updateSpy = jest.spyOn(profileService, 'update')
const errorSpy = jest.spyOn(toastService, 'showError')
@@ -151,4 +159,39 @@ describe('ProfileEditDialogComponent', () => {
)
expect(component.saveDisabled).toBeFalsy()
})
it('should support auth token copy', fakeAsync(() => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
const copySpy = jest.spyOn(clipboard, 'copy')
component.copyAuthToken()
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
expect(component.copied).toBeTruthy()
tick(3000)
expect(component.copied).toBeFalsy()
}))
it('should support generate token, display error if needed', () => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
const generateSpy = jest.spyOn(profileService, 'generateAuthToken')
const errorSpy = jest.spyOn(toastService, 'showError')
generateSpy.mockReturnValueOnce(
throwError(() => new Error('failed to generate'))
)
component.generateAuthToken()
expect(errorSpy).toHaveBeenCalled()
generateSpy.mockClear()
const newToken = '789101112hijk'
generateSpy.mockReturnValueOnce(of(newToken))
component.generateAuthToken()
expect(generateSpy).toHaveBeenCalled()
expect(component.form.get('auth_token').value).not.toEqual(
profile.auth_token
)
expect(component.form.get('auth_token').value).toEqual(newToken)
})
})

View File

@@ -4,6 +4,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import { Subject, takeUntil } from 'rxjs'
import { Clipboard } from '@angular/cdk/clipboard'
@Component({
selector: 'pngx-profile-edit-dialog',
@@ -22,6 +23,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
password_confirm: new FormControl({ value: null, disabled: true }),
first_name: new FormControl(''),
last_name: new FormControl(''),
auth_token: new FormControl(''),
})
private currentPassword: string
@@ -34,10 +36,13 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
private emailConfirm: string
public showEmailConfirm: boolean = false
public copied: boolean = false
constructor(
private profileService: ProfileService,
public activeModal: NgbActiveModal,
private toastService: ToastService
private toastService: ToastService,
private clipboard: Clipboard
) {}
ngOnInit(): void {
@@ -70,17 +75,17 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
return this.error?.password_confirm || this.error?.email_confirm
}
onEmailKeyUp(event: KeyboardEvent) {
onEmailKeyUp(event: KeyboardEvent): void {
this.newEmail = (event.target as HTMLInputElement)?.value
this.onEmailChange()
}
onEmailConfirmKeyUp(event: KeyboardEvent) {
onEmailConfirmKeyUp(event: KeyboardEvent): void {
this.emailConfirm = (event.target as HTMLInputElement)?.value
this.onEmailChange()
}
onEmailChange() {
onEmailChange(): void {
this.showEmailConfirm = this.currentEmail !== this.newEmail
if (this.showEmailConfirm) {
this.form.get('email_confirm').enable()
@@ -96,19 +101,18 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
}
}
onPasswordKeyUp(event: KeyboardEvent) {
onPasswordKeyUp(event: KeyboardEvent): void {
this.newPassword = (event.target as HTMLInputElement)?.value
this.onPasswordChange()
}
onPasswordConfirmKeyUp(event: KeyboardEvent) {
onPasswordConfirmKeyUp(event: KeyboardEvent): void {
this.passwordConfirm = (event.target as HTMLInputElement)?.value
this.onPasswordChange()
}
onPasswordChange() {
onPasswordChange(): void {
this.showPasswordConfirm = this.currentPassword !== this.newPassword
console.log(this.currentPassword, this.newPassword, this.passwordConfirm)
if (this.showPasswordConfirm) {
this.form.get('password_confirm').enable()
@@ -124,7 +128,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
}
}
save() {
save(): void {
const profile = Object.assign({}, this.form.value)
this.networkActive = true
this.profileService
@@ -142,7 +146,30 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
})
}
cancel() {
cancel(): void {
this.activeModal.close()
}
generateAuthToken(): void {
this.profileService.generateAuthToken().subscribe({
next: (token: string) => {
console.log(token)
this.form.patchValue({ auth_token: token })
},
error: (error) => {
this.toastService.showError(
$localize`Error generating auth token`,
error
)
},
})
}
copyAuthToken(): void {
this.clipboard.copy(this.form.get('auth_token').value)
this.copied = true
setTimeout(() => {
this.copied = false
}, 3000)
}
}

View File

@@ -3,4 +3,5 @@ export interface PaperlessUserProfile {
password?: string
first_name?: string
last_name?: string
auth_token?: string
}

View File

@@ -43,4 +43,12 @@ describe('ProfileService', () => {
email: 'foo@bar.com',
})
})
it('supports generating new auth token', () => {
service.generateAuthToken().subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}profile/generate_auth_token/`
)
expect(req.request.method).toEqual('POST')
})
})

View File

@@ -24,4 +24,11 @@ export class ProfileService {
profile
)
}
generateAuthToken(): Observable<string> {
return this.http.post<string>(
`${environment.apiBaseUrl}${this.endpoint}/generate_auth_token/`,
{}
)
}
}