Initial work on views

This commit is contained in:
shamoon 2024-10-18 19:50:43 -07:00
parent ea37ae0ce4
commit da75d4e0b3
10 changed files with 392 additions and 81 deletions

View File

@ -343,6 +343,7 @@ describe('AppFrameComponent', () => {
component.editProfile() component.editProfile()
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, { expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'xl',
}) })
}) })

View File

@ -136,6 +136,7 @@ export class AppFrameComponent
editProfile() { editProfile() {
this.modalService.open(ProfileEditDialogComponent, { this.modalService.open(ProfileEditDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'xl',
}) })
this.closeMenu() this.closeMenu()
} }

View File

@ -5,94 +5,179 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text> <div class="row">
<div ngbAccordion> <div class="col-12 col-md-6">
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent"> <pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
<div ngbAccordionCollapse> <div ngbAccordion>
<div ngbAccordionBody class="p-0 pb-3"> <div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text> <div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
</div>
</div>
</div> </div>
</div> </div>
</div> <pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password>
</div> <div ngbAccordion>
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password> <div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
<div ngbAccordion> <div ngbAccordionCollapse>
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent"> <div ngbAccordionBody class="p-0 pb-3">
<div ngbAccordionCollapse> <pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password>
<div ngbAccordionBody class="p-0 pb-3"> </div>
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password> </div>
</div> </div>
</div> </div>
</div> <pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
</div> <pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
<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">
@if (!copied) {
<i-bs width="1em" height="1em" name="clipboard-fill"></i-bs>
}
@if (copied) {
<i-bs width="1em" height="1em" name="clipboard-check-fill"></i-bs>
}
<span class="visually-hidden" i18n>Copy</span>
</button>
<pngx-confirm-button
title="Regenerate auth token"
i18n-title
buttonClasses=" btn-outline-secondary"
iconName="arrow-repeat"
[disabled]="!hasUsablePassword"
(confirm)="generateAuthToken()">
</pngx-confirm-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>
@if (socialAccounts?.length > 0) {
<div class="mb-3"> <div class="mb-3">
<p i18n>Connected social accounts</p> <label class="form-label" i18n>API Auth Token</label>
<ul class="list-group"> <div class="position-relative">
@for (account of socialAccounts; track account.id) { <div class="input-group">
<li class="list-group-item" <input type="text" class="form-control" formControlName="auth_token" readonly>
ngbPopover="Set a password before disconnecting social account." <button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
i18n-ngbPopover @if (!copied) {
[disablePopover]="hasUsablePassword" <i-bs width="1em" height="1em" name="clipboard-fill"></i-bs>
triggers="mouseenter:mouseleave"> }
{{account.name}} ({{account.provider}}) @if (copied) {
<i-bs width="1em" height="1em" name="clipboard-check-fill"></i-bs>
}
<span class="visually-hidden" i18n>Copy</span>
</button>
<pngx-confirm-button <pngx-confirm-button
label="Disconnect" title="Regenerate auth token"
i18n-label
title="Disconnect {{ account.name }} social account"
i18n-title i18n-title
buttonClasses="btn-outline-danger btn-sm ms-2 align-baseline" buttonClasses=" btn-outline-secondary"
iconName="trash" iconName="arrow-repeat"
[disabled]="!hasUsablePassword" [disabled]="!hasUsablePassword"
(confirm)="disconnectSocialAccount(account.id)"> (confirm)="generateAuthToken()">
</pngx-confirm-button> </pngx-confirm-button>
</li> </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>
</ul> </div>
<div class="form-text text-muted text-end fst-italic" i18n>Warning: disconnecting social accounts cannot be undone</div> <div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
</div>
}
@if (socialAccountProviders?.length > 0) {
<div class="mb-3">
<p i18n>Connect new social account</p>
<div class="list-group">
@for (provider of socialAccountProviders; track provider.name) {
<a class="list-group-item list-group-item-action text-primary d-flex align-items-center" href="{{ provider.login_url }}" rel="noopener noreferrer">
{{provider.name}}&nbsp;<i-bs class="pb-1 ps-1" name="box-arrow-up-right"></i-bs>
</a>
}
</div> </div>
</div> </div>
} <div class="col-12 col-md-6">
@if (socialAccounts?.length > 0) {
<div class="mb-3">
<p i18n>Connected social accounts</p>
<ul class="list-group">
@for (account of socialAccounts; track account.id) {
<li class="list-group-item"
ngbPopover="Set a password before disconnecting social account."
i18n-ngbPopover
[disablePopover]="hasUsablePassword"
triggers="mouseenter:mouseleave">
{{account.name}} ({{account.provider}})
<pngx-confirm-button
label="Disconnect"
i18n-label
title="Disconnect {{ account.name }} social account"
i18n-title
buttonClasses="btn-outline-danger btn-sm ms-2 align-baseline"
iconName="trash"
[disabled]="!hasUsablePassword"
(confirm)="disconnectSocialAccount(account.id)">
</pngx-confirm-button>
</li>
}
</ul>
<div class="form-text text-muted text-end fst-italic" i18n>Warning: disconnecting social accounts cannot be undone</div>
</div>
}
@if (socialAccountProviders?.length > 0) {
<div class="mb-3">
<p i18n>Connect new social account</p>
<div class="list-group">
@for (provider of socialAccountProviders; track provider.name) {
<a class="list-group-item list-group-item-action text-primary d-flex align-items-center" href="{{ provider.login_url }}" rel="noopener noreferrer">
{{provider.name}}&nbsp;<i-bs class="pb-1 ps-1" name="box-arrow-up-right"></i-bs>
</a>
}
</div>
</div>
}
@if (!isTotpEnabled) {
<div ngbAccordion>
<div ngbAccordionItem>
<h2 ngbAccordionHeader>
<button ngbAccordionButton (click)="gettotpSettings()" i18n>Two-factor Authentication</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
@if (totpSettingsLoading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
} @else if (totpSettings) {
<figure class="figure">
<div class="bg-white d-inline-block" [innerHTML]="totpSettings.qr_svg | safeHtml"></div>
<figcaption class="figure-caption text-end mt-2" i18n>Scan the QR code with your authenticator app and then enter the code below</figcaption>
</figure>
<p>
<ng-container i18n>Authenticator secret</ng-container>: <code>{{totpSettings.secret}}</code>.
<ng-container i18n>You can store this secret and use it to reinstall your authenticator app at a later time.</ng-container>
</p>
<div class="input-group mb-3">
<input type="text" class="form-control" formControlName="totp_code" placeholder="Code" i18n-placeholder>
<button type="button" class="btn btn-primary ml-auto" (click)="activateTotp()" [disabled]="totpLoading">
<ng-container i18n>Enable</ng-container>
@if (totpLoading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
}
</button>
</div>
}
</ng-template>
</div>
</div>
</div>
</div>
} @else {
<label class="d-block mb-2" i18n>Two-factor Authentication</label>
@if (recoveryCodes) {
<div class="alert alert-warning" role="alert">
<i-bs name="exclamation-triangle"></i-bs>&nbsp;<ng-container i18n>Recovery codes will not be shown again, make sure to save them.</ng-container>
</div>
<div class="d-flex flex-row align-items-start mb-3">
<ul class="list-group w-50">
@for (code of recoveryCodes; track code; let i = $index) {
@if (i % 2 === 0) {
<li class="list-group-item d-flex justify-content-around align-items-center">
<code>{{code}}</code>
@if (recoveryCodes[i + 1]) {
<code>{{recoveryCodes[i + 1]}}</code>
}
</li>
}
}
</ul>
<button type="button" class="btn btn-sm btn-outline-secondary ms-2" (click)="copyRecoveryCodes()" i18n-title title="Copy">
@if (!codesCopied) {
<i-bs width="1em" height="1em" name="clipboard-fill"></i-bs>
&nbsp;<span i18n>Copy codes</span>
}
@if (codesCopied) {
<i-bs width="1em" height="1em" name="clipboard-check-fill" class="text-primary"></i-bs>
&nbsp;<span class="text-primary" i18n>Copied!</span>
}
</button>
</div>
}
<pngx-confirm-button
label="Disable Two-factor Authentication"
i18n-label
title="Disable Two-factor Authentication"
i18n-title
buttonClasses="btn-outline-danger btn-sm"
iconName="trash"
[disabled]="totpLoading"
(confirm)="deactivateTotp()">
</pngx-confirm-button>
}
</div>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@ -2,7 +2,11 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms' import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ProfileService } from 'src/app/services/profile.service' import { ProfileService } from 'src/app/services/profile.service'
import { SocialAccount, SocialAccountProvider } from 'src/app/data/user-profile' import {
TotpSettings,
SocialAccount,
SocialAccountProvider,
} from 'src/app/data/user-profile'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { Subject, takeUntil } from 'rxjs' import { Subject, takeUntil } from 'rxjs'
import { Clipboard } from '@angular/cdk/clipboard' import { Clipboard } from '@angular/cdk/clipboard'
@ -25,6 +29,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
first_name: new FormControl(''), first_name: new FormControl(''),
last_name: new FormControl(''), last_name: new FormControl(''),
auth_token: new FormControl(''), auth_token: new FormControl(''),
totp_code: new FormControl(''),
}) })
private currentPassword: string private currentPassword: string
@ -38,7 +43,14 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
private emailConfirm: string private emailConfirm: string
public showEmailConfirm: boolean = false public showEmailConfirm: boolean = false
public isTotpEnabled: boolean = false
public totpSettings: TotpSettings
public totpSettingsLoading: boolean = false
public totpLoading: boolean = false
public recoveryCodes: string[]
public copied: boolean = false public copied: boolean = false
public codesCopied: boolean = false
public socialAccounts: SocialAccount[] = [] public socialAccounts: SocialAccount[] = []
public socialAccountProviders: SocialAccountProvider[] = [] public socialAccountProviders: SocialAccountProvider[] = []
@ -70,6 +82,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
this.onPasswordChange() this.onPasswordChange()
}) })
this.socialAccounts = profile.social_accounts this.socialAccounts = profile.social_accounts
this.isTotpEnabled = profile.is_mfa_enabled
}) })
this.profileService this.profileService
@ -147,6 +160,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
const passwordChanged = const passwordChanged =
this.newPassword && this.currentPassword !== this.newPassword this.newPassword && this.currentPassword !== this.newPassword
const profile = Object.assign({}, this.form.value) const profile = Object.assign({}, this.form.value)
delete profile.totp_code
this.networkActive = true this.networkActive = true
this.profileService this.profileService
.update(profile) .update(profile)
@ -213,4 +227,82 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
}, },
}) })
} }
public gettotpSettings(): void {
this.totpSettingsLoading = true
this.profileService
.getTotpSettings()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (totpSettings) => {
this.totpSettingsLoading = false
this.totpSettings = totpSettings
},
error: (error) => {
this.toastService.showError(
$localize`Error fetching TOTP settings`,
error
)
this.totpSettingsLoading = false
},
})
}
public activateTotp(): void {
this.totpLoading = true
this.form.get('totp_code').disable()
this.profileService
.activateTotp(this.totpSettings.secret, this.form.get('totp_code').value)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (activationResponse) => {
console.log(activationResponse)
this.totpLoading = false
this.isTotpEnabled = activationResponse.success
this.recoveryCodes = activationResponse.recovery_codes
if (activationResponse.success) {
this.toastService.showInfo($localize`TOTP activated successfully`)
} else {
this.toastService.showError($localize`Error activating TOTP`)
}
},
error: (error) => {
this.totpLoading = false
this.form.get('totp_code').enable()
this.toastService.showError($localize`Error activating TOTP`, error)
},
})
}
public deactivateTotp(): void {
this.totpLoading = true
this.profileService
.deactivateTotp()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (success) => {
this.totpLoading = false
this.isTotpEnabled = !success
this.recoveryCodes = null
if (success) {
this.toastService.showInfo($localize`TOTP deactivated successfully`)
} else {
this.toastService.showError($localize`Error deactivating TOTP`)
}
},
error: (error) => {
this.totpLoading = false
this.toastService.showError($localize`Error deactivating TOTP`, error)
},
})
}
public copyRecoveryCodes(): void {
this.clipboard.copy(this.recoveryCodes.join('\n'))
this.codesCopied = true
setTimeout(() => {
this.codesCopied = false
}, 3000)
}
} }

View File

@ -17,4 +17,11 @@ export interface PaperlessUserProfile {
auth_token?: string auth_token?: string
social_accounts?: SocialAccount[] social_accounts?: SocialAccount[]
has_usable_password?: boolean has_usable_password?: boolean
is_mfa_enabled?: boolean
}
export interface TotpSettings {
url: string
qr_svg: string
secret: string
} }

View File

@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { import {
TotpSettings,
PaperlessUserProfile, PaperlessUserProfile,
SocialAccountProvider, SocialAccountProvider,
} from '../data/user-profile' } from '../data/user-profile'
@ -47,4 +48,30 @@ export class ProfileService {
`${environment.apiBaseUrl}${this.endpoint}/social_account_providers/` `${environment.apiBaseUrl}${this.endpoint}/social_account_providers/`
) )
} }
getTotpSettings(): Observable<TotpSettings> {
return this.http.get<TotpSettings>(
`${environment.apiBaseUrl}${this.endpoint}/totp_activate/`
)
}
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_activate/`,
{
secret: totpSecret,
code: totpCode,
}
)
}
deactivateTotp(): Observable<boolean> {
return this.http.delete<boolean>(
`${environment.apiBaseUrl}${this.endpoint}/totp_activate/`,
{}
)
}
} }

View File

@ -1,5 +1,6 @@
import logging import logging
from allauth.mfa.adapter import get_adapter as get_mfa_adapter
from allauth.socialaccount.models import SocialAccount from allauth.socialaccount.models import SocialAccount
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
@ -130,6 +131,11 @@ class ProfileSerializer(serializers.ModelSerializer):
read_only=True, read_only=True,
source="socialaccount_set", source="socialaccount_set",
) )
is_mfa_enabled = serializers.SerializerMethodField()
def get_is_mfa_enabled(self, user: User):
mfa_adapter = get_mfa_adapter()
return mfa_adapter.is_mfa_enabled(user)
class Meta: class Meta:
model = User model = User
@ -141,6 +147,7 @@ class ProfileSerializer(serializers.ModelSerializer):
"auth_token", "auth_token",
"social_accounts", "social_accounts",
"has_usable_password", "has_usable_password",
"is_mfa_enabled",
) )

View File

@ -459,6 +459,8 @@ SOCIALACCOUNT_PROVIDERS = json.loads(
os.getenv("PAPERLESS_SOCIALACCOUNT_PROVIDERS", "{}"), os.getenv("PAPERLESS_SOCIALACCOUNT_PROVIDERS", "{}"),
) )
MFA_TOTP_ISSUER = "Paperless-ngx"
ACCOUNT_EMAIL_SUBJECT_PREFIX = "[Paperless-ngx] " ACCOUNT_EMAIL_SUBJECT_PREFIX = "[Paperless-ngx] "
DISABLE_REGULAR_LOGIN = __get_boolean("PAPERLESS_DISABLE_REGULAR_LOGIN") DISABLE_REGULAR_LOGIN = __get_boolean("PAPERLESS_DISABLE_REGULAR_LOGIN")

View File

@ -54,6 +54,7 @@ from paperless.views import GenerateAuthTokenView
from paperless.views import GroupViewSet from paperless.views import GroupViewSet
from paperless.views import ProfileView from paperless.views import ProfileView
from paperless.views import SocialAccountProvidersView from paperless.views import SocialAccountProvidersView
from paperless.views import TOTPActivateView
from paperless.views import UserViewSet from paperless.views import UserViewSet
from paperless_mail.views import MailAccountTestView from paperless_mail.views import MailAccountTestView
from paperless_mail.views import MailAccountViewSet from paperless_mail.views import MailAccountViewSet
@ -157,8 +158,21 @@ urlpatterns = [
), ),
re_path( re_path(
"^profile/", "^profile/",
ProfileView.as_view(), include(
name="profile_view", [
re_path(
"^$",
ProfileView.as_view(),
name="profile_view",
),
path(
"totp_activate/",
TOTPActivateView.as_view(),
name="activate",
),
# TODO: remove allauth urls?
],
),
), ),
re_path( re_path(
"^status/", "^status/",

View File

@ -1,6 +1,12 @@
import os import os
from collections import OrderedDict from collections import OrderedDict
from allauth.mfa import signals
from allauth.mfa.adapter import get_adapter as get_mfa_adapter
from allauth.mfa.base.internal.flows import delete_and_cleanup
from allauth.mfa.models import Authenticator
from allauth.mfa.recovery_codes.internal.flows import auto_generate_recovery_codes
from allauth.mfa.totp.internal import auth as totp_auth
from allauth.socialaccount.adapter import get_adapter from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.models import SocialAccount from allauth.socialaccount.models import SocialAccount
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
@ -145,6 +151,75 @@ class ProfileView(GenericAPIView):
return Response(serializer.to_representation(user)) return Response(serializer.to_representation(user))
class TOTPActivateView(GenericAPIView):
"""
TOTP views
"""
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
user = self.request.user
mfa_adapter = get_mfa_adapter()
secret = totp_auth.get_totp_secret(regenerate=True)
url = mfa_adapter.build_totp_url(user, secret)
svg = mfa_adapter.build_totp_svg(url)
return Response(
{
"url": url,
"qr_svg": svg,
"secret": secret,
},
)
def post(self, request, *args, **kwargs):
valid = totp_auth.validate_totp_code(
request.data["secret"],
request.data["code"],
)
recovery_codes = None
if valid:
# from allauth.mfa.totp.internal.flows activate_totp
auth = totp_auth.TOTP.activate(
request.user,
request.data["secret"],
).instance
signals.authenticator_added.send(
sender=Authenticator,
request=request,
user=request.user,
authenticator=auth,
)
# adapter = get_adapter()
# adapter.add_message(request, messages.SUCCESS, "mfa/messages/totp_activated.txt")
# adapter.send_notification_mail("mfa/email/totp_activated", request.user)
rc_auth: Authenticator = auto_generate_recovery_codes(request)
if rc_auth:
recovery_codes = rc_auth.wrap().get_unused_codes()
return Response(
{
"success": valid,
"recovery_codes": recovery_codes,
},
)
def delete(self, request, *args, **kwargs):
user = self.request.user
try:
# from allauth.mfa.totp.internal.flows deactivate_totp
authenticator = Authenticator.objects.filter(
user=user,
type=Authenticator.Type.TOTP,
).first()
delete_and_cleanup(request, authenticator)
# adapter = get_account_adapter(request)
# adapter.add_message(request, messages.SUCCESS, "mfa/messages/totp_deactivated.txt")
# adapter.send_notification_mail("mfa/email/totp_deactivated", request.user)
return Response(True)
except Authenticator.DoesNotExist:
return HttpResponseBadRequest("TOTP not found")
class GenerateAuthTokenView(GenericAPIView): class GenerateAuthTokenView(GenericAPIView):
""" """
Generates (or re-generates) an auth token, requires a logged in user Generates (or re-generates) an auth token, requires a logged in user