email_auth_ses_mailer.hs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. {-# LANGUAGE DeriveDataTypeable #-}
  2. {-# LANGUAGE FlexibleContexts #-}
  3. {-# LANGUAGE GADTs #-}
  4. {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  5. {-# LANGUAGE MultiParamTypeClasses #-}
  6. {-# LANGUAGE OverloadedStrings #-}
  7. {-# LANGUAGE QuasiQuotes #-}
  8. {-# LANGUAGE TemplateHaskell #-}
  9. {-# LANGUAGE TypeFamilies #-}
  10. {-# LANGUAGE OverloadedStrings #-}
  11. import Control.Monad (join)
  12. import Control.Monad.Logger (runNoLoggingT)
  13. import Data.Maybe (isJust)
  14. import Data.Yaml
  15. import Data.Text (Text)
  16. import qualified Data.Text.Encoding as TE
  17. import qualified Data.Text.Lazy.Encoding as LTE
  18. import Data.Typeable (Typeable)
  19. import Database.Persist.Sqlite
  20. import Database.Persist.TH
  21. import Network.Mail.Mime
  22. import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
  23. import Text.Shakespeare.Text (stext)
  24. import Yesod
  25. import Yesod.Auth
  26. import Yesod.Auth.Email
  27. import Network.Mail.Mime.SES
  28. import Data.ByteString.Char8
  29. import Control.Monad (mzero)
  30. import Network.HTTP.Client.Conduit (Manager, newManager, HasHttpManager (getHttpManager))
  31. import System.Exit (exitWith, ExitCode( ExitFailure ))
  32. share [mkPersist sqlSettings { mpsGeneric = False }, mkMigrate "migrateAll"] [persistLowerCase|
  33. User
  34. email Text
  35. password Text Maybe -- Password may not be set yet
  36. verkey Text Maybe -- Used for resetting passwords
  37. verified Bool
  38. UniqueUser email
  39. deriving Typeable
  40. |]
  41. data App = App
  42. { sqlBackend :: SqlBackend
  43. , appHttpManager :: Manager
  44. }
  45. instance HasHttpManager App where
  46. getHttpManager = appHttpManager
  47. mkYesod "App" [parseRoutes|
  48. / HomeR GET
  49. /auth AuthR Auth getAuth
  50. |]
  51. instance Yesod App where
  52. -- Emails will include links, so be sure to include an approot so that
  53. -- the links are valid!
  54. approot = ApprootStatic "http://localhost:3000"
  55. instance RenderMessage App FormMessage where
  56. renderMessage _ _ = defaultFormMessage
  57. -- Set up Persistent
  58. instance YesodPersist App where
  59. type YesodPersistBackend App = SqlBackend
  60. runDB f = do
  61. App conn _ <- getYesod
  62. runSqlConn f conn
  63. instance YesodAuth App where
  64. type AuthId App = UserId
  65. loginDest _ = HomeR
  66. logoutDest _ = HomeR
  67. authPlugins _ = [authEmail]
  68. -- Need to find the UserId for the given email address.
  69. getAuthId creds = runDB $ do
  70. x <- insertBy $ User (credsIdent creds) Nothing Nothing False
  71. return $ Just $
  72. case x of
  73. Left (Entity userid _) -> userid -- newly added user
  74. Right userid -> userid -- existing user
  75. authHttpManager = error "Email doesn't need an HTTP manager"
  76. instance YesodAuthPersist App
  77. -- Here's all of the email-specific code
  78. data SesKeys = SesKeys { accessKey :: !Text, secretKey :: !Text }
  79. instance FromJSON SesKeys where
  80. parseJSON (Object v) =
  81. SesKeys <$> v .: "accessKey"
  82. <*> v .: "secretKey"
  83. parseJSON _ = mzero
  84. instance YesodAuthEmail App where
  85. type AuthEmailId App = UserId
  86. afterPasswordRoute _ = HomeR
  87. addUnverified email verkey =
  88. runDB $ insert $ User email Nothing (Just verkey) False
  89. -- Send the verification email with your SES credentials located in config/secrets.yaml
  90. -- NOTE: The email address you're sending from will have to be verified on SES
  91. sendVerifyEmail email _ verurl = do
  92. h <- getYesod
  93. sesCreds <- liftIO $ getSESCredentials
  94. liftIO $ renderSendMailSES (getHttpManager h) sesCreds (emptyMail $ Address Nothing "noreply@example.com")
  95. { mailTo = [Address Nothing email]
  96. , mailHeaders =
  97. [ ("Subject", "Verify your email address")
  98. ]
  99. , mailParts = [[textPart, htmlPart]]
  100. }
  101. where
  102. getSESCredentials :: IO SES
  103. getSESCredentials = do
  104. key <- getsesAccessKey
  105. return SES {
  106. sesTo = [(TE.encodeUtf8 email)],
  107. sesFrom = "noreply@example.com",
  108. sesAccessKey = TE.encodeUtf8 $ accessKey key,
  109. sesSecretKey = TE.encodeUtf8 $ secretKey key,
  110. sesRegion = usWest2 }
  111. getsesAccessKey :: IO SesKeys
  112. getsesAccessKey = do
  113. ymlConfig <- Data.ByteString.Char8.readFile "config/secrets.yaml"
  114. case decode ymlConfig of
  115. Nothing -> do Data.ByteString.Char8.putStrLn "Error while parsing secrets.yaml"; System.Exit.exitWith (ExitFailure 1)
  116. Just c -> return c
  117. textPart = Part
  118. { partType = "text/plain; charset=utf-8"
  119. , partEncoding = None
  120. , partFilename = Nothing
  121. , partContent = LTE.encodeUtf8 $
  122. [stext|
  123. Please confirm your email address by clicking on the link below.
  124. #{verurl}
  125. Thank you
  126. |]
  127. , partHeaders = []
  128. }
  129. htmlPart = Part
  130. { partType = "text/html; charset=utf-8"
  131. , partEncoding = None
  132. , partFilename = Nothing
  133. , partContent = renderHtml
  134. [shamlet|
  135. <p>Please confirm your email address by clicking on the link below.
  136. <p>
  137. <a href=#{verurl}>#{verurl}
  138. <p>Thank you
  139. |]
  140. , partHeaders = []
  141. }
  142. getVerifyKey = runDB . fmap (join . fmap userVerkey) . get
  143. setVerifyKey uid key = runDB $ update uid [UserVerkey =. Just key]
  144. verifyAccount uid = runDB $ do
  145. mu <- get uid
  146. case mu of
  147. Nothing -> return Nothing
  148. Just u -> do
  149. update uid [UserVerified =. True]
  150. return $ Just uid
  151. getPassword = runDB . fmap (join . fmap userPassword) . get
  152. setPassword uid pass = runDB $ update uid [UserPassword =. Just pass]
  153. getEmailCreds email = runDB $ do
  154. mu <- getBy $ UniqueUser email
  155. case mu of
  156. Nothing -> return Nothing
  157. Just (Entity uid u) -> return $ Just EmailCreds
  158. { emailCredsId = uid
  159. , emailCredsAuthId = Just uid
  160. , emailCredsStatus = isJust $ userPassword u
  161. , emailCredsVerkey = userVerkey u
  162. , emailCredsEmail = email
  163. }
  164. getEmail = runDB . fmap (fmap userEmail) . get
  165. getHomeR :: Handler Html
  166. getHomeR = do
  167. maid <- maybeAuthId
  168. defaultLayout
  169. [whamlet|
  170. <p>Your current auth ID: #{show maid}
  171. $maybe _ <- maid
  172. <p>
  173. <a href=@{AuthR LogoutR}>Logout
  174. $nothing
  175. <p>
  176. <a href=@{AuthR LoginR}>Go to the login page
  177. |]
  178. main :: IO ()
  179. main = runNoLoggingT $ withSqliteConn "email.db3" $ \conn -> liftIO $ do
  180. runSqlConn (runMigration migrateAll) conn
  181. httpManager <- newManager
  182. warp 3000 $ App conn httpManager