From 765150281853237d18599179a4eaa39b032b4d26 Mon Sep 17 00:00:00 2001 From: Debora Crescenzo Date: Tue, 10 Jun 2025 19:10:21 +0100 Subject: [PATCH] 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 --- src/api/user.js | 5 +- src/helpers/parse-blob-to-object.js | 4 + src/i18n/de.json | 14 +- src/i18n/en.json | 14 +- src/i18n/es.json | 14 +- src/i18n/fr.json | 14 +- src/i18n/it.json | 14 +- src/pages/CscPageLogin.vue | 203 +++++++++++++++++++++++++++- src/store/user.js | 101 +++++++++++++- 9 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 src/helpers/parse-blob-to-object.js diff --git a/src/api/user.js b/src/api/user.js index a5831bbd..6a523470 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -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}` diff --git a/src/helpers/parse-blob-to-object.js b/src/helpers/parse-blob-to-object.js new file mode 100644 index 00000000..6648cc34 --- /dev/null +++ b/src/helpers/parse-blob-to-object.js @@ -0,0 +1,4 @@ +export async function parseBlobToObject (blob) { + const text = await blob.text() + return JSON.parse(text) +} diff --git a/src/i18n/de.json b/src/i18n/de.json index 2d065d1f..896d4fbc 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -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", diff --git a/src/i18n/en.json b/src/i18n/en.json index 4aa5f044..469ce69c 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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", diff --git a/src/i18n/es.json b/src/i18n/es.json index 1ef48d43..c9076bbd 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -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", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index ed1eb9d6..c96883c6 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -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 l’application", "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 à l’administrateur", "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 l’application Google Authenticator sur votre appareil mobile ou utilisez votre application d’authentification 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 l’application d’authentification 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 l’application d’authentification 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", diff --git a/src/i18n/it.json b/src/i18n/it.json index ffbdaf37..1467f2ec 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -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", diff --git a/src/pages/CscPageLogin.vue b/src/pages/CscPageLogin.vue index 84627f7e..ff086135 100644 --- a/src/pages/CscPageLogin.vue +++ b/src/pages/CscPageLogin.vue @@ -59,6 +59,163 @@ clearable @keypress.enter="login()" /> +
+ + + +

+ {{ $t('OTP Verification') }} +

+
+ + + + +
+
+ + {{ $t('Download App') }} +
+

+ {{ $t('Install Google Authenticator app on your mobile device or use your preferred Authenticator app.') }} +

+
+
+ +
+ Get it on Google Play + + Download on the App Store +
+ + +
+
+ {{ $t('Scan QR code') }} +
+

+ {{ $t('Open the Authenticator app to register your NGCP account.') }} +

+
+
+
+ + + {{ $t('Display as text') }} + +
+
+ +

+ {{ OTPSecret.data }} +

+
+ + {{ $t('Display as QR code') }} + +
+
+
+ + + + +
+
+ {{ $t('Enter OTP') }} +
+

+ {{ $t('Use the Authenticator app to generate the verification code.') }} +

+
+
+ + + +
+
+
+
diff --git a/src/store/user.js b/src/store/user.js index 8831319f..4470bb76 100644 --- a/src/store/user.js +++ b/src/store/user.js @@ -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 {