Feature: two-factor authentication (#8012)

This commit is contained in:
shamoon
2024-11-18 10:34:46 -08:00
committed by GitHub
parent 6c3d6d562d
commit e94a92ed59
29 changed files with 1128 additions and 175 deletions

View File

@@ -439,4 +439,25 @@ describe('PermissionsService', () => {
expect(permissionsService.isAdmin()).toBeFalsy()
})
it('correctly checks superuser status', () => {
permissionsService.initialize([], {
username: 'testuser',
last_name: 'User',
first_name: 'Test',
id: 1,
is_superuser: true,
})
expect(permissionsService.isSuperUser()).toBeTruthy()
permissionsService.initialize([], {
username: 'testuser',
last_name: 'User',
first_name: 'Test',
id: 1,
})
expect(permissionsService.isSuperUser()).toBeFalsy()
})
})

View File

@@ -56,6 +56,10 @@ export class PermissionsService {
return this.currentUser?.is_staff
}
public isSuperUser(): boolean {
return this.currentUser?.is_superuser
}
public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
return (
!object ||

View File

@@ -72,4 +72,32 @@ describe('ProfileService', () => {
)
expect(req.request.method).toEqual('GET')
})
it('calls get totp settings endpoint', () => {
service.getTotpSettings().subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}profile/totp/`
)
expect(req.request.method).toEqual('GET')
})
it('calls activate totp endpoint', () => {
service.activateTotp('secret', 'code').subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}profile/totp/`
)
expect(req.request.method).toEqual('POST')
expect(req.request.body).toEqual({
secret: 'secret',
code: 'code',
})
})
it('calls deactivate totp endpoint', () => {
service.deactivateTotp().subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}profile/totp/`
)
expect(req.request.method).toEqual('DELETE')
})
})

View File

@@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import {
TotpSettings,
PaperlessUserProfile,
SocialAccountProvider,
} from '../data/user-profile'
@@ -47,4 +48,30 @@ export class ProfileService {
`${environment.apiBaseUrl}${this.endpoint}/social_account_providers/`
)
}
getTotpSettings(): Observable<TotpSettings> {
return this.http.get<TotpSettings>(
`${environment.apiBaseUrl}${this.endpoint}/totp/`
)
}
activateTotp(
totpSecret: string,
totpCode: string
): Observable<{ success: boolean; recovery_codes: string[] }> {
return this.http.post<{ success: boolean; recovery_codes: string[] }>(
`${environment.apiBaseUrl}${this.endpoint}/totp/`,
{
secret: totpSecret,
code: totpCode,
}
)
}
deactivateTotp(): Observable<boolean> {
return this.http.delete<boolean>(
`${environment.apiBaseUrl}${this.endpoint}/totp/`,
{}
)
}
}

View File

@@ -160,6 +160,18 @@ const user = {
commonAbstractNameFilterPaperlessServiceTests(endpoint, UserService)
describe('Additional service tests for UserService', () => {
beforeEach(() => {
// Dont need to setup again
httpTestingController = TestBed.inject(HttpTestingController)
service = TestBed.inject(UserService)
})
afterEach(() => {
subscription?.unsubscribe()
httpTestingController.verify()
})
it('should retain permissions on update', () => {
subscription = service.listAll().subscribe()
let req = httpTestingController.expectOne(
@@ -179,15 +191,11 @@ describe('Additional service tests for UserService', () => {
)
})
beforeEach(() => {
// Dont need to setup again
httpTestingController = TestBed.inject(HttpTestingController)
service = TestBed.inject(UserService)
})
afterEach(() => {
subscription?.unsubscribe()
httpTestingController.verify()
it('should deactivate totp', () => {
subscription = service.deactivateTotp(user).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${user.id}/deactivate_totp/`
)
expect(req.request.method).toEqual('POST')
})
})

View File

@@ -5,6 +5,7 @@ import { User } from 'src/app/data/user'
import { PermissionsService } from '../permissions.service'
import { AbstractNameFilterService } from './abstract-name-filter-service'
const endpoint = 'users'
@Injectable({
providedIn: 'root',
})
@@ -13,7 +14,7 @@ export class UserService extends AbstractNameFilterService<User> {
http: HttpClient,
private permissionService: PermissionsService
) {
super(http, 'users')
super(http, endpoint)
}
update(o: User): Observable<User> {
@@ -31,4 +32,11 @@ export class UserService extends AbstractNameFilterService<User> {
})
)
}
deactivateTotp(u: User): Observable<boolean> {
return this.http.post<boolean>(
`${this.getResourceUrl(u.id, 'deactivate_totp')}`,
null
)
}
}