From 999c39833ad1ce319b638d78cee746b573302102 Mon Sep 17 00:00:00 2001 From: eikek Date: Mon, 30 Aug 2021 23:54:37 +0200 Subject: [PATCH] Add user setting page for totp --- modules/webapp/src/main/elm/Api.elm | 50 ++ modules/webapp/src/main/elm/Comp/OtpSetup.elm | 430 ++++++++++++++++++ .../src/main/elm/Messages/Comp/OtpSetup.elm | 100 ++++ .../main/elm/Messages/Page/UserSettings.elm | 7 + .../src/main/elm/Page/UserSettings/Data.elm | 13 +- .../src/main/elm/Page/UserSettings/Update.elm | 15 + .../src/main/elm/Page/UserSettings/View2.elm | 34 ++ 7 files changed, 648 insertions(+), 1 deletion(-) create mode 100644 modules/webapp/src/main/elm/Comp/OtpSetup.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/OtpSetup.elm diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 9d65af14..18ed9cbd 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -19,6 +19,7 @@ module Api exposing , changePassword , checkCalEvent , confirmMultiple + , confirmOtp , createImapSettings , createMailSettings , createNewFolder @@ -42,6 +43,7 @@ module Api exposing , deleteSource , deleteTag , deleteUser + , disableOtp , fileURL , getAttachmentMeta , getClientSettings @@ -63,6 +65,7 @@ module Api exposing , getOrgFull , getOrgLight , getOrganizations + , getOtpState , getPersonFull , getPersons , getPersonsLight @@ -72,6 +75,7 @@ module Api exposing , getTagCloud , getTags , getUsers + , initOtp , itemBasePreviewURL , itemDetail , itemIndexSearch @@ -194,6 +198,9 @@ import Api.Model.OptionalId exposing (OptionalId) import Api.Model.OptionalText exposing (OptionalText) import Api.Model.Organization exposing (Organization) import Api.Model.OrganizationList exposing (OrganizationList) +import Api.Model.OtpConfirm exposing (OtpConfirm) +import Api.Model.OtpResult exposing (OtpResult) +import Api.Model.OtpState exposing (OtpState) import Api.Model.PasswordChange exposing (PasswordChange) import Api.Model.Person exposing (Person) import Api.Model.PersonList exposing (PersonList) @@ -2128,6 +2135,49 @@ saveClientSettings flags settings receive = +--- OTP + + +getOtpState : Flags -> (Result Http.Error OtpState -> msg) -> Cmd msg +getOtpState flags receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/user/otp/state" + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.OtpState.decoder + } + + +initOtp : Flags -> (Result Http.Error OtpResult -> msg) -> Cmd msg +initOtp flags receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/user/otp/init" + , account = getAccount flags + , body = Http.emptyBody + , expect = Http.expectJson receive Api.Model.OtpResult.decoder + } + + +confirmOtp : Flags -> OtpConfirm -> (Result Http.Error BasicResult -> msg) -> Cmd msg +confirmOtp flags confirm receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/user/otp/confirm" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.OtpConfirm.encode confirm) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +disableOtp : Flags -> (Result Http.Error BasicResult -> msg) -> Cmd msg +disableOtp flags receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/user/otp/disable" + , account = getAccount flags + , body = Http.emptyBody + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + + --- Helper diff --git a/modules/webapp/src/main/elm/Comp/OtpSetup.elm b/modules/webapp/src/main/elm/Comp/OtpSetup.elm new file mode 100644 index 00000000..3b7e62d5 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/OtpSetup.elm @@ -0,0 +1,430 @@ +{- + Copyright 2020 Docspell Contributors + + SPDX-License-Identifier: GPL-3.0-or-later +-} + + +module Comp.OtpSetup exposing (Model, Msg, init, update, view) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.OtpConfirm exposing (OtpConfirm) +import Api.Model.OtpResult exposing (OtpResult) +import Api.Model.OtpState exposing (OtpState) +import Comp.Basic as B +import Comp.PasswordInput +import Data.Flags exposing (Flags) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick, onInput, onSubmit) +import Http +import Markdown +import Messages.Comp.OtpSetup exposing (Texts) +import QRCode +import Styles as S + + +type Model + = InitialModel + | StateError Http.Error + | InitError Http.Error + | DisableError Http.Error + | ConfirmError Http.Error + | StateEnabled EnabledModel + | StateDisabled DisabledModel + | SetupSuccessful + + +type alias DisabledModel = + { loading : Bool + , result : Maybe OtpResult + , secretModel : Comp.PasswordInput.Model + , confirmCode : String + , confirmError : Bool + } + + +initDisabledModel : DisabledModel +initDisabledModel = + { loading = False + , result = Nothing + , secretModel = Comp.PasswordInput.init + , confirmCode = "" + , confirmError = False + } + + +type alias EnabledModel = + { created : Int + , loading : Bool + , confirmText : String + , confirmTextWrong : Bool + } + + +initEnabledModel : Int -> EnabledModel +initEnabledModel created = + { created = created + , loading = False + , confirmText = "" + , confirmTextWrong = False + } + + +emptyModel : Model +emptyModel = + InitialModel + + +type Msg + = GetStateResp (Result Http.Error OtpState) + | Initialize + | InitResp (Result Http.Error OtpResult) + | SetConfirmCode String + | SecretMsg Comp.PasswordInput.Msg + | Confirm + | ConfirmResp (Result Http.Error BasicResult) + | SetDisableConfirmText String + | Disable + | DisableResp (Result Http.Error BasicResult) + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, Api.getOtpState flags GetStateResp ) + + + +--- Update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update flags msg model = + case msg of + GetStateResp (Ok state) -> + if state.enabled then + ( StateEnabled <| initEnabledModel (Maybe.withDefault 0 state.created), Cmd.none ) + + else + ( StateDisabled initDisabledModel, Cmd.none ) + + GetStateResp (Err err) -> + ( StateError err, Cmd.none ) + + Initialize -> + case model of + StateDisabled _ -> + ( StateDisabled { initDisabledModel | loading = True } + , Api.initOtp flags InitResp + ) + + _ -> + ( model, Cmd.none ) + + InitResp (Ok r) -> + case model of + StateDisabled m -> + ( StateDisabled { m | result = Just r, loading = False }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + InitResp (Err err) -> + ( InitError err, Cmd.none ) + + SetConfirmCode str -> + case model of + StateDisabled m -> + ( StateDisabled { m | confirmCode = str }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + SecretMsg lm -> + case model of + StateDisabled m -> + let + ( pm, _ ) = + Comp.PasswordInput.update lm m.secretModel + in + ( StateDisabled { m | secretModel = pm }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + Confirm -> + case model of + StateDisabled m -> + ( StateDisabled { m | loading = True } + , Api.confirmOtp flags (OtpConfirm m.confirmCode) ConfirmResp + ) + + _ -> + ( model, Cmd.none ) + + ConfirmResp (Ok result) -> + case model of + StateDisabled m -> + if result.success then + ( SetupSuccessful, Cmd.none ) + + else + ( StateDisabled { m | confirmError = True, loading = False }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + ConfirmResp (Err err) -> + ( ConfirmError err, Cmd.none ) + + SetDisableConfirmText str -> + case model of + StateEnabled m -> + ( StateEnabled { m | confirmText = str }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + Disable -> + case model of + StateEnabled m -> + if String.toLower m.confirmText == "ok" then + ( StateEnabled { m | confirmTextWrong = False, loading = True } + , Api.disableOtp flags DisableResp + ) + + else + ( StateEnabled { m | confirmTextWrong = True }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + DisableResp (Ok result) -> + if result.success then + init flags + + else + ( model, Cmd.none ) + + DisableResp (Err err) -> + ( DisableError err, Cmd.none ) + + + +--- View + + +view : Texts -> Model -> Html Msg +view texts model = + case model of + InitialModel -> + div [] [] + + StateError err -> + viewHttpError texts texts.stateErrorInfoText err + + InitError err -> + viewHttpError texts texts.initErrorInfo err + + ConfirmError err -> + viewHttpError texts texts.confirmErrorInfo err + + DisableError err -> + viewHttpError texts texts.disableErrorInfo err + + SetupSuccessful -> + viewSetupSuccessful texts + + StateEnabled m -> + viewEnabled texts m + + StateDisabled m -> + viewDisabled texts m + + +viewEnabled : Texts -> EnabledModel -> Html Msg +viewEnabled texts model = + div [] + [ h2 [ class S.header2 ] + [ text texts.twoFaActiveSince + , text <| texts.formatDateShort model.created + ] + , p [] + [ text texts.revert2FAText + ] + , div [ class "flex flex-col items-center mt-6" ] + [ div [ class "flex flex-row max-w-md" ] + [ input + [ type_ "text" + , value model.confirmText + , onInput SetDisableConfirmText + , class S.textInput + , class "rounded-r-none" + ] + [] + , B.genericButton + { label = texts.disableButton + , icon = + if model.loading then + "fa fa-circle-notch animate-spin" + + else + "fa fa-exclamation-circle" + , handler = onClick Disable + , disabled = model.loading + , attrs = [ href "#" ] + , baseStyle = S.primaryButtonPlain ++ " rounded-r" + , activeStyle = S.primaryButtonHover + } + ] + , div + [ class S.errorMessage + , class "my-2" + , classList [ ( "hidden", not model.confirmTextWrong ) ] + ] + [ text texts.disableConfirmErrorMsg + ] + , Markdown.toHtml [ class "mt-2" ] texts.disableConfirmBoxInfo + ] + ] + + +viewDisabled : Texts -> DisabledModel -> Html Msg +viewDisabled texts model = + div [] + [ h2 [ class S.header2 ] + [ text texts.setupTwoFactorAuth + ] + , p [] + [ text texts.setupTwoFactorAuthInfo + ] + , case model.result of + Nothing -> + div [ class "flex flex-row items-center justify-center my-6 px-2" ] + [ B.primaryButton + { label = texts.activateButton + , icon = + if model.loading then + "fa fa-circle-notch animate-spin" + + else + "fa fa-key" + , disabled = model.loading + , handler = onClick Initialize + , attrs = [ href "#" ] + } + ] + + Just data -> + div [ class "flex flex-col mt-6" ] + [ div [ class "flex flex-col items-center justify-center" ] + [ div + [ class S.border + , class S.qrCode + ] + [ qrCodeView texts data.authenticatorUrl + ] + , div [ class "mt-4" ] + [ p [] + [ text texts.scanQRCode + ] + ] + ] + , div [ class "flex flex-col items-center justify-center mt-4" ] + [ Html.form [ class "flex flex-row relative", onSubmit Confirm ] + [ input + [ type_ "text" + , name "confirm-setup" + , autocomplete False + , onInput SetConfirmCode + , value model.confirmCode + , autofocus True + , class "pl-2 pr-10 py-2 rounded-lg max-w-xs text-center font-mono " + , class S.textInput + , if model.confirmError then + class S.inputErrorBorder + + else + class "" + , placeholder "123456" + ] + [] + , a + [ class S.inputLeftIconLink + , href "#" + , onClick Confirm + ] + [ if model.loading then + i [ class "fa fa-circle-notch animate-spin" ] [] + + else + i [ class "fa fa-check" ] [] + ] + ] + , div + [ classList [ ( "hidden", not model.confirmError ) ] + , class S.errorMessage + , class "mt-2" + ] + [ text texts.setupCodeInvalid ] + , div [ class "mt-6" ] + [ p [] [ text texts.ifNotQRCode ] + , div [ class "max-w-md mx-auto mt-4" ] + [ Html.map SecretMsg + (Comp.PasswordInput.view2 + { placeholder = "" } + (Just data.secret) + False + model.secretModel + ) + ] + ] + ] + ] + ] + + +qrCodeView : Texts -> String -> Html msg +qrCodeView texts message = + QRCode.encode message + |> Result.map QRCode.toSvg + |> Result.withDefault + (Html.text texts.errorGeneratingQR) + + +viewHttpError : Texts -> String -> Http.Error -> Html Msg +viewHttpError texts descr err = + div [ class S.errorMessage ] + [ h2 [ class S.header2 ] + [ text texts.errorTitle + ] + , p [] + [ text descr + , text " " + , text <| texts.httpError err + ] + , p [] + [ text texts.reloadToTryAgain + ] + ] + + +viewSetupSuccessful : Texts -> Html msg +viewSetupSuccessful texts = + div [ class "flex flex-col" ] + [ div + [ class S.successMessage + , class "text-lg" + ] + [ h2 + [ class "text-2xl font-medium tracking-wide" + ] + [ i [ class "fa fa-check mr-2" ] [] + , text texts.twoFactorNowActive + ] + ] + , div [ class "mt-4" ] + [ text texts.revertInfo + ] + ] diff --git a/modules/webapp/src/main/elm/Messages/Comp/OtpSetup.elm b/modules/webapp/src/main/elm/Messages/Comp/OtpSetup.elm new file mode 100644 index 00000000..e292245f --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/OtpSetup.elm @@ -0,0 +1,100 @@ +{- + Copyright 2020 Docspell Contributors + + SPDX-License-Identifier: GPL-3.0-or-later +-} + + +module Messages.Comp.OtpSetup exposing + ( Texts + , de + , gb + ) + +import Http +import Messages.Comp.HttpError +import Messages.DateFormat +import Messages.UiLanguage + + +type alias Texts = + { httpError : Http.Error -> String + , formatDateShort : Int -> String + , errorTitle : String + , stateErrorInfoText : String + , errorGeneratingQR : String + , initErrorInfo : String + , confirmErrorInfo : String + , disableErrorInfo : String + , twoFaActiveSince : String + , revert2FAText : String + , disableButton : String + , disableConfirmErrorMsg : String + , disableConfirmBoxInfo : String + , setupTwoFactorAuth : String + , setupTwoFactorAuthInfo : String + , activateButton : String + , setupConfirmLabel : String + , scanQRCode : String + , setupCodeInvalid : String + , ifNotQRCode : String + , reloadToTryAgain : String + , twoFactorNowActive : String + , revertInfo : String + } + + +gb : Texts +gb = + { httpError = Messages.Comp.HttpError.gb + , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.English + , errorTitle = "Error" + , stateErrorInfoText = "There was a problem determining the current state of your two factor authentication scheme:" + , errorGeneratingQR = "Error generating QR Code" + , initErrorInfo = "There was an error when initializing two-factor authentication." + , confirmErrorInfo = "There was an error when confirming the setup!" + , disableErrorInfo = "There was an error when disabling 2FA!" + , twoFaActiveSince = "Two Factor Authentication is active since " + , revert2FAText = "If you really want to revert back to password-only authentication, you can do this here. You can run the setup any time to enable the second factor again." + , disableButton = "Disable 2FA" + , disableConfirmErrorMsg = "Please type OK if you really want to disable this!" + , disableConfirmBoxInfo = "Type `OK` into the text box and click the button to disable 2FA." + , setupTwoFactorAuth = "Setup Two Factor Authentication" + , setupTwoFactorAuthInfo = "You can setup a second factor for authentication using a one-time password. When clicking the button a secret is generated that you can load into an app on your mobile device. The app then provides a 6 digit code that you need to pass in the field in order to confirm and finalize the setup." + , activateButton = "Activate two-factor authentication" + , setupConfirmLabel = "Confirm" + , scanQRCode = "Scan this QR code with your device and enter the 6 digit code:" + , setupCodeInvalid = "The confirmation code was invalid!" + , ifNotQRCode = "If you cannot use the qr code, enter this secret:" + , reloadToTryAgain = "If you want to try again, reload the page." + , twoFactorNowActive = "Two Factor Authentication is now active!" + , revertInfo = "You can revert back to password-only auth any time (reload this page)." + } + + +de : Texts +de = + { httpError = Messages.Comp.HttpError.de + , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.German + , errorTitle = "Fehler" + , stateErrorInfoText = "Es gab ein Problem, den Status der Zwei-Faktor-Authentifizierung zu ermittlen:" + , errorGeneratingQR = "Fehler beim Generieren des QR-Code" + , initErrorInfo = "Es gab ein Problem beim Initialisieren der Zwei-Faktor-Authentifizierung." + , confirmErrorInfo = "Es gab ein Problem bei der Verifizierung!" + , disableErrorInfo = "Es gab ein Problem die Zwei-Faktor-Authentifizierung zu entfernen." + , twoFaActiveSince = "Die Zwei-Faktor-Authentifizierung ist aktiv seit " + , revert2FAText = "Die Zwei-Faktor-Authentifizierung kann hier wieder deaktiviert werden. Danach kann die Einrichtung wieder von neuem gestartet werden, um 2FA wieder zu aktivieren." + , disableButton = "Deaktiviere 2FA" + , disableConfirmErrorMsg = "Bitte tippe OK ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren." + , disableConfirmBoxInfo = "Tippe `OK` in das Feld und klicke, um die Zwei-Faktor-Authentifizierung zu deaktivieren." + , setupTwoFactorAuth = "Zwei-Faktor-Authentifizierung einrichten" + , setupTwoFactorAuthInfo = "Ein zweiter Faktor zur Authentifizierung mittels eines Einmalkennworts kann eingerichtet werden. Beim Klicken des Button wird ein Schlüssel generiert, der an eine Authentifizierungs-App eines mobilen Gerätes übetragen werden kann. Danach präsentiert die App ein 6-stelliges Kennwort, welches zur Bestätigung und zum Abschluss angegeben werden muss." + , activateButton = "Zwei-Faktor-Authentifizierung aktivieren" + , setupConfirmLabel = "Bestätigung" + , scanQRCode = "Scanne den QR Code mit der Authentifizierungs-App und gebe den 6-stelligen Code ein:" + , setupCodeInvalid = "Der Code war ungültig!" + , ifNotQRCode = "Wenn der QR-Code nicht möglich ist, kann der Schlüssel manuell eingegeben werden:" + , reloadToTryAgain = "Um es noch einmal zu versuchen, bitte die Seite neu laden." + , twoFactorNowActive = "Die Zwei-Faktor-Authentifizierung ist nun aktiv!" + , revertInfo = "Es kann jederzeit zur normalen Passwort-Authentifizierung zurück gegangen werden (dazu Seite neu laden)." + } diff --git a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm index 0c22ac71..2089c17a 100644 --- a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm +++ b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm @@ -15,6 +15,7 @@ import Messages.Comp.ChangePasswordForm import Messages.Comp.EmailSettingsManage import Messages.Comp.ImapSettingsManage import Messages.Comp.NotificationManage +import Messages.Comp.OtpSetup import Messages.Comp.ScanMailboxManage import Messages.Comp.UiSettingsManage @@ -26,6 +27,7 @@ type alias Texts = , imapSettingsManage : Messages.Comp.ImapSettingsManage.Texts , notificationManage : Messages.Comp.NotificationManage.Texts , scanMailboxManage : Messages.Comp.ScanMailboxManage.Texts + , otpSetup : Messages.Comp.OtpSetup.Texts , userSettings : String , uiSettings : String , notifications : String @@ -38,6 +40,7 @@ type alias Texts = , notificationRemindDaysInfo : String , scanMailboxInfo1 : String , scanMailboxInfo2 : String + , otpMenu : String } @@ -49,6 +52,7 @@ gb = , imapSettingsManage = Messages.Comp.ImapSettingsManage.gb , notificationManage = Messages.Comp.NotificationManage.gb , scanMailboxManage = Messages.Comp.ScanMailboxManage.gb + , otpSetup = Messages.Comp.OtpSetup.gb , userSettings = "User Settings" , uiSettings = "UI Settings" , notifications = "Notifications" @@ -80,6 +84,7 @@ gb = or to just leave it there. In the latter case you should adjust the schedule to avoid reading over the same mails again.""" + , otpMenu = "Two Factor" } @@ -91,6 +96,7 @@ de = , imapSettingsManage = Messages.Comp.ImapSettingsManage.de , notificationManage = Messages.Comp.NotificationManage.de , scanMailboxManage = Messages.Comp.ScanMailboxManage.de + , otpSetup = Messages.Comp.OtpSetup.de , userSettings = "Benutzereinstellung" , uiSettings = "Oberfläche" , notifications = "Benachrichtigungen" @@ -122,4 +128,5 @@ E-Mail-Einstellungen (IMAP) notwendig.""" ist es gut, die Kriterien so zu gestalten, dass die gleichen E-Mails möglichst nicht noch einmal eingelesen werden.""" + , otpMenu = "Zwei Faktor Auth" } diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm index 17d4aeaf..07856053 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm @@ -16,6 +16,7 @@ import Comp.ChangePasswordForm import Comp.EmailSettingsManage import Comp.ImapSettingsManage import Comp.NotificationManage +import Comp.OtpSetup import Comp.ScanMailboxManage import Comp.UiSettingsManage import Data.Flags exposing (Flags) @@ -30,6 +31,7 @@ type alias Model = , notificationModel : Comp.NotificationManage.Model , scanMailboxModel : Comp.ScanMailboxManage.Model , uiSettingsModel : Comp.UiSettingsManage.Model + , otpSetupModel : Comp.OtpSetup.Model } @@ -38,6 +40,9 @@ init flags settings = let ( um, uc ) = Comp.UiSettingsManage.init flags settings + + ( otpm, otpc ) = + Comp.OtpSetup.init flags in ( { currentTab = Just UiSettingsTab , changePassModel = Comp.ChangePasswordForm.emptyModel @@ -46,8 +51,12 @@ init flags settings = , notificationModel = Tuple.first (Comp.NotificationManage.init flags) , scanMailboxModel = Tuple.first (Comp.ScanMailboxManage.init flags) , uiSettingsModel = um + , otpSetupModel = otpm } - , Cmd.map UiSettingsMsg uc + , Cmd.batch + [ Cmd.map UiSettingsMsg uc + , Cmd.map OtpSetupMsg otpc + ] ) @@ -58,6 +67,7 @@ type Tab | NotificationTab | ScanMailboxTab | UiSettingsTab + | OtpTab type Msg @@ -68,5 +78,6 @@ type Msg | ImapSettingsMsg Comp.ImapSettingsManage.Msg | ScanMailboxMsg Comp.ScanMailboxManage.Msg | UiSettingsMsg Comp.UiSettingsManage.Msg + | OtpSetupMsg Comp.OtpSetup.Msg | UpdateSettings | ReceiveBrowserSettings StoredUiSettings diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm index 925d3b57..6d200376 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm @@ -11,6 +11,7 @@ import Comp.ChangePasswordForm import Comp.EmailSettingsManage import Comp.ImapSettingsManage import Comp.NotificationManage +import Comp.OtpSetup import Comp.ScanMailboxManage import Comp.UiSettingsManage import Data.Flags exposing (Flags) @@ -79,6 +80,9 @@ update flags settings msg model = UiSettingsTab -> UpdateResult m Cmd.none Sub.none Nothing + OtpTab -> + UpdateResult m Cmd.none Sub.none Nothing + ChangePassMsg m -> let ( m2, c2 ) = @@ -145,6 +149,17 @@ update flags settings msg model = , newSettings = res.newSettings } + OtpSetupMsg lm -> + let + ( otpm, otpc ) = + Comp.OtpSetup.update flags lm model.otpSetupModel + in + { model = { model | otpSetupModel = otpm } + , cmd = Cmd.map OtpSetupMsg otpc + , sub = Sub.none + , newSettings = Nothing + } + UpdateSettings -> update flags settings diff --git a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm index 1c058f4e..6bb22ea4 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm @@ -11,6 +11,7 @@ import Comp.ChangePasswordForm import Comp.EmailSettingsManage import Comp.ImapSettingsManage import Comp.NotificationManage +import Comp.OtpSetup import Comp.ScanMailboxManage import Comp.UiSettingsManage import Data.Flags exposing (Flags) @@ -104,6 +105,17 @@ viewSidebar texts visible _ _ model = [ class "ml-3" ] [ text texts.changePassword ] ] + , a + [ href "#" + , onClick (SetTab OtpTab) + , menuEntryActive model OtpTab + , class S.sidebarLink + ] + [ i [ class "fa fa-key" ] [] + , span + [ class "ml-3" ] + [ text texts.otpMenu ] + ] ] ] @@ -133,6 +145,9 @@ viewContent texts flags settings model = Just UiSettingsTab -> viewUiSettings texts flags settings model + Just OtpTab -> + viewOtpSetup texts settings model + Nothing -> [] ) @@ -151,6 +166,25 @@ menuEntryActive model tab = class "" +viewOtpSetup : Texts -> UiSettings -> Model -> List (Html Msg) +viewOtpSetup texts _ model = + [ h2 + [ class S.header1 + , class "inline-flex items-center" + ] + [ i [ class "fa fa-key" ] [] + , div [ class "ml-3" ] + [ text texts.otpMenu + ] + ] + , Html.map OtpSetupMsg + (Comp.OtpSetup.view + texts.otpSetup + model.otpSetupModel + ) + ] + + viewChangePassword : Texts -> Model -> List (Html Msg) viewChangePassword texts model = [ h2