matterhorn-31000.0.0: src/Login.hs
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RankNTypes #-}
module Login
( interactiveGatherCredentials
) where
import Prelude ()
import Prelude.Compat
import Brick
import Brick.Widgets.Edit
import Brick.Widgets.Center
import Brick.Widgets.Border
import Control.Monad.IO.Class (liftIO)
import Data.Char (isNumber)
import Data.Maybe (isNothing)
import Data.Monoid ((<>))
import Text.Read (readMaybe)
import Lens.Micro.Platform
import qualified Data.Text as T
import Graphics.Vty hiding (Config)
import System.Exit (exitSuccess)
import Network.Mattermost.Exceptions (LoginFailureException(..))
import Config
import Markdown
import Types (ConnectionInfo(..), AuthenticationException(..))
data Name = Hostname | Port | Username | Password deriving (Ord, Eq, Show)
data State =
State { _hostnameEdit :: Editor T.Text Name
, _portEdit :: Editor T.Text Name
, _usernameEdit :: Editor T.Text Name
, _passwordEdit :: Editor T.Text Name
, _focus :: Name
, _previousError :: Maybe AuthenticationException
}
makeLenses ''State
toPassword :: [T.Text] -> Widget a
toPassword s = txt $ T.replicate (T.length $ T.concat s) "*"
validHostname :: State -> Bool
validHostname st =
all (flip notElem (":/"::String)) $ T.unpack $ T.concat $ getEditContents $ st^.hostnameEdit
validPort :: State -> Bool
validPort st =
all isNumber $ T.unpack $ T.concat $ getEditContents $ st^.portEdit
interactiveGatherCredentials :: Config
-> Maybe AuthenticationException
-> IO ConnectionInfo
interactiveGatherCredentials config authError = do
let state = newState config authError
finalSt <- defaultMain app state
let finalH = T.concat $ getEditContents $ finalSt^.hostnameEdit
finalPort = read $ T.unpack $ T.concat $ getEditContents $ finalSt^.portEdit
finalU = T.concat $ getEditContents $ finalSt^.usernameEdit
finalPass = T.concat $ getEditContents $ finalSt^.passwordEdit
return $ ConnectionInfo finalH finalPort finalU finalPass
newState :: Config -> Maybe AuthenticationException -> State
newState config authError = state
where
state = State { _hostnameEdit = editor Hostname (Just 1) hStr
, _portEdit = editor Port (Just 1) (T.pack $ show $ configPort config)
, _usernameEdit = editor Username (Just 1) uStr
, _passwordEdit = editor Password (Just 1) pStr
, _focus = initialFocus
, _previousError = authError
}
hStr = maybe "" id $ configHost config
uStr = maybe "" id $ configUser config
pStr = case configPass config of
Just (PasswordString s) -> s
_ -> ""
initialFocus = if | T.null hStr -> Hostname
| T.null uStr -> Username
| T.null pStr -> Password
| otherwise -> Hostname
app :: App State e Name
app = App
{ appDraw = credsDraw
, appChooseCursor = showFirstCursor
, appHandleEvent = onEvent
, appStartEvent = return
, appAttrMap = const colorTheme
}
errorAttr :: AttrName
errorAttr = "errorMessage"
colorTheme :: AttrMap
colorTheme = attrMap defAttr
[ (editAttr, black `on` white)
, (editFocusedAttr, black `on` yellow)
, (errorAttr, fg red)
]
credsDraw :: State -> [Widget Name]
credsDraw st =
[ center (credentialsForm st <=> errorMessageDisplay st)
]
errorMessageDisplay :: State -> Widget Name
errorMessageDisplay st = do
case st^.previousError of
Nothing -> emptyWidget
Just e ->
hCenter $ hLimit uiWidth $
padTop (Pad 1) $ renderError $ renderText $
"Error: " <> renderAuthError e
renderAuthError :: AuthenticationException -> T.Text
renderAuthError (ConnectError _) =
"Could not connect to server"
renderAuthError (ResolveError _) =
"Could not resolve server hostname"
renderAuthError (OtherAuthError e) =
T.pack $ show e
renderAuthError (LoginError (LoginFailureException msg)) =
T.pack msg
renderError :: Widget a -> Widget a
renderError = withDefAttr errorAttr
uiWidth :: Int
uiWidth = 50
credentialsForm :: State -> Widget Name
credentialsForm st =
hCenter $ hLimit uiWidth $ vLimit 15 $
border $
vBox [ renderText "Please enter your Mattermost credentials to log in."
, padTop (Pad 1) $
txt "Hostname: " <+> renderEditor (txt . T.concat) (st^.focus == Hostname) (st^.hostnameEdit)
, if validHostname st
then txt " "
else hCenter $ renderError $ txt "Invalid hostname"
, txt "Port: " <+> renderEditor (txt . T.concat) (st^.focus == Port) (st^.portEdit)
, if validPort st
then txt " "
else hCenter $ renderError $ txt "Invalid port"
, txt "Username: " <+> renderEditor (txt . T.concat) (st^.focus == Username) (st^.usernameEdit)
, padTop (Pad 1) $
txt "Password: " <+> renderEditor toPassword (st^.focus == Password) (st^.passwordEdit)
, padTop (Pad 1) $
hCenter $ renderText "Press Enter to log in or Esc to exit."
]
onEvent :: State -> BrickEvent Name e -> EventM Name (Next State)
onEvent _ (VtyEvent (EvKey KEsc [])) = liftIO exitSuccess
onEvent st (VtyEvent (EvKey (KChar '\t') [])) =
continue $ st & focus %~ nextFocus
onEvent st (VtyEvent (EvKey KEnter [])) =
if badState st then continue st else halt st
onEvent st (VtyEvent e) = do
let target :: Lens' State (Editor T.Text Name)
target = getFocusedEditor st
continue =<< handleEventLensed st target handleEditorEvent e
onEvent st _ = continue st
nextFocus :: Name -> Name
nextFocus Hostname = Port
nextFocus Port = Username
nextFocus Username = Password
nextFocus Password = Hostname
getFocusedEditor :: State -> Lens' State (Editor T.Text Name)
getFocusedEditor st =
case st^.focus of
Hostname -> hostnameEdit
Port -> portEdit
Username -> usernameEdit
Password -> passwordEdit
badState :: State -> Bool
badState st = bad
where
-- check for valid (non-empty) contents
h = T.concat $ getEditContents $ st^.hostnameEdit
u = T.concat $ getEditContents $ st^.usernameEdit
p = T.concat $ getEditContents $ st^.passwordEdit
port :: Maybe Int
port = readMaybe (T.unpack $ T.concat $ getEditContents $ st^.portEdit)
bad = or [ T.null h
, T.null u
, T.null p
, isNothing port
, (not $ validHostname st)
, (not $ validPort st)
]