acme-not-a-joke-0.1.0.0: src/Acme/NotAJoke/Api/Account.hs
module Acme.NotAJoke.Api.Account where
import Control.Lens hiding ((.=))
import Data.Aeson (encode, object, (.=))
import Data.ByteString.Lazy (ByteString)
import Data.Text (Text)
import qualified Data.Text.Encoding as Encoding
import qualified Network.Wreq as Wreq
import qualified Crypto.JOSE.JWS as JWS
import Acme.NotAJoke.Api.Endpoint
import Acme.NotAJoke.Api.Field
import Acme.NotAJoke.Api.JWS
import Acme.NotAJoke.Api.Nonce
-- | The contact-field of an account (something like `mailto:certmaster@example.com`)
type Contact = Text
-- | An RFC-defined account status.
data AccountStatus
= AccountValid
| AccountDeactivated
| AccountRevoked
deriving (Show, Eq)
-- | A structure holding various Account field.
data Account a
= Account
{ status :: Field a "status" AccountStatus
, orders :: Field a "orders" (Endpoint "orders")
, agreement :: Field a "agreement" (Url "TOS")
, termsOfServiceAgreed :: Field a "termsOfServiceAgreed" Bool
, contact :: Field a "contact" [Contact]
, onlyReturnExisting :: Field a "onlyReturnExisting" Bool
}
type instance Field "account-create" "termsOfServiceAgreed" x = x
type instance Field "account-create" "contact" x = x
type instance Field "account-create" "status" x = ()
type instance Field "account-create" "orders" x = ()
type instance Field "account-create" "agreement" x = ()
type instance Field "account-create" "onlyReturnExisting" x = ()
type instance Field "account-fetch" "termsOfServiceAgreed" x = x
type instance Field "account-fetch" "contact" x = x
type instance Field "account-fetch" "status" x = ()
type instance Field "account-fetch" "orders" x = ()
type instance Field "account-fetch" "agreement" x = ()
type instance Field "account-fetch" "onlyReturnExisting" x = x
newtype AccountCreated = AccountCreated (Wreq.Response ByteString)
deriving (Show)
-- | Initializes an account structure, assuming we have read the terms-of-service.
createAccount :: [Contact] -> Account "account-create"
createAccount = createAccount1 True
type HasReadTermsOfService = Bool
-- | Initializes an account structure.
createAccount1 :: HasReadTermsOfService -> [Contact] -> Account "account-create"
createAccount1 tos contacts = Account () () () tos contacts ()
fetchAccount :: [Contact] -> Account "account-fetch"
fetchAccount = fetchAccount1 True True
fetchAccount1 :: Bool -> Bool -> [Contact] -> Account "account-fetch"
fetchAccount1 tos onlyfetch contacts = Account () () () tos contacts onlyfetch
-- | Lookup a Key Identifier for the account.
readKID :: AccountCreated -> Maybe KID
readKID (AccountCreated rsp) = rsp ^? Wreq.responseHeader "location" . to (KID . Encoding.decodeUtf8)
-- | Fetches or create an account (a single API call).
postCreateAccount :: JWS.JWK -> Endpoint "newAccount" -> Nonce -> Account "account-create" -> IO (Maybe AccountCreated)
postCreateAccount jwk ep nonce acc = do
let opts = Wreq.defaults & Wreq.header "Content-Type" .~ ["application/jose+json"]
ebody <- (jwkSign jwk ep nonce $ encode $ serialized)
case ebody of
Right body -> do
e <- Wreq.postWith opts (wrequrl ep) $ encode body
pure $ Just $ AccountCreated e
Left err -> do
print err
pure Nothing
where
serialized =
object
[ "termsOfServiceAgreed" .= acc.termsOfServiceAgreed
, "contact" .= acc.contact
]
-- | Only fetches an account (i.e., does not create the account if missing).
postFetchAccount :: JWS.JWK -> Endpoint "newAccount" -> Nonce -> Account "account-fetch" -> IO (Maybe AccountCreated)
postFetchAccount jwk ep nonce acc = do
let opts = Wreq.defaults & Wreq.header "Content-Type" .~ ["application/jose+json"]
ebody <- (jwkSign jwk ep nonce $ encode $ serialized)
case ebody of
Right body -> do
e <- Wreq.postWith opts (wrequrl ep) $ encode body
pure $ Just $ AccountCreated e
Left err -> do
print err
pure Nothing
where
serialized =
object
[ "termsOfServiceAgreed" .= acc.termsOfServiceAgreed
, "contact" .= acc.contact
, "onlyReturnExisting" .= acc.onlyReturnExisting
]