O2BZOX7MS4JCDS3C6EJQXAWUEQV6HVDCIF2FIN2BCJNRLIU6ZVKAC
UWMGUJOW5X5HQTS76T2FD7MNAJF7SESPQVU5FDIZO52V75TT2X6AC
SAESJLLYCQJUIHKFYFV53AWHFOSGI5SKLVS7DPTQO6BKGITPYPUQC
WZFQDWW4XK6M4A4PQ7WQJUTZUPRGQR7V7ZVZY5ZTL5AMGIFMHB2QC
64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC
TCOAKCGGHOIRJCTZYEZQ3K6KCGL2LGAYGYFRGSPCHBTJJY2V6AXAC
NJNMO72S7VIUV22JXB4IFPZMHWTJAOTP6EC6Z4QSKYIROSXT52MQC
WRPIYG3EUHZR6N6T74ZXZDXATRMIRLXAQ24UNUNSVTVYGMT2VDSQC
RSF6UAJKG7CEKILSVXI6C4YZXY7PIYZM2EMA2IXKQ7SADKNVSH7QC
EA5BFM5GMM7KNMDLTVOSUKVKMSIDD72TAFVHDVGEOUY5VELECU3QC
JXG3FCXYBDKMUD77DOM7RCIJYKB7BILC43OHHDZBE7YQRGAMUCCAC
QMEYU4MWLTSWPWEEOFRLK2IKE64BY3V5X73323WPLCGPP3TPDYGAC
PT4276XCOP5NJ3GRFJLIBZKVNVAOATAY5PLWV7FWK6RZW5FTEP5AC
3LMXT7Z6SIGLQ2OMH7OKPJPWNPN2CSGD3BKUD2NMJVCX2CSAMFYQC
TUA4HMUDRRXLVOH4WPID2ZJGEIJTSCMM5OBP3E26ECYHSHG3IBDQC
TKGBRIQT7XCPJ3LA5JAEMMGMPFWQWINMSDRW76V2IMZZGT5AWTYAC
5R2Z7FSXJD7Z53QSU2NSTEBONTYK43FIJOSOMUST5XMYIWRXY2HQC
RB2ETNIFLQUA6OA66DAEOXZ25ENMQGNKX5CZRSKEYHTD6BQ6NTFQC
4R7XIYK3BP664CO3YJ2VM64ES2JYN27UTQG5KS34OTEPAIODSZLQC
6L5BK5EHPAOQX3JCKUJ273UDNAC23LPQL4HIJGM4AV3P3QK5OKIQC
V2VDN77HCSRYYWXDJJ2XOVHV4P6PVWNJZLXZ7JUYPQEZQIH5BZ3QC
EFSXYZPOGA5M4DN65IEIDO7JK6U34DMQELAPHOIL2UAT6HRC66BAC
2XQD6KKKD6QVHFHAEMVE3XXY7X2T7BLTLL7XIILZAXNJJH2YONUQC
PBD7LZYQHXAA3KLH2ZUX5GW4UFML6BQ32KXZF4KZ6OYFASUYFJ5QC
BROSTG5KP3NUNLSYPVQID254TE47E5RKQAKLPIY7PGWETE6JNMTAC
I2KHGVD44KT4MQJXGCTVSQKMBO6TVCY72F26TLXGWRL6PHGF6RNQC
MB5SHULBN3WP7TGUWZDP6BRGP423FTYKF67T5IF5YHHLNXKQ5REAC
NEDDHXUK3GNFMOFO3KLU7NRIHCTYNWBT3D6HTKZAOXFDE6HMDZ6AC
QMRKFEPGFBCEWAIXPEIG5ILKAJ2JH5L3TOITHR4HNJXK5FN3KXBQC
BXGLKYRXO2O4NRM3BLNWQ7AWVPQXAMFS57MFYHJNOZZEZZW5BH6AC
ADMKQQGCGVSHHIMVQ4XFRDCG544SBJCYALSKZV45CQQBZ4ACUH2AC
module Aftok.Api.Account where
import Prelude
import Control.Monad.Trans.Class (lift)
import Data.Argonaut.Encode (encodeJson)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Effect.Class.Console (log)
import Affjax (post, get, printError)
import Affjax.StatusCode (StatusCode(..))
import Affjax.RequestBody as RB
import Affjax.ResponseFormat as RF
import Halogen as H
import Halogen.HTML.Core (ClassName(..))
import Halogen.HTML as HH
import Halogen.HTML.CSS as CSS
import Halogen.HTML.Events as E
import Web.Event.Event as WE
import Halogen.HTML.Properties as P
import CSS (backgroundImage, url)
import Landkit.Card as Card
import Aftok.Types (System)
type LoginRequest = { username :: String, password :: String }
data LoginResponse
= LoginOK
| LoginForbidden
| LoginError { status :: Maybe StatusCode, message :: String }
-- | Post credentials to the login service and interpret the response
login :: String -> String -> Aff LoginResponse
login user pass = do
log "Sending login request to /api/login ..."
result <- post RF.ignore "/api/login" (Just <<< RB.Json <<< encodeJson $ { username: user, password : pass })
case result of
Left err -> log ("Login failed: " <> printError err)
Right r -> log ("Login status: " <> show r.status)
pure $ case result of
Left err -> LoginError { status: Nothing, message: printError err }
Right r -> case r.status of
StatusCode 403 -> LoginForbidden
StatusCode 200 -> LoginOK
other -> LoginError { status: Just other, message: r.statusText }
checkLogin :: Aff LoginResponse
checkLogin = do
result <- get RF.ignore "/api/login/check"
case result of
Left err -> do
pure $ LoginError { status: Nothing, message: printError err }
Right r -> do
pure $ case r.status of
StatusCode 200 -> LoginOK
StatusCode _ -> LoginForbidden
logout :: Aff Unit
logout = void $ get RF.ignore "/api/logout"
data RecoverBy
= RecoverByEmail String
| RecoverByZAddr String
type SignupRequest =
{ username :: String
, password :: String
, recoverBy :: RecoverBy
, captchaToken :: String
}
signupRequest :: String -> String -> RecoverBy -> String -> SignupRequest
signupRequest username password recoverBy captchaToken =
{ username, password, recoverBy, captchaToken }
data SignupResponse
= SignupOK
| CaptchaInvalid
| ZAddrInvalid
| UsernameTaken
data UsernameCheckResponse
= UsernameCheckOK
| UsernameCheckTaken
data ZAddrCheckResponse
= ZAddrCheckOK
| ZAddrCheckInvalid
checkUsername :: String -> Aff UsernameCheckResponse
checkUsername uname = do
pure UsernameCheckOK
checkZAddr :: String -> Aff ZAddrCheckResponse
checkZAddr zaddr = do
pure ZAddrCheckOK
signup :: SignupRequest -> Aff SignupResponse
signup req = do
pure SignupOK
"use strict";
exports.getRecaptchaResponseInternal = useElemId => elemId => () => {
if (useElemId) {
return grecaptcha.getResponse(elemId);
} else {
return grecaptcha.getResponse();
}
}
module Aftok.Api.Recaptcha
( getRecaptchaResponse
) where
import Prelude (bind, (==), ($), pure)
import Data.Maybe (Maybe(..))
import Effect (Effect)
getRecaptchaResponse :: Maybe String -> Effect (Maybe String)
getRecaptchaResponse elemId = do
resp <- case elemId of
Just eid -> getRecaptchaResponseInternal true eid
Nothing -> getRecaptchaResponseInternal false ""
pure $ if resp == "" then Nothing else Just resp
foreign import getRecaptchaResponseInternal :: Boolean -> String -> Effect String
import Effect.Class.Console (log)
import Affjax (post, get, printError)
import Affjax.StatusCode (StatusCode(..))
import Affjax.RequestBody as RB
import Affjax.ResponseFormat as RF
Just OK ->
HH.div
[ P.classes (ClassName <$> ["alert alert-warning"]) ]
[ HH.text "Login ok, but you should have been redirected. Why are you still here?" ]
Just Forbidden ->
HH.div
[ P.classes (ClassName <$> ["alert alert-danger"]) ]
[ HH.text "Login failed. Check your username and password." ]
Just (Error e) ->
HH.div
[ P.classes (ClassName <$> ["alert alert-danger"]) ]
[ HH.text ("Login failed: " <> e.message) ]
Just err ->
let message = case err of
Forbidden -> "Login failed. Check your username and password."
ServerError -> "Login failed due to an internal error. Please contact support."
in HH.div
[ P.classes (ClassName <$> ["alert alert-danger"]) ]
[ HH.text message ]
OK -> H.raise (LoginComplete { username: user })
_ -> pure unit
-- | Post credentials to the login service and interpret the response
login :: String -> String -> Aff LoginResponse
login user pass = do
log "Sending login request to /api/login ..."
result <- post RF.ignore "/api/login" (Just <<< RB.Json <<< encodeJson $ { username: user, password : pass })
case result of
Left err -> log ("Login failed: " <> printError err)
Right r -> log ("Login status: " <> show r.status)
pure $ case result of
Left err -> Error { status: Nothing, message: printError err }
Right r -> case r.status of
StatusCode 403 -> Forbidden
StatusCode 200 -> OK
other -> Error { status: Just other, message: r.statusText }
checkLogin :: Aff LoginResponse
checkLogin = do
log "Sending login check to /api/login/check ..."
result <- get RF.ignore "/api/login/check"
case result of
Left err -> do
log ("Login failed: " <> printError err)
pure $ Error { status: Nothing, message: printError err }
Right r -> do
log ("Login status: " <> show r.status)
pure $ case r.status of
StatusCode 200 -> OK
StatusCode _ -> Forbidden
LoginOK -> H.raise (LoginComplete { username: user })
LoginForbidden -> H.modify_ (_ { loginError = Just Forbidden })
LoginError _ -> H.modify_ (_ { loginError = Just ServerError })
data SignupResponse
= OK
| Error { status :: Maybe StatusCode, message :: String }
data SignupError
= UsernameRequired
| UsernameTaken
| PasswordRequired
| ConfirmRequired
| PasswordMismatch
| EmailRequired
| ZAddrRequired
| ZAddrInvalid
| CaptchaError
| APIError { status :: Maybe StatusCode, message :: String }
{ signup :: String -> String -> m SignupResponse
{ checkUsername :: String -> m Acc.UsernameCheckResponse
, checkZAddr :: String -> m Acc.ZAddrCheckResponse
, signup :: SignupRequest -> m SignupResponse
, getRecaptchaResponse :: Maybe String -> m (Maybe String)
SetUsername user -> H.modify_ (_ { username = Just user })
SetPassword pass -> H.modify_ (_ { password = Just pass })
SetUsername user -> do
ures <- lift $ caps.checkUsername user
H.modify_ (_ { username = Just user })
case ures of
Acc.UsernameCheckOK -> pure unit
Acc.UsernameCheckTaken -> H.modify_ (_ { signupErrors = [UsernameTaken] })
SetPassword pass -> do
H.modify_ (_ { password = Just pass })
confirm <- H.gets (_.passwordConfirm)
when (any (notEq pass) confirm) (H.modify_ (_ { signupErrors = [PasswordMismatch] }))
ConfirmPassword confirm -> do
H.modify_ (_ { passwordConfirm = Just confirm })
password <- H.gets (_.password)
when (any (notEq confirm) password) (H.modify_ (_ { signupErrors = [PasswordMismatch] }))
ConfirmPassword pass -> H.modify_ (_ { passwordConfirm = Just pass })
_ -> pure unit
SetRecoveryEmail email -> H.modify_ (_ { recoveryEmail = Just email })
SetRecoveryZAddr addr -> do
zres <- lift $ caps.checkZAddr addr
H.modify_ (_ { recoveryZAddr = Just addr })
case zres of
Acc.ZAddrCheckOK -> pure unit
Acc.ZAddrCheckInvalid -> H.modify_ (_ { signupErrors = [ZAddrInvalid] })
Signin ev -> do
lift $ system.preventDefault (ME.toEvent ev)
H.raise SigninNav
Signup ev -> do
lift $ system.preventDefault ev
recType <- H.gets (_.recoveryType)
usernameV <- V <<< note [UsernameRequired] <$> H.gets (_.username)
pwdFormV <- V <<< note [PasswordRequired] <$> H.gets (_.password)
pwdConfV <- V <<< note [ConfirmRequired ] <$> H.gets (_.passwordConfirm)
recoveryType <- H.gets (_.recoveryType)
recoveryV <- case recoveryType of
RecoveryEmail ->
V <<< note [EmailRequired] <<< map Acc.RecoverByEmail <$> H.gets (_.recoveryEmail)
RecoveryZAddr ->
V <<< note [ZAddrRequired] <<< map Acc.RecoverByZAddr <$> H.gets (_.recoveryZAddr)
recapV <- lift $ V <<< note [CaptchaError] <$> caps.getRecaptchaResponse Nothing
let reqV :: V (Array SignupError) Acc.SignupRequest
reqV = signupRequest <$> usernameV
<*> ((eq <$> pwdFormV <*> pwdConfV) `andThen`
(if _ then pwdFormV else invalid [PasswordMismatch]))
<*> recoveryV
<*> recapV
case toEither reqV of
Left errors ->
H.modify_ (_ { signupErrors = errors })
Right req -> do
response <- lift (caps.signup req)
case response of
Acc.SignupOK -> H.raise (SignupComplete $ req.username)
Acc.CaptchaInvalid -> H.modify_ (_ { signupErrors = [CaptchaError] })
Acc.ZAddrInvalid -> H.modify_ (_ { signupErrors = [ZAddrInvalid] })
Acc.UsernameTaken -> H.modify_ (_ { signupErrors = [UsernameTaken] })
data CaptchaError
= MissingInputSecret
| InvalidInputSecret
| MissingInputResponse
| InvalidInputResponse
| BadRequest
| TimeoutOrDuplicate
| CaptchaError Text
deriving (Eq, Show)
data CaptchaConfig = CaptchaConfig
{ secretKey :: Text }
data CaptchaResponse = CaptchaResponse
{ success :: Bool
, errorCodes :: [CaptchaError]
}
instance A.FromJSON CaptchaResponse where
parseJSON (A.Object v) =
CaptchaResponse <$> v .: "success"
<*> (fmap toError . join . toList <$> v .:? "error-codes")
where
toError = \case
"missing-input-secret" -> MissingInputSecret
"invalid-input-secret" -> InvalidInputSecret
"missing-input-response" -> MissingInputResponse
"invalid-input-response" -> InvalidInputResponse
"bad-request" -> BadRequest
"timeout-or-duplicate" -> TimeoutOrDuplicate
other -> CaptchaError $ "Unexpected error code: " <> other
parseJSON _ =
fail "Captcha response body was not a valid JSON object."
checkCaptcha :: CaptchaConfig -> Text -> IO CaptchaCheckResult
checkCaptcha cfg token = do
request <- parseRequest "https://www.google.com/recaptcha/api/siteverify"
reqWithBody <- formDataBody [partBS "secret" (T.encodeUtf8 $ secretKey cfg), partBS "response" (T.encodeUtf8 token)] request
manager <- newTlsManager
response <- httpLbs reqWithBody manager
pure $ case statusCode (responseStatus response) of
200 ->
case A.eitherDecode (responseBody response) of
Left err -> Left [CaptchaError $ "Failed to decode JSON response: " <> T.pack err]
Right cr -> if success cr then Right () else Left (errorCodes cr)
errCode ->
Left $ [CaptchaError $ "Unexpected status code: " <> T.pack (show errCode)]