Add signup form, captcha check.
[?]
Sep 9, 2020, 5:26 AM
O2BZOX7MS4JCDS3C6EJQXAWUEQV6HVDCIF2FIN2BCJNRLIU6ZVKACDependencies
- [2]
UWMGUJOWAutoformat sources. - [3]
SAESJLLYInitial experiments in hash routing. - [4]
EA5BFM5GSplit Login component into its own module. - [5]
TKGBRIQTLogin component now raises LoginComplete message. - [6]
TUA4HMUDUse real API capability for login. - [7]
QMRKFEPGRefactor QDB to use a free monad algebra instead. - [8]
MB5SHULBAdd route for accepting an invitation with an existing account - [9]
B6HWAPDPModularize & update to recent haskoin. - [10]
JXG3FCXYUpgrade ps + halogen versions. - [11]
5R2Z7FSXInitial rendering for signup controls. - [12]
PBD7LZYQPostgres & auth are beginning to function. - [13]
I2KHGVD4Require project permissions for access to most data. - [14]
NEDDHXUKReformat via stylish-haskell - [15]
OUR4PAOTUse local dates for display of intervals. - [16]
4U7F3CPITHE GREAT RENAMING OF THINGS! - [17]
6L5BK5EHUse generic SMTP rather than Sendmail-specific mail client. - [18]
DFOBMSAOInitial work on payments API - [19]
BROSTG5KBeginning of modularization of server. - [20]
IPG33FAWAdd billing daemon - [21]
QMEYU4MWAdd display for prior intervals. - [22]
2XQD6KKKAdd invitation logic and clean up DBProg error handling. - [23]
BXGLKYRXAdded primitive user registration handler. - [24]
EFSXYZPOAutoformat everything with brittany. - [25]
WRPIYG3EUse project listing functionality to check for whether we have a cookie. - [26]
HMDM3B55Implement core of payments/billing infrastructure. - [27]
3LMXT7Z6preventDefault on login form submission. - [28]
NJNMO72SAdd zcash.com submodule and update client to modern halogen. - [29]
LTSVBVA2Update to a recent haskoin-core. Fix Stack build. - [30]
4R7XIYK3Switch from ClassyPrelude to Relude - [31]
PT4276XCAdd logout functionality. - [*]
64C6AWH6Rename Ananke -> Quixotic, project reboot. - [*]
WZFQDWW4Add retrieval/storage of current exchange rate data to payment recording. - [*]
TCOAKCGGCompleted conversion to snap. - [*]
RSF6UAJKBreak out api module for timeline. - [*]
RB2ETNIFAdd skeletal PureScript client project. - [*]
V2VDN77HEnable postgres configuration via environment variable for Heroku. - [*]
ADMKQQGCInitial empty Snap project.
Change contents
- edit in aftok.cabal at line 164
, http-client-tls - edit in aftok.cabal at line 166
, http-types - edit in client/dev/index.html at line 8
<script src="https://www.google.com/recaptcha/api.js" async defer></script> - edit in client/spago.dhall at line 12
, "validation" - file addition: Account.purs[36.1]
module Aftok.Api.Account whereimport Preludeimport 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 RBimport Affjax.ResponseFormat as RFimport Halogen as Himport Halogen.HTML.Core (ClassName(..))import Halogen.HTML as HHimport Halogen.HTML.CSS as CSSimport Halogen.HTML.Events as Eimport Web.Event.Event as WEimport Halogen.HTML.Properties as Pimport CSS (backgroundImage, url)import Landkit.Card as Cardimport 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 responselogin :: String -> String -> Aff LoginResponselogin user pass = dolog "Sending login request to /api/login ..."result <- post RF.ignore "/api/login" (Just <<< RB.Json <<< encodeJson $ { username: user, password : pass })case result ofLeft err -> log ("Login failed: " <> printError err)Right r -> log ("Login status: " <> show r.status)pure $ case result ofLeft err -> LoginError { status: Nothing, message: printError err }Right r -> case r.status ofStatusCode 403 -> LoginForbiddenStatusCode 200 -> LoginOKother -> LoginError { status: Just other, message: r.statusText }checkLogin :: Aff LoginResponsecheckLogin = doresult <- get RF.ignore "/api/login/check"case result ofLeft err -> dopure $ LoginError { status: Nothing, message: printError err }Right r -> dopure $ case r.status ofStatusCode 200 -> LoginOKStatusCode _ -> LoginForbiddenlogout :: Aff Unitlogout = void $ get RF.ignore "/api/logout"data RecoverBy= RecoverByEmail String| RecoverByZAddr Stringtype SignupRequest ={ username :: String, password :: String, recoverBy :: RecoverBy, captchaToken :: String}signupRequest :: String -> String -> RecoverBy -> String -> SignupRequestsignupRequest username password recoverBy captchaToken ={ username, password, recoverBy, captchaToken }data SignupResponse= SignupOK| CaptchaInvalid| ZAddrInvalid| UsernameTakendata UsernameCheckResponse= UsernameCheckOK| UsernameCheckTakendata ZAddrCheckResponse= ZAddrCheckOK| ZAddrCheckInvalidcheckUsername :: String -> Aff UsernameCheckResponsecheckUsername uname = dopure UsernameCheckOKcheckZAddr :: String -> Aff ZAddrCheckResponsecheckZAddr zaddr = dopure ZAddrCheckOKsignup :: SignupRequest -> Aff SignupResponsesignup req = dopure SignupOK - file addition: Recaptcha.js[36.1]
"use strict";exports.getRecaptchaResponseInternal = useElemId => elemId => () => {if (useElemId) {return grecaptcha.getResponse(elemId);} else {return grecaptcha.getResponse();}} - file addition: Recaptcha.purs[36.1]
module Aftok.Api.Recaptcha( getRecaptchaResponse) whereimport Prelude (bind, (==), ($), pure)import Data.Maybe (Maybe(..))import Effect (Effect)getRecaptchaResponse :: Maybe String -> Effect (Maybe String)getRecaptchaResponse elemId = doresp <- case elemId ofJust eid -> getRecaptchaResponseInternal true eidNothing -> getRecaptchaResponseInternal false ""pure $ if resp == "" then Nothing else Just respforeign import getRecaptchaResponseInternal :: Boolean -> String -> Effect String - edit in client/src/Aftok/Login.purs at line 7
import Data.Argonaut.Encode (encodeJson)import Data.Either (Either(..)) - edit in client/src/Aftok/Login.purs at line 10[4.295408]→[4.21:55](∅→∅),[4.55]→[4.86:124](∅→∅),[4.30]→[4.86:124](∅→∅),[4.124]→[4.295460:295534](∅→∅),[4.295460]→[4.295460:295534](∅→∅),[4.295534]→[4.125:160](∅→∅)
import Effect.Class.Console (log)import Affjax (post, get, printError)import Affjax.StatusCode (StatusCode(..))import Affjax.RequestBody as RBimport Affjax.ResponseFormat as RF - replacement in client/src/Aftok/Login.purs at line 24[4.85]→[4.157:158](∅→∅),[4.295631]→[4.157:158](∅→∅),[4.192]→[4.295631:295695](∅→∅),[4.295631]→[4.295631:295695](∅→∅)
type LoginRequest = { username :: String, password :: String }import Aftok.Api.Account (LoginResponse(..), login, checkLogin, logout) - replacement in client/src/Aftok/Login.purs at line 26
data LoginResponse= OK| Forbidden| Error { status :: Maybe StatusCode, message :: String }data LoginError= Forbidden| ServerError - replacement in client/src/Aftok/Login.purs at line 33
, loginResponse :: Maybe LoginResponse, loginError :: Maybe LoginError - replacement in client/src/Aftok/Login.purs at line 65
initialState _ = { username: "", password: "", loginResponse: Nothing }initialState _ = { username: "", password: "", loginError: Nothing } - replacement in client/src/Aftok/Login.purs at line 128
, case st.loginResponse of, case st.loginError of - replacement in client/src/Aftok/Login.purs at line 131
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 ofForbidden -> "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 ] - replacement in client/src/Aftok/Login.purs at line 140
[ P.classes (ClassName <$> ["btn", "btn-block", "btn-primary"])][ P.classes (ClassName <$> ["btn", "btn-block", "btn-primary"]) ] - edit in client/src/Aftok/Login.purs at line 163
H.modify_ (_ { loginResponse = Just response }) - replacement in client/src/Aftok/Login.purs at line 164[4.300507]→[4.471:530](∅→∅),[4.530]→[4.300546:300572](∅→∅),[4.300546]→[4.300546:300572](∅→∅),[4.300572]→[4.2466:2467](∅→∅),[4.2466]→[4.2466:2467](∅→∅),[4.2565]→[4.2565:2635](∅→∅),[4.2635]→[4.300573:300620](∅→∅),[4.300620]→[4.2715:2736](∅→∅),[4.2715]→[4.2715:2736](∅→∅),[4.2736]→[4.272:320](∅→∅),[4.320]→[4.217:329](∅→∅),[4.329]→[4.321:338](∅→∅),[4.300846]→[4.321:338](∅→∅),[4.338]→[4.330:449](∅→∅),[4.434]→[4.300846:301097](∅→∅),[4.449]→[4.300846:301097](∅→∅),[4.300846]→[4.300846:301097](∅→∅),[4.301097]→[4.450:902](∅→∅)
OK -> H.raise (LoginComplete { username: user })_ -> pure unit-- | Post credentials to the login service and interpret the responselogin :: String -> String -> Aff LoginResponselogin user pass = dolog "Sending login request to /api/login ..."result <- post RF.ignore "/api/login" (Just <<< RB.Json <<< encodeJson $ { username: user, password : pass })case result ofLeft err -> log ("Login failed: " <> printError err)Right r -> log ("Login status: " <> show r.status)pure $ case result ofLeft err -> Error { status: Nothing, message: printError err }Right r -> case r.status ofStatusCode 403 -> ForbiddenStatusCode 200 -> OKother -> Error { status: Just other, message: r.statusText }checkLogin :: Aff LoginResponsecheckLogin = dolog "Sending login check to /api/login/check ..."result <- get RF.ignore "/api/login/check"case result ofLeft err -> dolog ("Login failed: " <> printError err)pure $ Error { status: Nothing, message: printError err }Right r -> dolog ("Login status: " <> show r.status)pure $ case r.status ofStatusCode 200 -> OKStatusCode _ -> ForbiddenLoginOK -> H.raise (LoginComplete { username: user })LoginForbidden -> H.modify_ (_ { loginError = Just Forbidden })LoginError _ -> H.modify_ (_ { loginError = Just ServerError }) - edit in client/src/Aftok/Login.purs at line 168
logout :: Aff Unitlogout = dolog "Logging out on server with /api/logout ..."result <- get RF.ignore "/api/logout"case result ofLeft err -> log ("Logout failed: " <> printError err)Right r -> log ("Logout status: " <> show r.status) - replacement in client/src/Aftok/Login.purs at line 173
{ login: \_ _ -> pure OK, checkLogin: pure OK{ login: \_ _ -> pure LoginOK, checkLogin: pure LoginOK - replacement in client/src/Aftok/Signup.purs at line 5
-- import Control.Monad.Trans.Class (lift)import Control.Monad.Trans.Class (lift) - edit in client/src/Aftok/Signup.purs at line 7
import Data.Foldable (any) - edit in client/src/Aftok/Signup.purs at line 9
import Data.Either (Either(..), note)import Data.Validation.Semigroup (V(..), toEither, andThen, invalid) - edit in client/src/Aftok/Signup.purs at line 12
import Effect.Aff (Aff)import Effect.Class (liftEffect) - edit in client/src/Aftok/Signup.purs at line 33
import Aftok.Api.Account as Accimport Aftok.Api.Account (SignupRequest, SignupResponse, signupRequest)import Aftok.Api.Recaptcha (getRecaptchaResponse) - replacement in client/src/Aftok/Signup.purs at line 37
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 } - replacement in client/src/Aftok/Signup.purs at line 62
, loginResponse :: Maybe SignupResponse, signupErrors :: Array SignupError - replacement in client/src/Aftok/Signup.purs at line 76
= SignupComplete { username :: String }= SignupComplete String| SigninNav - replacement in client/src/Aftok/Signup.purs at line 82
{ 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) - replacement in client/src/Aftok/Signup.purs at line 113
, loginResponse: Nothing, signupErrors: [] - replacement in client/src/Aftok/Signup.purs at line 136
[ P.classes (ClassName <$> ["mb-6"]) ][ P.classes (ClassName <$> ["mb-6"]), E.onSubmit (Just <<< Signup)] - replacement in client/src/Aftok/Signup.purs at line 168
, P.id_ "password", P.id_ "passwordConfirm" - replacement in client/src/Aftok/Signup.purs at line 180
[ P.classes (ClassName <$> ["form-group", "mb-3"])[ P.classes (ClassName <$> ["g-recaptcha", "mx-auto"]) - replacement in client/src/Aftok/Signup.purs at line 202
SetUsername user -> H.modify_ (_ { username = Just user })SetPassword pass -> H.modify_ (_ { password = Just pass })SetUsername user -> doures <- lift $ caps.checkUsername userH.modify_ (_ { username = Just user })case ures ofAcc.UsernameCheckOK -> pure unitAcc.UsernameCheckTaken -> H.modify_ (_ { signupErrors = [UsernameTaken] })SetPassword pass -> doH.modify_ (_ { password = Just pass })confirm <- H.gets (_.passwordConfirm)when (any (notEq pass) confirm) (H.modify_ (_ { signupErrors = [PasswordMismatch] }))ConfirmPassword confirm -> doH.modify_ (_ { passwordConfirm = Just confirm })password <- H.gets (_.password)when (any (notEq confirm) password) (H.modify_ (_ { signupErrors = [PasswordMismatch] })) - replacement in client/src/Aftok/Signup.purs at line 220
ConfirmPassword pass -> H.modify_ (_ { passwordConfirm = Just pass })_ -> pure unitSetRecoveryEmail email -> H.modify_ (_ { recoveryEmail = Just email })SetRecoveryZAddr addr -> dozres <- lift $ caps.checkZAddr addrH.modify_ (_ { recoveryZAddr = Just addr })case zres ofAcc.ZAddrCheckOK -> pure unitAcc.ZAddrCheckInvalid -> H.modify_ (_ { signupErrors = [ZAddrInvalid] }) - edit in client/src/Aftok/Signup.purs at line 228
Signin ev -> dolift $ system.preventDefault (ME.toEvent ev)H.raise SigninNavSignup ev -> dolift $ system.preventDefault evrecType <- 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 ofRecoveryEmail ->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 Nothinglet reqV :: V (Array SignupError) Acc.SignupRequestreqV = signupRequest <$> usernameV<*> ((eq <$> pwdFormV <*> pwdConfV) `andThen`(if _ then pwdFormV else invalid [PasswordMismatch]))<*> recoveryV<*> recapVcase toEither reqV ofLeft errors ->H.modify_ (_ { signupErrors = errors })Right req -> doresponse <- lift (caps.signup req)case response ofAcc.SignupOK -> H.raise (SignupComplete $ req.username)Acc.CaptchaInvalid -> H.modify_ (_ { signupErrors = [CaptchaError] })Acc.ZAddrInvalid -> H.modify_ (_ { signupErrors = [ZAddrInvalid] })Acc.UsernameTaken -> H.modify_ (_ { signupErrors = [UsernameTaken] }) - replacement in client/src/Aftok/Signup.purs at line 332
mockCapability :: forall m. Applicative m => Capability mmockCapability :: Capability Aff - replacement in client/src/Aftok/Signup.purs at line 334
{ signup: \_ _ -> pure OK{ checkUsername: \_ -> pure Acc.UsernameCheckOK, checkZAddr: \_ -> pure Acc.ZAddrCheckOK, signup: \_ -> pure Acc.SignupOK, getRecaptchaResponse: liftEffect <<< getRecaptchaResponse - edit in client/src/Main.purs at line 24
import Aftok.Api.Account as Acc - replacement in client/src/Main.purs at line 85
initialState _ = { view: VLoading, config: { recaptchaKey: "" } }initialState _ ={ view: VLoading, config: { recaptchaKey: "6LdiA78ZAAAAAGGvDId_JmDbhalduIDZSqbuikfq" }} - replacement in client/src/Main.purs at line 117
Login.Forbidden -> pure VLoginAcc.LoginForbidden -> pure VLoginAcc.LoginError _ -> pure VLogin - edit in client/src/Main.purs at line 128
SignupAction (Signup.SigninNav) ->H.modify_ (_ { view = VLogin }) - edit in server/Aftok/QConfig.hs at line 36
, _recaptchaSecret :: Maybe Text - edit in server/Aftok/QConfig.hs at line 65
<*> C.lookup cfg "recaptchaSecret" - replacement in server/Aftok/Snaplet/Projects.hs at line 66
uid <- requireUserIdpid <- requireProjectIduid <- requireUserIdpid <- requireProjectId - replacement in server/Aftok/Snaplet/Projects.hs at line 69
t <- liftIO C.getCurrentTimet <- liftIO C.getCurrentTime - edit in server/Aftok/Snaplet/Users.hs at line 1
{-# LANGUAGE OverloadedStrings #-} - edit in server/Aftok/Snaplet/Users.hs at line 8
, CaptchaConfig(..), CaptchaError(..), checkCaptcha - replacement in server/Aftok/Snaplet/Users.hs at line 17
import Data.Aeson as Aimport qualified Data.Aeson as Aimport Data.Aeson ((.:), (.:?)) - edit in server/Aftok/Snaplet/Users.hs at line 21
import Data.Text.Encoding as T - edit in server/Aftok/Snaplet/Users.hs at line 25
import Network.HTTP.Client ( parseRequest, responseBody, responseStatus, httpLbs)import Network.HTTP.Client.TLS ( newTlsManager )import Network.HTTP.Client.MultipartFormData (formDataBody, partBS)import Network.HTTP.Types.Status ( statusCode ) - replacement in server/Aftok/Snaplet/Users.hs at line 39
import Snap.Coreimport Snap.Snaplet as Simport qualified Snap.Core as Simport qualified Snap.Snaplet as S - replacement in server/Aftok/Snaplet/Users.hs at line 50
instance FromJSON CUser whereparseJSON (Object v) =instance A.FromJSON CUser whereparseJSON (A.Object v) = - replacement in server/Aftok/Snaplet/Users.hs at line 71
requestBody <- readRequestBody 4096rbody <- S.readRequestBody 4096 - replacement in server/Aftok/Snaplet/Users.hs at line 74
$ A.decode requestBody$ A.decode rbody - replacement in server/Aftok/Snaplet/Users.hs at line 87[4.11292]→[4.3764:3800](∅→∅),[4.12605]→[4.3764:3800](∅→∅),[4.56909]→[4.3764:3800](∅→∅),[4.3764]→[4.3764:3800](∅→∅)
authUser <- with auth createSUserauthUser <- S.with auth createSUser - replacement in server/Aftok/Snaplet/Users.hs at line 94
params <- getParamsparams <- S.getParams - replacement in server/Aftok/Snaplet/Users.hs at line 96
(pure . traverse (parseInvCode . decodeUtf8))(pure . traverse (parseInvCode . T.decodeUtf8)) - edit in server/Aftok/Snaplet/Users.hs at line 105
type CaptchaCheckResult = Either [CaptchaError] () - edit in server/Aftok/Snaplet/Users.hs at line 107
data CaptchaError= MissingInputSecret| InvalidInputSecret| MissingInputResponse| InvalidInputResponse| BadRequest| TimeoutOrDuplicate| CaptchaError Textderiving (Eq, Show)data CaptchaConfig = CaptchaConfig{ secretKey :: Text }data CaptchaResponse = CaptchaResponse{ success :: Bool, errorCodes :: [CaptchaError]}instance A.FromJSON CaptchaResponse whereparseJSON (A.Object v) =CaptchaResponse <$> v .: "success"<*> (fmap toError . join . toList <$> v .:? "error-codes")wheretoError = \case"missing-input-secret" -> MissingInputSecret"invalid-input-secret" -> InvalidInputSecret"missing-input-response" -> MissingInputResponse"invalid-input-response" -> InvalidInputResponse"bad-request" -> BadRequest"timeout-or-duplicate" -> TimeoutOrDuplicateother -> CaptchaError $ "Unexpected error code: " <> otherparseJSON _ =fail "Captcha response body was not a valid JSON object." - edit in server/Aftok/Snaplet/Users.hs at line 141[4.9322]
checkCaptcha :: CaptchaConfig -> Text -> IO CaptchaCheckResultcheckCaptcha cfg token = dorequest <- parseRequest "https://www.google.com/recaptcha/api/siteverify"reqWithBody <- formDataBody [partBS "secret" (T.encodeUtf8 $ secretKey cfg), partBS "response" (T.encodeUtf8 token)] requestmanager <- newTlsManagerresponse <- httpLbs reqWithBody managerpure $ case statusCode (responseStatus response) of200 ->case A.eitherDecode (responseBody response) ofLeft 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)] - edit in server/Main.hs at line 66
registerRoute = void $ method POST registerHandler - edit in server/Main.hs at line 67
registerRoute = void $ method POST registerHandler