halogen-websocket-ipc-playzone/src/App/AuthenticationForm.purs

387 lines
14 KiB
Plaintext
Raw Normal View History

2023-05-22 02:11:40 +02:00
module App.AuthenticationForm where
import Prelude (Unit, Void, bind, discard, map, show, void, when, ($), (&&), (<$>), (<<<), (<>), (>>=))
2023-05-22 02:11:40 +02:00
2023-06-08 21:51:12 +02:00
import Bulma as Bulma
2023-05-22 02:11:40 +02:00
import Control.Monad.State (class MonadState)
import Data.Tuple (Tuple(..))
2023-05-22 02:11:40 +02:00
import Data.Const (Const)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..), isJust, isNothing, maybe)
import Data.String as String
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Web.Event.Event (Event)
import Web.Event.Event as Event
import Web.Socket.ReadyState (ReadyState(Connecting, Open, Closing, Closed))
import Web.Socket.WebSocket as WS
import Effect.Class (class MonadEffect)
import App.Utils
import App.IPC as IPC
import App.Email as Email
2023-05-22 02:11:40 +02:00
import App.Messages.AuthenticationDaemon as AuthD
import Data.ArrayBuffer.Types (ArrayBuffer)
import Web.Socket.BinaryType (BinaryType(ArrayBuffer))
--------------------------------------------------------------------------------
-- Root component module
--------------------------------------------------------------------------------
2023-06-08 21:51:12 +02:00
data Output = AuthToken (Tuple Int String)
type Slot = H.Slot Query Output
2023-05-22 02:11:40 +02:00
type Query :: forall k. k -> Type
type Query = Const Void
type Input = String
2023-06-04 01:24:05 +02:00
data AuthenticationInput
= AUTH_INP_login String
| AUTH_INP_pass String
data RegisterInput
= REG_INP_login String
| REG_INP_email String
| REG_INP_pass String
2023-05-22 02:11:40 +02:00
data Action
= Initialize
| WebSocketParseError String
| ConnectWebSocket
2023-06-03 00:54:18 +02:00
2023-06-04 01:24:05 +02:00
| HandleAuthenticationInput AuthenticationInput
| HandleRegisterInput RegisterInput
2023-05-22 02:11:40 +02:00
| AuthenticationAttempt Event
2023-06-03 00:54:18 +02:00
| RegisterAttempt Event
| Finalize
2023-05-22 02:11:40 +02:00
| HandleWebSocket (WebSocketEvent WebSocketMessageType)
2023-06-04 01:24:05 +02:00
type StateAuthenticationForm = { login :: String, pass :: String }
type StateRegistrationForm = { login :: String, email :: String, pass :: String }
type WSInfo
= { url :: String
, connection :: Maybe WS.WebSocket
, reconnect :: Boolean
}
2023-05-22 02:11:40 +02:00
type State =
2023-06-03 00:54:18 +02:00
{ messages :: Array String
, messageHistoryLength :: Int
, authenticationForm :: StateAuthenticationForm
, registrationForm :: StateRegistrationForm
, wsInfo :: WSInfo
2023-05-22 02:11:40 +02:00
}
component :: forall m. MonadAff m => H.Component Query Input Output m
component =
2023-05-22 02:11:40 +02:00
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ initialize = Just Initialize
, handleAction = handleAction
, finalize = Just Finalize
2023-05-22 02:11:40 +02:00
}
}
initialState :: Input -> State
initialState input =
{ messages: []
, messageHistoryLength: 10
2023-06-03 00:54:18 +02:00
2023-06-04 01:24:05 +02:00
, authenticationForm: { login: "", pass: "" }
, registrationForm: { login: "", email: "", pass: "" }
2023-06-03 00:54:18 +02:00
, wsInfo: { url: input
, connection: Nothing
, reconnect: false
}
2023-05-22 02:11:40 +02:00
}
render :: forall m. State -> H.ComponentHTML Action () m
2023-06-03 00:54:18 +02:00
render {
messages,
wsInfo,
2023-06-03 00:54:18 +02:00
2023-06-04 01:24:05 +02:00
authenticationForm,
registrationForm }
2023-06-08 21:51:12 +02:00
= HH.div_
[ Bulma.columns_ [ Bulma.column_ auth_form, Bulma.column_ register_form ]
2023-06-03 00:54:18 +02:00
, render_messages
, renderReconnectButton (isNothing wsInfo.connection && wsInfo.reconnect)
2023-06-03 00:54:18 +02:00
]
where
2023-06-08 21:51:12 +02:00
auth_form
= [ Bulma.h3 "Authentication"
, render_auth_form
]
register_form
= [ Bulma.h3 "Register!"
, render_register_form
]
2023-06-03 00:54:18 +02:00
render_auth_form = HH.form
[ HE.onSubmit AuthenticationAttempt ]
2023-06-09 00:28:03 +02:00
[ Bulma.box_input "Login" "login" -- title, placeholder
(HandleAuthenticationInput <<< AUTH_INP_login) -- action
authenticationForm.login -- value
true -- validity (TODO)
(maybe (HP.disabled true) (\_ -> HP.enabled true) wsInfo.connection) -- condition
2023-06-09 00:28:03 +02:00
, Bulma.box_password "Password" "password" -- title, placeholder
(HandleAuthenticationInput <<< AUTH_INP_pass) -- action
authenticationForm.pass -- value
true -- validity (TODO)
(maybe (HP.disabled true) (\_ -> HP.enabled true) wsInfo.connection) -- condition
, HH.button
[ HP.style "padding: 0.5rem 1.25rem;"
, HP.type_ HP.ButtonSubmit
, maybe (HP.disabled true) (\_ -> HP.enabled true) wsInfo.connection
]
[ HH.text "Send Message to Server" ]
2023-05-22 02:11:40 +02:00
]
2023-06-03 00:54:18 +02:00
render_register_form = HH.form
[ HE.onSubmit RegisterAttempt ]
[ Bulma.box_input "Login" "login" -- title, placeholder
(HandleRegisterInput <<< REG_INP_login) -- action
registrationForm.login -- value
true -- validity (TODO)
(maybe (HP.disabled true) (\_ -> HP.enabled true) wsInfo.connection) -- condition
, Bulma.box_input "Email" "email@example.com" -- title, placeholder
(HandleRegisterInput <<< REG_INP_email) -- action
registrationForm.email -- value
true -- validity (TODO)
(maybe (HP.disabled true) (\_ -> HP.enabled true) wsInfo.connection) -- condition
, Bulma.box_password "Password" "password" -- title, placeholder
(HandleRegisterInput <<< REG_INP_pass) -- action
registrationForm.pass -- value
true -- validity (TODO)
(maybe (HP.disabled true) (\_ -> HP.enabled true) wsInfo.connection) -- condition
, HH.div_
[ HH.button
[ HP.style "padding: 0.5rem 1.25rem;"
, HP.type_ HP.ButtonSubmit
, maybe (HP.disabled true) (\_ -> HP.enabled true) wsInfo.connection
2023-06-03 00:54:18 +02:00
]
[ HH.text "Send Message to Server" ]
2023-06-03 00:54:18 +02:00
]
2023-05-22 02:11:40 +02:00
]
2023-06-03 00:54:18 +02:00
render_messages = HH.ul_ $ map (\msg -> HH.li_ [ HH.text msg ]) messages
renderFootnote :: String -> H.ComponentHTML Action () m
renderFootnote txt =
HH.div [ HP.style "margin-bottom: 0.125rem; color: grey;" ] [ HH.small_ [ HH.text txt ] ]
renderReconnectButton :: Boolean -> H.ComponentHTML Action () m
renderReconnectButton cond =
if cond
then
HH.p_
[ HH.button
[ HP.type_ HP.ButtonButton
, HE.onClick \_ -> ConnectWebSocket
]
[ HH.text "Reconnect?" ]
]
else
HH.p_
[ renderFootnote "NOTE: A 'Reconnect?' button will appear if the connection drops"
]
2023-05-22 02:11:40 +02:00
handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit
handleAction = case _ of
Initialize ->
handleAction ConnectWebSocket
Finalize -> do
{ wsInfo } <- H.get
systemMessage "Finalize"
case wsInfo.connection of
Nothing -> systemMessage "No socket? How is that even possible?"
Just socket -> H.liftEffect $ WS.close socket
2023-05-22 02:11:40 +02:00
WebSocketParseError error ->
systemMessage $ renderError (UnknownError error)
ConnectWebSocket -> do
{ wsInfo } <- H.get
systemMessage ("Connecting to \"" <> wsInfo.url <> "\"...")
webSocket <- H.liftEffect $ WS.create wsInfo.url []
2023-05-22 02:11:40 +02:00
H.liftEffect $ WS.setBinaryType webSocket ArrayBuffer
H.modify_ _ { wsInfo { connection = Just webSocket }}
2023-05-22 02:11:40 +02:00
void $ H.subscribe (HandleWebSocket <$> webSocketEmitter webSocket)
2023-06-04 01:24:05 +02:00
HandleAuthenticationInput authinp -> do
case authinp of
AUTH_INP_login v -> H.modify_ _ { authenticationForm { login = v } }
AUTH_INP_pass v -> H.modify_ _ { authenticationForm { pass = v } }
HandleRegisterInput reginp -> do
case reginp of
REG_INP_login v -> H.modify_ _ { registrationForm { login = v } }
REG_INP_email v -> H.modify_ _ { registrationForm { email = v } }
REG_INP_pass v -> H.modify_ _ { registrationForm { pass = v } }
2023-06-03 00:54:18 +02:00
RegisterAttempt ev -> do
H.liftEffect $ Event.preventDefault ev
{ wsInfo, registrationForm } <- H.get
2023-06-04 01:24:05 +02:00
let login = registrationForm.login
email = registrationForm.email
pass = registrationForm.pass
case wsInfo.connection, login, email, pass of
Nothing, _, _, _ ->
unableToSend "Not connected to server."
Just _, "", _, _ ->
unableToSend "Write your login!"
Just _, _, "", _ ->
unableToSend "Write your email!"
Just _, _, _, "" ->
unableToSend "Write your password!"
2023-06-04 01:24:05 +02:00
Just webSocket, _, _, _ -> do
H.liftEffect (WS.readyState webSocket) >>= case _ of
Connecting ->
unableToSend "Still connecting to server."
Closing ->
unableToSend "Connection to server is closing."
Closed -> do
unableToSend "Connection to server has been closed."
maybeCurrentConnection <- H.gets _.wsInfo.connection
when (isJust maybeCurrentConnection) do
H.modify_ _ { wsInfo { connection = Nothing, reconnect = true }}
Open -> do
H.liftEffect $ do
ab <- AuthD.serialize $ AuthD.MkRegister { login: login
, email: Just (Email.Email email)
, password: pass }
sendArrayBuffer webSocket ab
2023-07-03 13:38:21 +02:00
appendMessage "[😇] Trying to register"
2023-06-03 00:54:18 +02:00
2023-05-22 02:11:40 +02:00
AuthenticationAttempt ev -> do
H.liftEffect $ Event.preventDefault ev
{ wsInfo, authenticationForm } <- H.get
2023-05-22 02:11:40 +02:00
case wsInfo.connection, authenticationForm.login, authenticationForm.pass of
2023-05-22 02:11:40 +02:00
Nothing, _, _ ->
unableToSend "Not connected to server."
Just _ , "" , _ ->
unableToSend "Write your login!"
Just _ , _ , "" ->
unableToSend "Write your password!"
Just webSocket, login, pass -> do
H.liftEffect (WS.readyState webSocket) >>= case _ of
Connecting ->
unableToSend "Still connecting to server."
Closing ->
unableToSend "Connection to server is closing."
Closed -> do
unableToSend "Connection to server has been closed."
maybeCurrentConnection <- H.gets _.wsInfo.connection
2023-05-22 02:11:40 +02:00
when (isJust maybeCurrentConnection) do
H.modify_ _ { wsInfo { connection = Nothing, reconnect = true }}
2023-05-22 02:11:40 +02:00
Open -> do
H.liftEffect $ do
2023-06-10 18:30:31 +02:00
ab <- AuthD.serialize (AuthD.MkLogin { login: login, password: pass })
2023-05-22 02:11:40 +02:00
sendArrayBuffer webSocket ab
2023-07-03 13:38:21 +02:00
appendMessage $ "[😇] Trying to connect with login: " <> login
2023-05-22 02:11:40 +02:00
HandleWebSocket wsEvent ->
case wsEvent of
WebSocketMessage messageEvent -> do
receivedMessage <- H.liftEffect $ AuthD.deserialize messageEvent.message
case receivedMessage of
-- Cases where we didn't understand the message.
Left err -> do
case err of
2023-05-25 00:07:59 +02:00
(AuthD.JSONERROR jerr) -> do
print_json_string messageEvent.message
2023-05-25 00:07:59 +02:00
handleAction $ WebSocketParseError ("JSON parsing error: " <> jerr <> " JSON is: " <> jerr)
(AuthD.UnknownError unerr) -> handleAction $ WebSocketParseError ("Parsing error: AuthD.UnknownError" <> (show unerr))
(AuthD.UnknownNumber ) -> handleAction $ WebSocketParseError ("Parsing error: AuthD.UnknownNumber")
-- Cases where we understood the message.
2023-05-22 02:11:40 +02:00
Right response -> do
case response of
-- The authentication failed.
(AuthD.GotError errmsg) -> do
appendMessage $ "[😈] Failed: " <> maybe "server didn't tell why" (\v -> v) errmsg.reason
-- The authentication was a success!
(AuthD.GotToken msg) -> do
2023-05-22 02:11:40 +02:00
appendMessage $ "[😈] Success! user " <> (show msg.uid) <> " has token: " <> msg.token
2023-06-08 21:51:12 +02:00
H.raise $ AuthToken (Tuple msg.uid msg.token)
-- WTH?!
2023-05-22 02:11:40 +02:00
_ -> do
2023-06-03 00:54:18 +02:00
appendMessage $ "[😈] Failed! Authentication server didn't send a valid message."
2023-05-22 02:11:40 +02:00
WebSocketOpen -> do
{ wsInfo } <- H.get
systemMessage ("Successfully connected to WebSocket at \"" <> wsInfo.url <> "\"!🎉")
2023-05-22 02:11:40 +02:00
WebSocketClose { code, reason, wasClean } -> do
systemMessage $ renderCloseMessage code wasClean reason
maybeCurrentConnection <- H.gets _.wsInfo.connection
2023-05-22 02:11:40 +02:00
when (isJust maybeCurrentConnection) do
H.modify_ _ { wsInfo { connection = Nothing, reconnect = true }}
2023-05-22 02:11:40 +02:00
WebSocketError errorType ->
systemMessage $ renderError errorType
where
renderCloseMessage
:: Int
-> Boolean
-> String
-> String
renderCloseMessage code wasClean = case _ of
"" -> baseCloseMessage
reason -> baseCloseMessage <> "Reason: " <> reason
where
baseCloseMessage :: String
baseCloseMessage =
String.joinWith " "
[ "Connection to WebSocket closed"
, "[ CODE:"
, show code
, "|"
, if wasClean then "CLEAN" else "DIRTY"
, "]"
]
print_json_string :: forall m. MonadEffect m => MonadState State m => ArrayBuffer -> m Unit
print_json_string arraybuffer = do
-- fromTypedIPC :: ArrayBuffer -> Effect (Either ParseError (Tuple UInt String))
value <- H.liftEffect $ IPC.fromTypedIPC arraybuffer
appendMessage $ case (value) of
Left _ -> "Cannot even fromTypedIPC the message."
2023-05-23 02:50:08 +02:00
Right (Tuple messageTypeNumber string) -> "Number is: " <> show messageTypeNumber <> ", received string: " <> string