mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Adopt login process for two-factor auth
This commit is contained in:
@ -141,6 +141,7 @@ module Api exposing
|
||||
, startReIndex
|
||||
, submitNotifyDueItems
|
||||
, toggleTags
|
||||
, twoFactor
|
||||
, unconfirmMultiple
|
||||
, updateNotifyDueItems
|
||||
, updateScanMailbox
|
||||
@ -209,6 +210,7 @@ import Api.Model.Registration exposing (Registration)
|
||||
import Api.Model.ScanMailboxSettings exposing (ScanMailboxSettings)
|
||||
import Api.Model.ScanMailboxSettingsList exposing (ScanMailboxSettingsList)
|
||||
import Api.Model.SearchStats exposing (SearchStats)
|
||||
import Api.Model.SecondFactor exposing (SecondFactor)
|
||||
import Api.Model.SentMails exposing (SentMails)
|
||||
import Api.Model.SimpleMail exposing (SimpleMail)
|
||||
import Api.Model.SourceAndTags exposing (SourceAndTags)
|
||||
@ -942,6 +944,15 @@ login flags up receive =
|
||||
}
|
||||
|
||||
|
||||
twoFactor : Flags -> SecondFactor -> (Result Http.Error AuthResult -> msg) -> Cmd msg
|
||||
twoFactor flags sf receive =
|
||||
Http.post
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/open/auth/two-factor"
|
||||
, body = Http.jsonBody (Api.Model.SecondFactor.encode sf)
|
||||
, expect = Http.expectJson receive Api.Model.AuthResult.decoder
|
||||
}
|
||||
|
||||
|
||||
logout : Flags -> (Result Http.Error () -> msg) -> Cmd msg
|
||||
logout flags receive =
|
||||
Http2.authPost
|
||||
|
@ -28,6 +28,7 @@ type alias Texts =
|
||||
, loginSuccessful : String
|
||||
, noAccount : String
|
||||
, signupLink : String
|
||||
, otpCode : String
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +46,7 @@ gb =
|
||||
, loginSuccessful = "Login successful"
|
||||
, noAccount = "No account?"
|
||||
, signupLink = "Sign up!"
|
||||
, otpCode = "Authentication code"
|
||||
}
|
||||
|
||||
|
||||
@ -62,4 +64,5 @@ de =
|
||||
, loginSuccessful = "Anmeldung erfolgreich"
|
||||
, noAccount = "Kein Konto?"
|
||||
, signupLink = "Hier registrieren!"
|
||||
, otpCode = "Authentifizierungscode"
|
||||
}
|
||||
|
@ -128,5 +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"
|
||||
, otpMenu = "Zwei-Faktor-Authentifizierung"
|
||||
}
|
||||
|
@ -6,7 +6,8 @@
|
||||
|
||||
|
||||
module Page.Login.Data exposing
|
||||
( FormState(..)
|
||||
( AuthStep(..)
|
||||
, FormState(..)
|
||||
, Model
|
||||
, Msg(..)
|
||||
, emptyModel
|
||||
@ -20,8 +21,10 @@ import Page exposing (Page(..))
|
||||
type alias Model =
|
||||
{ username : String
|
||||
, password : String
|
||||
, otp : String
|
||||
, rememberMe : Bool
|
||||
, formState : FormState
|
||||
, authStep : AuthStep
|
||||
}
|
||||
|
||||
|
||||
@ -32,12 +35,19 @@ type FormState
|
||||
| FormInitial
|
||||
|
||||
|
||||
type AuthStep
|
||||
= StepLogin
|
||||
| StepOtp AuthResult
|
||||
|
||||
|
||||
emptyModel : Model
|
||||
emptyModel =
|
||||
{ username = ""
|
||||
, password = ""
|
||||
, otp = ""
|
||||
, rememberMe = False
|
||||
, formState = FormInitial
|
||||
, authStep = StepLogin
|
||||
}
|
||||
|
||||
|
||||
@ -47,3 +57,5 @@ type Msg
|
||||
| ToggleRememberMe
|
||||
| Authenticate
|
||||
| AuthResp (Result Http.Error AuthResult)
|
||||
| SetOtp String
|
||||
| AuthOtp AuthResult
|
||||
|
@ -24,6 +24,9 @@ update referrer flags msg model =
|
||||
SetPassword str ->
|
||||
( { model | password = str }, Cmd.none, Nothing )
|
||||
|
||||
SetOtp str ->
|
||||
( { model | otp = str }, Cmd.none, Nothing )
|
||||
|
||||
ToggleRememberMe ->
|
||||
( { model | rememberMe = not model.rememberMe }, Cmd.none, Nothing )
|
||||
|
||||
@ -37,17 +40,33 @@ update referrer flags msg model =
|
||||
in
|
||||
( model, Api.login flags userPass AuthResp, Nothing )
|
||||
|
||||
AuthOtp acc ->
|
||||
let
|
||||
sf =
|
||||
{ rememberMe = model.rememberMe
|
||||
, token = Maybe.withDefault "" acc.token
|
||||
, otp = model.otp
|
||||
}
|
||||
in
|
||||
( model, Api.twoFactor flags sf AuthResp, Nothing )
|
||||
|
||||
AuthResp (Ok lr) ->
|
||||
let
|
||||
gotoRef =
|
||||
Maybe.withDefault HomePage referrer |> Page.goto
|
||||
in
|
||||
if lr.success then
|
||||
if lr.success && not lr.requireSecondFactor then
|
||||
( { model | formState = AuthSuccess lr, password = "" }
|
||||
, Cmd.batch [ setAccount lr, gotoRef ]
|
||||
, Just lr
|
||||
)
|
||||
|
||||
else if lr.success && lr.requireSecondFactor then
|
||||
( { model | formState = FormInitial, authStep = StepOtp lr, password = "" }
|
||||
, Cmd.none
|
||||
, Nothing
|
||||
)
|
||||
|
||||
else
|
||||
( { model | formState = AuthFailed lr, password = "" }
|
||||
, Ports.removeAccount ()
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
module Page.Login.View2 exposing (viewContent, viewSidebar)
|
||||
|
||||
import Api.Model.AuthResult exposing (AuthResult)
|
||||
import Api.Model.VersionInfo exposing (VersionInfo)
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
@ -46,104 +47,12 @@ viewContent texts flags versionInfo _ model =
|
||||
, div [ class "font-medium self-center text-xl sm:text-2xl" ]
|
||||
[ text texts.loginToDocspell
|
||||
]
|
||||
, Html.form
|
||||
[ action "#"
|
||||
, onSubmit Authenticate
|
||||
, autocomplete False
|
||||
]
|
||||
[ div [ class "flex flex-col mt-6" ]
|
||||
[ label
|
||||
[ for "username"
|
||||
, class S.inputLabel
|
||||
]
|
||||
[ text texts.username
|
||||
]
|
||||
, div [ class "relative" ]
|
||||
[ div [ class S.inputIcon ]
|
||||
[ i [ class "fa fa-user" ] []
|
||||
]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, name "username"
|
||||
, autocomplete False
|
||||
, onInput SetUsername
|
||||
, value model.username
|
||||
, autofocus True
|
||||
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
|
||||
, placeholder texts.collectiveSlashLogin
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "flex flex-col my-3" ]
|
||||
[ label
|
||||
[ for "password"
|
||||
, class S.inputLabel
|
||||
]
|
||||
[ text texts.password
|
||||
]
|
||||
, div [ class "relative" ]
|
||||
[ div [ class S.inputIcon ]
|
||||
[ i [ class "fa fa-lock" ] []
|
||||
]
|
||||
, input
|
||||
[ type_ "password"
|
||||
, name "password"
|
||||
, autocomplete False
|
||||
, onInput SetPassword
|
||||
, value model.password
|
||||
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
|
||||
, placeholder texts.password
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "flex flex-col my-3" ]
|
||||
[ label
|
||||
[ class "inline-flex items-center"
|
||||
, for "rememberme"
|
||||
]
|
||||
[ input
|
||||
[ id "rememberme"
|
||||
, type_ "checkbox"
|
||||
, onCheck (\_ -> ToggleRememberMe)
|
||||
, checked model.rememberMe
|
||||
, name "rememberme"
|
||||
, class S.checkboxInput
|
||||
]
|
||||
[]
|
||||
, span
|
||||
[ class "mb-1 ml-2 text-xs sm:text-sm tracking-wide my-1"
|
||||
]
|
||||
[ text texts.rememberMe
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "flex flex-col my-3" ]
|
||||
[ button
|
||||
[ type_ "submit"
|
||||
, class S.primaryButton
|
||||
]
|
||||
[ text texts.loginButton
|
||||
]
|
||||
]
|
||||
, resultMessage texts model
|
||||
, div
|
||||
[ class "flex justify-end text-sm pt-4"
|
||||
, classList [ ( "hidden", flags.config.signupMode == "closed" ) ]
|
||||
]
|
||||
[ span []
|
||||
[ text texts.noAccount
|
||||
]
|
||||
, a
|
||||
[ Page.href RegisterPage
|
||||
, class ("ml-2" ++ S.link)
|
||||
]
|
||||
[ i [ class "fa fa-user-plus mr-1" ] []
|
||||
, text texts.signupLink
|
||||
]
|
||||
]
|
||||
]
|
||||
, case model.authStep of
|
||||
StepOtp token ->
|
||||
otpForm texts flags model token
|
||||
|
||||
StepLogin ->
|
||||
loginForm texts flags model
|
||||
]
|
||||
, a
|
||||
[ class "inline-flex items-center mt-4 text-xs opacity-50 hover:opacity-90"
|
||||
@ -163,6 +72,151 @@ viewContent texts flags versionInfo _ model =
|
||||
]
|
||||
|
||||
|
||||
otpForm : Texts -> Flags -> Model -> AuthResult -> Html Msg
|
||||
otpForm texts flags model acc =
|
||||
Html.form
|
||||
[ action "#"
|
||||
, onSubmit (AuthOtp acc)
|
||||
, autocomplete False
|
||||
]
|
||||
[ div [ class "flex flex-col mt-6" ]
|
||||
[ label
|
||||
[ for "otp"
|
||||
, class S.inputLabel
|
||||
]
|
||||
[ text texts.otpCode
|
||||
]
|
||||
, div [ class "relative" ]
|
||||
[ div [ class S.inputIcon ]
|
||||
[ i [ class "fa fa-key" ] []
|
||||
]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, name "otp"
|
||||
, autocomplete False
|
||||
, onInput SetOtp
|
||||
, value model.otp
|
||||
, autofocus True
|
||||
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
|
||||
, placeholder "123456"
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div [ class "flex flex-col my-3" ]
|
||||
[ button
|
||||
[ type_ "submit"
|
||||
, class S.primaryButton
|
||||
]
|
||||
[ text texts.loginButton
|
||||
]
|
||||
]
|
||||
, resultMessage texts model
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
loginForm : Texts -> Flags -> Model -> Html Msg
|
||||
loginForm texts flags model =
|
||||
Html.form
|
||||
[ action "#"
|
||||
, onSubmit Authenticate
|
||||
, autocomplete False
|
||||
]
|
||||
[ div [ class "flex flex-col mt-6" ]
|
||||
[ label
|
||||
[ for "username"
|
||||
, class S.inputLabel
|
||||
]
|
||||
[ text texts.username
|
||||
]
|
||||
, div [ class "relative" ]
|
||||
[ div [ class S.inputIcon ]
|
||||
[ i [ class "fa fa-user" ] []
|
||||
]
|
||||
, input
|
||||
[ type_ "text"
|
||||
, name "username"
|
||||
, autocomplete False
|
||||
, onInput SetUsername
|
||||
, value model.username
|
||||
, autofocus True
|
||||
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
|
||||
, placeholder texts.collectiveSlashLogin
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "flex flex-col my-3" ]
|
||||
[ label
|
||||
[ for "password"
|
||||
, class S.inputLabel
|
||||
]
|
||||
[ text texts.password
|
||||
]
|
||||
, div [ class "relative" ]
|
||||
[ div [ class S.inputIcon ]
|
||||
[ i [ class "fa fa-lock" ] []
|
||||
]
|
||||
, input
|
||||
[ type_ "password"
|
||||
, name "password"
|
||||
, autocomplete False
|
||||
, onInput SetPassword
|
||||
, value model.password
|
||||
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
|
||||
, placeholder texts.password
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
, div [ class "flex flex-col my-3" ]
|
||||
[ label
|
||||
[ class "inline-flex items-center"
|
||||
, for "rememberme"
|
||||
]
|
||||
[ input
|
||||
[ id "rememberme"
|
||||
, type_ "checkbox"
|
||||
, onCheck (\_ -> ToggleRememberMe)
|
||||
, checked model.rememberMe
|
||||
, name "rememberme"
|
||||
, class S.checkboxInput
|
||||
]
|
||||
[]
|
||||
, span
|
||||
[ class "mb-1 ml-2 text-xs sm:text-sm tracking-wide my-1"
|
||||
]
|
||||
[ text texts.rememberMe
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "flex flex-col my-3" ]
|
||||
[ button
|
||||
[ type_ "submit"
|
||||
, class S.primaryButton
|
||||
]
|
||||
[ text texts.loginButton
|
||||
]
|
||||
]
|
||||
, resultMessage texts model
|
||||
, div
|
||||
[ class "flex justify-end text-sm pt-4"
|
||||
, classList [ ( "hidden", flags.config.signupMode == "closed" ) ]
|
||||
]
|
||||
[ span []
|
||||
[ text texts.noAccount
|
||||
]
|
||||
, a
|
||||
[ Page.href RegisterPage
|
||||
, class ("ml-2" ++ S.link)
|
||||
]
|
||||
[ i [ class "fa fa-user-plus mr-1" ] []
|
||||
, text texts.signupLink
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
resultMessage : Texts -> Model -> Html Msg
|
||||
resultMessage texts model =
|
||||
case model.formState of
|
||||
|
Reference in New Issue
Block a user