MT#62982 Add 2FA for Subscribers

Subscriber can now use 2FA with API v1.
2FA can be enabled from the admin portal:
- at domain level from domain preferences.
- at subscriber level from subscriber preferences.

Flow when 2fa is enabled:
- subscriber attempts login with username and PW
- scenario1 --> 1st LOGIN:
   * request returns 403 Invalid OTP
   * UI shows QR code (with the possibility to
     visualize it as a string), instructions on
     how to use 2FA and a input for OTP token
   * once relevant info are submitted the user is logged in
- scenario2 --> NOT 1st LOGIN:
   * request returns 400 no OTP
   * UI shows only input for OTP token
   * once relevant info are submitted the user is logged in

Change-Id: Ief1ed703d3d1cfa896fe62a2c7a55897260d2cfe
mr13.4
Debora Crescenzo 4 months ago
parent d8c1f097cc
commit 7651502818

@ -9,13 +9,14 @@ import {
} from 'src/api/common'
import { getFaxServerSettings } from 'src/api/fax'
export function login (username, password) {
export function login ({ username, password, otp = null }) {
return new Promise((resolve, reject) => {
let jwt = null
let subscriberId = null
httpApi.post('login_jwt', {
username,
password
password,
...(otp && { otp })
}).then((result) => {
jwt = result.data.jwt
subscriberId = `${result.data.subscriber_id}`

@ -0,0 +1,4 @@
export async function parseBlobToObject (blob) {
const text = await blob.text()
return JSON.parse(text)
}

@ -61,8 +61,8 @@
"Block List for outbounds calls": "Blockliste für ausgehende Anrufe",
"Block Mode for inbound calls": "Blockmodus für eingehende Anrufe",
"Block Mode for outbounds calls": "Blockmodus für ausgehende Anrufe",
"Block anonymous inbound calls": "Anonyme eingehende Anrufe blockieren",
"Block Outgoing": "Ausgehende Anrufe blockieren",
"Block anonymous inbound calls": "Anonyme eingehende Anrufe blockieren",
"Busy Greeting": "Begrüßung, wenn besetzt",
"Busy Lamp Field": "Besetztlampenfeld",
"CDR": "CDR",
@ -172,7 +172,10 @@
"Disable": "Deaktivieren",
"Disable phone web interface": "Webinterface des Telefons deaktivieren",
"Display Name": "Anzeigename",
"Display as QR code": "Als QR-Code anzeigen",
"Display as text": "Als Text anzeigen",
"Do not ring primary number": "Die Hauptnummer nicht klingeln lassen",
"Download App": "App herunterladen",
"Download CSV": "CSV herunterladen",
"Download fax": "Fax herunterladen",
"Download voicemail": "Voicemail herunterladen",
@ -188,6 +191,7 @@
"Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.": "Aktivieren des strengen Modus, der verlangt, dass alle mail2fax-E-Mails in der ersten Zeile den Geheimschlüssel gefolgt von einer Leerzeile enthalten. Der Schlüssel wird nach dem Abgleich aus der E-Mail entfernt.",
"End time": "Endzeit",
"English": "Englisch",
"Enter OTP": "OTP-Code eingeben",
"Enter a number to dial": "Geben Sie eine Telefonnummer zum Anwählen an",
"Entity belongs to admin": "Diese Einheit gehört dem Admin",
"Expires": "Läuft aus",
@ -266,7 +270,9 @@
"Input a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein",
"Input is required": "Eingabe erforderlich",
"Input must be a valid number": "Die Eingabe muss eine gültige Zahl sein",
"Install Google Authenticator app on your mobile device or use your preferred Authenticator app.": "Installieren Sie die Google Authenticator-App auf Ihrem mobilen Gerät oder verwenden Sie Ihre bevorzugte Authenticator-App.",
"Interval when the secret key is automatically renewed.": "Intervall, nach dem der geheime Schlüssel automatisch erneuert wird.",
"Invalid OTP Code": "Ungültiger OTP-Code",
"Italian": "Italiano",
"January": "Januar",
"Join conference": "Konferenz beitreten",
@ -354,6 +360,8 @@
"Number list": "Nummernliste",
"Number list name": "Name der Nummernliste",
"Numbers": "Nummern",
"OTP Code": "OTP-Code",
"OTP Verification": "OTP-Verifizierung",
"October": "Oktober",
"Office Hours Announcement": "Ansage zu den Bürozeiten",
"On weekdays": "An Werktagen",
@ -361,6 +369,7 @@
"Only none decimal numbers are allowed": "Nur Ganzzahlen erlaubt",
"Only once": "Nur einmal",
"Only outgoing calls to listed numbers are allowed": "Nur ausgehende Anrufe an aufgelistete Nummern erlaubt",
"Open the Authenticator app to register your NGCP account.": "Öffnen Sie die Authenticator-App, um Ihr NGCP-Konto zu registrieren.",
"Out": "Aus",
"Outgoing": "Ausgehend",
"PBX Configuration": "PBX-Konfiguration",
@ -466,6 +475,7 @@
"Saturday": "Samstag",
"Save": "Speichern",
"Save new password": "Neues Passwort speichern",
"Scan QR code": "QR-Code scannen",
"Scan to login sip:phone": "Zur Anmeldung in sip:phone scannen",
"Seat": "Nebenstelle",
"Seat Display Name": "Nebenstelle Anzeigename",
@ -554,6 +564,7 @@
"Unblock Incoming/Outgoing": "Ein-/Ausgehende Anrufe entsperren",
"Unblock Outgoing": "Ausgehende Anrufe entsperren",
"Undo": "Rückgängig machen",
"Unexpected error": "Unerwarteter Fehler",
"Unknown error": "Unbekannter Fehler",
"Unknown name": "Unbekannter Name",
"Unmute": "Stummschaltung aufheben",
@ -570,6 +581,7 @@
"Use as default for all seats and groups": "Standard für alle Nebenstellen und Gruppen",
"Use custom number": "Benutzerdefinierte Nummer verwenden",
"Use language specific preset": "Sprachspezifische Voreinstellungen verwenden",
"Use the Authenticator app to generate the verification code.": "Verwenden Sie die Authenticator-App, um den Bestätigungscode zu generieren.",
"User Agent": "User Agent",
"User config priority over provisioning": "Benutzerkonfiguration hat Vorrang vor Provisionierung",
"User settings": "Benutzereinstellungen",

@ -60,8 +60,8 @@
"Block List for outbounds calls": "Block List for outbounds calls",
"Block Mode for inbound calls": "Block Mode for inbound calls",
"Block Mode for outbounds calls": "Block Mode for outbounds calls",
"Block anonymous inbound calls": "Block anonymous inbound calls",
"Block Outgoing": "Block Outgoing",
"Block anonymous inbound calls": "Block anonymous inbound calls",
"Busy Greeting": "Busy Greeting",
"Busy Lamp Field": "Busy Lamp Field",
"CDR": "CDR",
@ -169,7 +169,10 @@
"Disable": "Disable",
"Disable phone web interface": "Disable phone web interface",
"Display Name": "Display Name",
"Display as QR code": "Display as QR code",
"Display as text": "Display as text",
"Do not ring primary number": "Do not ring primary number",
"Download App": "Download App",
"Download CSV": "Download CSV",
"Download fax": "Download fax",
"Download voicemail": "Download voicemail",
@ -185,6 +188,7 @@
"Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.": "Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.",
"End time": "End time",
"English": "English",
"Enter OTP": "Enter OTP",
"Enter a number to dial": "Enter a number to dial",
"Entity belongs to admin": "Entity belongs to admin",
"Expires": "Expires",
@ -262,7 +266,9 @@
"Input a valid phone number": "Input a valid phone number",
"Input is required": "Input is required",
"Input must be a valid number": "Input must be a valid number",
"Install Google Authenticator app on your mobile device or use your preferred Authenticator app.": "Install Google Authenticator app on your mobile device or use your preferred Authenticator app.",
"Interval when the secret key is automatically renewed.": "Interval when the secret key is automatically renewed.",
"Invalid OTP Code": "Invalid OTP Code",
"Italian": "Italiano",
"January": "January",
"July": "July",
@ -340,6 +346,8 @@
"Number list": "Number list",
"Number list name": "Number list name",
"Numbers": "Numbers",
"OTP Code": "OTP Code",
"OTP Verification": "OTP Verification",
"October": "October",
"Office Hours Announcement": "Office Hours Announcement",
"On weekdays": "On weekdays",
@ -347,6 +355,7 @@
"Only none decimal numbers are allowed": "Only none decimal numbers are allowed",
"Only once": "Only once",
"Only outgoing calls to listed numbers are allowed": "Only outgoing calls to listed numbers are allowed",
"Open the Authenticator app to register your NGCP account.": "Open the Authenticator app to register your NGCP account.",
"Out": "Out",
"Outgoing": "Outgoing",
"PBX Configuration": "PBX Configuration",
@ -451,6 +460,7 @@
"Saturday": "Saturday",
"Save": "Save",
"Save new password": "Save new password",
"Scan QR code": "Scan QR code",
"Scan to login sip:phone": "Scan to login sip:phone",
"Seat": "Seat",
"Seat Display Name": "Seat Display Name",
@ -537,6 +547,7 @@
"Unblock Incoming/Outgoing": "Unblock Incoming/Outgoing",
"Unblock Outgoing": "Unblock Outgoing",
"Undo": "Undo",
"Unexpected error": "Unexpected error",
"Unknown error": "Unknown error",
"Unknown name": "Unknown name",
"Updated {field} for call queue {callQueue} successfully": "Updated {field} for call queue {callQueue} successfully",
@ -551,6 +562,7 @@
"Use as default for all seats and groups": "Use as default for all seats and groups",
"Use custom number": "Use custom number",
"Use language specific preset": "Use language specific preset",
"Use the Authenticator app to generate the verification code.": "Use the Authenticator app to generate the verification code.",
"User Agent": "User Agent",
"User config priority over provisioning": "User config priority over provisioning",
"User settings": "User settings",

@ -61,8 +61,8 @@
"Block List for outbounds calls": "Block List for outbounds calls",
"Block Mode for inbound calls": "Block Mode for inbound calls",
"Block Mode for outbounds calls": "Block Mode for outbounds calls",
"Block anonymous inbound calls": "Block anonymous inbound calls",
"Block Outgoing": "Bloquear Salientes",
"Block anonymous inbound calls": "Block anonymous inbound calls",
"Busy Greeting": "Saludo de Ocupado",
"Busy Lamp Field": "Campo de lámpara ocupado",
"CDR": "CDR",
@ -172,7 +172,10 @@
"Disable": "Desactivar",
"Disable phone web interface": "Deshabilitar la interfaz web del teléfono",
"Display Name": "Display Name",
"Display as QR code": "Mostrar como código QR",
"Display as text": "Mostrar como texto",
"Do not ring primary number": "No llamar al número principal",
"Download App": "Descargar la aplicación",
"Download CSV": "Download CSV",
"Download fax": "Descargar fax",
"Download voicemail": "Descargar correo de voz",
@ -188,6 +191,7 @@
"Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.": "Activar modo estricto requiere que todos los correos electrónicos de Correo a Fax tengan la clave secreta como la primera línea del correo electrónico y una línea vacía. La clave se elimina del correo electrónico una vez que coincide.",
"End time": "Hora de finalización",
"English": "English",
"Enter OTP": "Introduce el código OTP",
"Enter a number to dial": "Introduzca un número para marcar",
"Entity belongs to admin": "Esta entidad pertenece a admin",
"Expires": "Expira",
@ -266,7 +270,9 @@
"Input a valid phone number": "Ingrese un número de teléfono válido",
"Input is required": "El campo es obligatorio",
"Input must be a valid number": "El campo debe ser un número válido",
"Install Google Authenticator app on your mobile device or use your preferred Authenticator app.": "Instala la aplicación Google Authenticator en tu dispositivo móvil o usa tu aplicación de autenticación preferida.",
"Interval when the secret key is automatically renewed.": "Intervalo en el que la clave secreta se renueva automáticamente.",
"Invalid OTP Code": "Código OTP no válido",
"Italian": "Italiano",
"January": "Enero",
"Join conference": "Unirse a la conferencia",
@ -353,6 +359,8 @@
"Number list": "Lista de números",
"Number list name": "Nombre de la lista de números",
"Numbers": "Números",
"OTP Code": "Código OTP",
"OTP Verification": "Verificación OTP",
"October": "Octubre",
"Office Hours Announcement": "Anuncio de Horarios de Oficina",
"On weekdays": "En días de la semana",
@ -360,6 +368,7 @@
"Only none decimal numbers are allowed": "Sólo se permiten números no decimales",
"Only once": "Solo una vez",
"Only outgoing calls to listed numbers are allowed": "Permitir solo los números listados",
"Open the Authenticator app to register your NGCP account.": "Abre la aplicación de autenticación para registrar tu cuenta NGCP.",
"Out": "Out",
"Outgoing": "Saliente",
"PBX Configuration": "Configuración PBX",
@ -466,6 +475,7 @@
"Saturday": "Sábado",
"Save": "Guardar",
"Save new password": "Guardar nueva contraseña",
"Scan QR code": "Escanear código QR",
"Scan to login sip:phone": "Escanear para iniciar sesión Sip:phone",
"Seat": "Asiento",
"Seat Display Name": "Seat Display Name",
@ -556,6 +566,7 @@
"Unblock Incoming/Outgoing": "Desbloquear Entrantes/Salientes",
"Unblock Outgoing": "Desbloquear Salientes",
"Undo": "Deshacer",
"Unexpected error": "Error inesperado",
"Unknown error": "Error desconocido",
"Unknown name": "Nombre desconocido",
"Unmute": "Activar sonido",
@ -572,6 +583,7 @@
"Use as default for all seats and groups": "Usar por defecto para todos los asientos y grupos",
"Use custom number": "Use custom number",
"Use language specific preset": "Usar un preajuste específico de idioma",
"Use the Authenticator app to generate the verification code.": "Usa la aplicación de autenticación para generar el código de verificación.",
"User Agent": "Agente de usuario",
"User config priority over provisioning": "Prioridad de la configuración del usuario sobre la provisión",
"User settings": "Ajustes de usuario",

@ -61,8 +61,8 @@
"Block List for outbounds calls": "Liste de blocage pour les appels sortants",
"Block Mode for inbound calls": "Mode blocage pour les appels entrants",
"Block Mode for outbounds calls": "Mode blocage pour les appels sortants",
"Block anonymous inbound calls": "Bloquer les appels entrants anonymes",
"Block Outgoing": "Bloquer les Sortants",
"Block anonymous inbound calls": "Bloquer les appels entrants anonymes",
"Busy Greeting": "Message d'accueil en cas d'occupation",
"Busy Lamp Field": "Champ de la lampe occupée",
"CDR": "CDR",
@ -172,7 +172,10 @@
"Disable": "Désactiver",
"Disable phone web interface": "Désactiver l'interface Web du téléphone",
"Display Name": "Display Name",
"Display as QR code": "Afficher sous forme de code QR",
"Display as text": "Afficher sous forme de texte",
"Do not ring primary number": "Ne pas appeler le numéro principal",
"Download App": "Télécharger lapplication",
"Download CSV": "Télécharger CSV",
"Download fax": "Télécharger le Fax",
"Download voicemail": "Télécharger le message vocal",
@ -188,6 +191,7 @@
"Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.": "Activez le mode strict qui exige que tous les e-mails de mail2fax aient la clé secrète comme toute première ligne de l'e-mail + une ligne vide. La clé est supprimée de l'e-mail une fois qu'elle a été trouvée.",
"End time": "Date de fin",
"English": "English",
"Enter OTP": "OTP-Code eingeben",
"Enter a number to dial": "Enter a number to dial",
"Entity belongs to admin": "Cette entité appartient à ladministrateur",
"Expires": "Expire",
@ -266,7 +270,9 @@
"Input a valid phone number": "Saisissez un numéro de téléphone valide",
"Input is required": "Le champ est obligatoire",
"Input must be a valid number": "Le champ doit être un nombre valide",
"Install Google Authenticator app on your mobile device or use your preferred Authenticator app.": "Installez lapplication Google Authenticator sur votre appareil mobile ou utilisez votre application dauthentification préférée.",
"Interval when the secret key is automatically renewed.": "Intervalle où la clé secrète est automatiquement renouvelée.",
"Invalid OTP Code": "Code OTP invalid",
"Italian": "Italiano",
"January": "Janvier",
"Join conference": "Rejoindre la conférence",
@ -354,6 +360,8 @@
"Number list": "Liste de numéros",
"Number list name": "Nom de la liste de numéros",
"Numbers": "Nombres",
"OTP Code": "Code OTP",
"OTP Verification": "Vérification OTP",
"October": "Octobre",
"Office Hours Announcement": "Office Hours Announcement",
"On weekdays": "Les jours de semaine",
@ -361,6 +369,7 @@
"Only none decimal numbers are allowed": "Seuls les nombres non décimaux sont autorisés",
"Only once": "Une seule fois",
"Only outgoing calls to listed numbers are allowed": "Seuls les appels sortants vers les numéros listés sont autorisés",
"Open the Authenticator app to register your NGCP account.": "Ouvrez lapplication dauthentification pour enregistrer votre compte NGCP.",
"Out": "Out",
"Outgoing": "Sortant",
"PBX Configuration": "Configuration du PBX",
@ -466,6 +475,7 @@
"Saturday": "Samedi",
"Save": "Enregistrer",
"Save new password": "Sauvegarder le nouveau mot de passe",
"Scan QR code": "Scanner le code QR",
"Scan to login sip:phone": "Scan to login sip:phone",
"Seat": "Siège",
"Seat Display Name": "Seat Display Name",
@ -554,6 +564,7 @@
"Unblock Incoming/Outgoing": "Débloquer les entrants/sortants",
"Unblock Outgoing": "Débloquer les sortants",
"Undo": "Annuler",
"Unexpected error": "Erreur inattendue",
"Unknown error": "Erreur inconnue",
"Unknown name": "Nom inconnu",
"Unmute": "Réactiver le son",
@ -570,6 +581,7 @@
"Use as default for all seats and groups": "Utiliser par défaut pour tous les sièges et groupes",
"Use custom number": "Use custom number",
"Use language specific preset": "Utiliser un préréglage spécifique à la langue",
"Use the Authenticator app to generate the verification code.": "Utilisez lapplication dauthentification pour générer le code de vérification.",
"User Agent": "User Agent",
"User config priority over provisioning": "Priorité de la configuration utilisateur sur le provisionnement",
"User settings": "Paramètres utilisateur",

@ -59,8 +59,8 @@
"Block List for outbounds calls": "Lista numeri bloccati in uscita",
"Block Mode for inbound calls": "Modalità di blocco per le chiamate in entrata",
"Block Mode for outbounds calls": "Modalità di blocco per le chiamate in uscita",
"Block anonymous inbound calls": "Blocca chiamate anonime in entrata",
"Block Outgoing": "Blocca Chiamate in Uscita",
"Block anonymous inbound calls": "Blocca chiamate anonime in entrata",
"Busy Greeting": "Messaggio per numero occupato",
"Busy Lamp Field": "Busy Lamp Field",
"CDR": "CDR",
@ -170,7 +170,10 @@
"Disable": "Disabilita",
"Disable phone web interface": "Disabilita interfaccia web del telefono",
"Display Name": "Nome visualizzato",
"Display as QR code": "Mostra come codice QR",
"Display as text": "Mostra come testo",
"Do not ring primary number": "Non squillare sul numero principale",
"Download App": "Scarica l'app",
"Download CSV": "Scarica CSV",
"Download fax": "Scarica fax",
"Download voicemail": "Scarica messaggio vocale",
@ -185,6 +188,7 @@
"Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.": "Abilita la modalità rigorosa che richiede che tutte le email mail2fax contengano la chiave segreta come prima riga + una riga vuota. La chiave viene rimossa una volta riconosciuta.",
"End time": "Orario di fine",
"English": "English",
"Enter OTP": " Inserisci il codice OTP",
"Enter a number to dial": "Inserisci un numero da chiamare",
"Entity belongs to admin": "L'entità appartiene all'amministratore",
"Expires": "Scade",
@ -263,7 +267,9 @@
"Input a valid phone number": "Inserire un numero di telefono valido",
"Input is required": "Il campo è obbligatorio",
"Input must be a valid number": "Il campo deve essere un numero valido",
"Install Google Authenticator app on your mobile device or use your preferred Authenticator app.": "Installa l'app Google Authenticator sul tuo dispositivo mobile o usa la tua app di autenticazione preferita.",
"Interval when the secret key is automatically renewed.": "Intervallo di rinnovo automatico della chiave segreta.",
"Invalid OTP Code": "Codice OTP non valido",
"Italian": "Italiano",
"January": "Gennaio",
"Join conference with name": "Partecipa alla conferenza con nome",
@ -347,6 +353,8 @@
"Number list": "Lista numeri",
"Number list name": "Nome della lista numeri",
"Numbers": "Numeri",
"OTP Code": "Codice OTP",
"OTP Verification": "Verifica OTP",
"October": "Ottobre",
"Office Hours Announcement": "Annuncio degli Orari di Ufficio",
"On weekdays": "Nei giorni feriali",
@ -354,6 +362,7 @@
"Only none decimal numbers are allowed": "Sono ammessi solo numeri non decimali",
"Only once": "Solo una volta",
"Only outgoing calls to listed numbers are allowed": "Sono ammesse solo le chiamate in uscita verso i numeri elencati",
"Open the Authenticator app to register your NGCP account.": "Apri l'app di autenticazione per registrare il tuo account NGCP.",
"Out": "Out",
"Outgoing": "In uscita",
"PBX Configuration": "Configurazione PBX",
@ -459,6 +468,7 @@
"Saturday": "Sabato",
"Save": "Salva",
"Save new password": "Salva nuova password",
"Scan QR code": "Scansiona il codice QR",
"Scan to login sip:phone": "Scansiona per accedere sip:phone",
"Seat": "Postazione",
"Seat Display Name": "Nome visualizzato postazione",
@ -546,6 +556,7 @@
"Unblock Incoming/Outgoing": "Sblocca entrata/uscita",
"Unblock Outgoing": "Sblocca chiamate in uscita",
"Undo": "Annulla",
"Unexpected error": "Errore imprevisto",
"Unknown error": "Errore sconosciuto",
"Unknown name": "Nome sconosciuto",
"Updated {field} for call queue {callQueue} successfully": "Aggiornato {field} per la coda chiamate {callQueue}",
@ -560,6 +571,7 @@
"Use as default for all seats and groups": "Usa come predefinito per tutte le postazioni e i gruppi",
"Use custom number": "Usa numero personalizzato",
"Use language specific preset": "Usa predefiniti per la lingua",
"Use the Authenticator app to generate the verification code.": "Usa l'app di autenticazione per generare il codice di verifica.",
"User Agent": "User Agent",
"User config priority over provisioning": "Priorità della configurazione utente sul provisioning",
"User settings": "Impostazioni utente",

@ -59,6 +59,163 @@
clearable
@keypress.enter="login()"
/>
<div
v-if="showOTP"
class="row q-mb-md"
>
<q-card
flat
bordered
class="q-mt-lg"
>
<q-card-section
v-if="showOTPSecret"
class="text-center q-mt-none"
>
<q-icon
name="key"
size="4rem"
color="primary"
/>
<h4 class="text-h4 h4 text-center q-mt-sm q-mb-sm">
{{ $t('OTP Verification') }}
</h4>
</q-card-section>
<q-card-section v-if="showOTPSecret">
<q-list>
<q-item>
<div>
<h6 class="q-ma-sm">
<q-icon
name="download"
size="2rem"
color="primary"
class="q-mr-sm"
/>
{{ $t('Download App') }}
</h6>
<p class="q-ml-md">
{{ $t('Install Google Authenticator app on your mobile device or use your preferred Authenticator app.') }}
</p>
</div>
</q-item>
<div
align="center"
class="q-pa-md"
>
<a
class="q-ma-md"
target="_blank"
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"
><img
alt="Get it on Google Play"
src="google-play-badge.png"
class="app-badge google-play-badge"
></a>
<a
class="q-ma-md"
target="_blank"
href="https://apps.apple.com/us/app/google-authenticator/id388497605?itsct=apps_box_badge&itscg=30200"
><img
src="apple-store-badge.svg"
alt="Download on the App Store"
class="app-badge apple-store-badge"
></a>
</div>
<q-item>
<div>
<h6 class="q-ma-sm">
<q-icon
name="qr_code"
size="2rem"
color="primary"
class="q-mr-sm"
/> {{ $t('Scan QR code') }}
</h6>
<p class="q-ml-md">
{{ $t('Open the Authenticator app to register your NGCP account.') }}
</p>
</div>
</q-item>
<div
v-if="OTPSecret && OTPSecret.type === 'qr'"
class="text-center q-pa-md"
>
<q-img
:src="OTPSecret.data"
class="qr-code bg-white"
/>
<q-btn
color="primary"
small
class="q-mt-md full-width"
@click="getOTPAsText"
>
{{ $t('Display as text') }}
</q-btn>
</div>
<div
v-if="OTPSecret && OTPSecret.type === 'text'"
class="text-center q-pa-md"
>
<q-card class="q-pa-sm">
<p class="text-bold text-green">
{{ OTPSecret.data }}
</p>
</q-card>
<q-btn
color="primary"
small
class="q-mt-md full-width"
@click="getOTPAsQrCode"
>
{{ $t('Display as QR code') }}
</q-btn>
</div>
</q-list>
</q-card-section>
<q-card-section>
<q-list>
<q-item>
<div>
<h6 class="q-ma-sm">
<q-icon
name="password"
size="2rem"
color="primary"
class="q-mr-sm"
/>{{ $t('Enter OTP') }}
</h6>
<p class="q-ml-md">
{{ $t('Use the Authenticator app to generate the verification code.') }}
</p>
</div>
</q-item>
<q-item class="justify-center">
<q-input
v-model="otp"
color="primary"
label-color="primary"
data-cy="otp-code"
:loading="loginRequesting"
:disable="loginRequesting"
:label="$t('OTP Code')"
:error="OTPError"
:error-message="loginError"
@input-clear="clearOTP"
@input="focusOTP"
@keypress.enter="login"
/>
</q-item>
</q-list>
</q-card-section>
</q-card>
</div>
</form>
</q-card-section>
<q-card-actions
@ -123,7 +280,9 @@ export default {
return {
username: '',
password: '',
showDialog: false
showDialog: false,
otp: null,
OTPError: false
}
},
computed: {
@ -140,8 +299,16 @@ export default {
...mapGetters('user', [
'loginRequesting',
'loginSucceeded',
'loginError'
])
'loginError',
'loginWaitingOTPCode',
'OTPSecret'
]),
showOTP () {
return this.loginWaitingOTPCode || this.loginError === 'Invalid OTP Code'
},
showOTPSecret () {
return this.OTPSecret !== null || (this.OTPSecret !== null && this.loginError === 'Invalid OTP Code')
}
},
watch: {
loginSucceeded (loggedIn) {
@ -152,6 +319,9 @@ export default {
loginError (error) {
if (error) {
showGlobalError(error)
const isInvalidOTPError = this.loginError === 'Invalid OTP Code'
this.OTPError = isInvalidOTPError
this.otp = null
}
}
},
@ -164,12 +334,33 @@ export default {
methods: {
login () {
this.$store.dispatch('user/login', {
username: this.username,
password: this.password,
...(this.otp && { otp: this.otp }),
type: 'qr'
})
},
getOTPAsText () {
this.$store.dispatch('user/getOTPSecretAsText', {
username: this.username,
password: this.password
})
},
getOTPAsQrCode () {
this.$store.dispatch('user/getOTPSecret', {
username: this.username,
password: this.password
})
},
showRetrievePasswordDialog () {
this.showDialog = true
},
focusOTP () {
this.OTPError = false
},
clearOTP () {
this.otp = null
this.OTPError = false
}
}
}
@ -179,4 +370,10 @@ export default {
#csc-login-card
margin: 0
margin-top: $header-height * -2
.qr-code
width: 200px
height: 200px
.app-badge
height: 50px
object-fit: contain
</style>

@ -3,7 +3,7 @@ import { i18n } from 'boot/i18n'
import _ from 'lodash'
import QRCode from 'qrcode'
import { date } from 'quasar'
import { apiDownloadFile, httpApi } from 'src/api/common'
import { apiDownloadFile, apiGet, httpApi } from 'src/api/common'
import { callInitialize } from 'src/api/ngcp-call'
import {
changePassword,
@ -41,6 +41,7 @@ import {
} from 'src/auth'
import { PROFILE_ATTRIBUTE_MAP } from 'src/constants'
import { getSipInstanceId } from 'src/helpers/call-utils'
import { parseBlobToObject } from 'src/helpers/parse-blob-to-object'
import { qrPayload } from 'src/helpers/qr'
import { PATH_CHANGE_PASSWORD } from 'src/router/routes'
import { setLocal } from 'src/storage'
@ -61,6 +62,8 @@ export default {
loginRequesting: false,
loginSucceeded: false,
loginError: null,
loginWaitingOTPCode: false,
OTPSecret: null,
userDataRequesting: false,
userDataSucceeded: false,
userDataError: null,
@ -135,6 +138,12 @@ export default {
loginError (state) {
return state.loginError
},
loginWaitingOTPCode (state) {
return state.loginWaitingOTPCode
},
OTPSecret (state) {
return state.OTPSecret
},
passwordRequirements (state) {
return state.platformInfo?.security?.password || []
},
@ -227,6 +236,8 @@ export default {
state.loginRequesting = true
state.loginSucceeded = false
state.loginError = null
state.loginWaitingOTPCode = false
state.OTPSecret = null
},
loginSucceeded (state, options) {
state.jwt = options.jwt
@ -234,11 +245,15 @@ export default {
state.loginRequesting = false
state.loginSucceeded = true
state.loginError = null
state.loginWaitingOTPCode = false
state.OTPSecret = null
},
loginFailed (state, error) {
state.loginRequesting = false
state.loginSucceeded = false
state.loginError = error
state.loginWaitingOTPCode = false
state.OTPSecret = null
},
userDataRequesting (state) {
state.resellerBranding = null
@ -329,13 +344,27 @@ export default {
if (index > -1) {
state.subscriberPhonebook[index].shared = value
}
},
loginWaitingForOTPCode (state) {
state.loginWaitingOTPCode = true
state.OTPSecret = null
state.loginRequesting = false
},
storeOTPSecret (state, payload) {
state.loginWaitingOTPCode = true
state.OTPSecret = payload
state.loginRequesting = false
}
},
actions: {
async login (context, options) {
context.commit('loginRequesting')
try {
const result = await login(options.username, options.password)
const result = await login({
username: options.username,
password: options.password,
...(options.otp && { otp: options.otp })
})
setJwt(result.jwt)
setSubscriberId(result.subscriberId)
context.commit('loginSucceeded', {
@ -345,6 +374,16 @@ export default {
await context.dispatch('initUser')
await this.$router.push({ name: 'dashboard' })
} catch (err) {
if (err.message === 'Invalid OTP') {
if (context.state.loginWaitingOTPCode) {
context.commit('loginFailed', i18n.global.t('Invalid OTP Code'))
throw err
}
return context.dispatch('getOTPSecret', {
username: options.username,
password: options.password
})
}
context.commit('loginFailed', err.message)
if (err.message === 'Password expired') {
this.$router.push({ path: PATH_CHANGE_PASSWORD })
@ -357,6 +396,64 @@ export default {
deleteJwt()
document.location.href = document.location.pathname
},
async getOTPSecret ({ commit }, options) {
try {
const token = `${options.username}:${options.password}`
const encodedToken = btoa(token).toString('base64')
const headers = {
Authorization: `Basic ${encodedToken}`,
'Cache-Control': 'no-cache',
Accept: 'image/png'
}
const res = await apiGet(
{
path: 'api/otpsecret',
config: {
responseType: 'blob',
headers
}
})
const url = URL.createObjectURL(res.data)
commit('storeOTPSecret', { type: 'qr', data: url })
} catch (err) {
try {
const errorData = await parseBlobToObject(err.response.data)
if ([400].includes(errorData?.code) && ['no OTP'].includes(errorData?.message)) {
return commit('loginWaitingForOTPCode')
}
} catch (err) {
commit('loginFailed', i18n.global.t('Unexpected error'))
throw err
}
}
},
async getOTPSecretAsText ({ commit }, options) {
try {
const token = `${options.username}:${options.password}`
const encodedToken = btoa(token).toString('base64')
const headers = {
Authorization: `Basic ${encodedToken}`,
'Cache-Control': 'no-cache',
Accept: 'text/plain'
}
const res = await apiGet(
{
path: 'api/otpsecret',
config: { headers }
})
commit('storeOTPSecret', { type: 'text', data: res.data })
} catch (err) {
try {
const errorData = await parseBlobToObject(err.response.data)
if ([400].includes(errorData?.code) && ['no OTP'].includes(errorData?.message)) {
return commit('loginWaitingForOTPCode')
}
} catch (err) {
commit('loginFailed', i18n.global.t('Unexpected error'))
throw err
}
}
},
async initUser (context) {
if (!context.getters.userDataSucceeded) {
try {

Loading…
Cancel
Save