on master help sign in
Links in email are now valid.
commitf9c223e6191ccd0b1a6234ebadab50f54c2070d9 authoriko <ilyakooo0@gmail.com> dateSat 11 May 2019, 20:32 (7 years ago) parentac2022b
8 files changed+151−31
denis/email.html+2−3 denis/package.yaml+2 denis/src/Data/Query.hs+7−9 denis/src/Server/App.hs+1−1 denis/src/Server/Auth.hs+62−9 denis/src/Server/Auth/HTML.hs+46 denis/src/Server/Auth/Mail.hs+29−7 denis/src/Server/Server.hs+2−2
denis/email.html+2−3
@@ -1,8 +1,7 @@
11<html>
22
33<body style="font-family: sans-serif;">
4− <h1>
5− <div align="center" style="margin-top: 55px;">
4+ <div style="text-align: center; margin-top: 55px;">
65 <a href="#" style="font-size: 34px; font-weight: bold; display: inline-block; padding: 7px 13px 7px 13px; margin-bottom: 89px; text-decoration: underline; font-family: sans-serif; color: #555;">
76 Log in
87 </a>
@@ -15,7 +14,7 @@
1514 21<div style="display: inline-block; width: 13px;"></div>55<div style="display: inline-block; width: 13px;"></div>43
1615 </div>
1716 </div>
18− <div align="center" style="margin-top: 233px;">
17+ <div style="text-align: center; margin-top: 233px;">
1918 <a href="#" style="text-decoration: underline; font-family: sans-serif; color: #555;font-size: 13px; margin-bottom: 21px;">Revoke access</a>
2019 </div>
2120</body>
denis/package.yaml+2
@@ -64,6 +64,8 @@ dependencies:
6464- periodic
6565- ua-parser
6666- cryptohash-sha512
67+- blaze-html
68+- servant-blaze
6769
6870
6971library:
denis/src/Data/Query.hs+7−9
@@ -85,7 +85,7 @@ import Data.Text (Text)
8585import qualified Data.Text as T
8686import Data.Channel.NamedChannel
8787import Data.Channel.AnonymousChannel
88−import Data.Maybe (fromMaybe, isJust)
88+import Data.Maybe (fromMaybe)
8989import Squeal.PostgreSQL.Render
9090import Data.GroupChat
9191import Data.Message
@@ -631,11 +631,11 @@ getTokenActivationQ = selectStar (from (table #tokens) &
631631 where_ (#tokenUserId .== param @1 .&&
632632 #tokenActivationCode .== param @2))
633633
634−deactivateTokenQ :: Manipulation Schema (TuplePG (UserId, ByteString)) (RowPG (Only UserId))
634+deactivateTokenQ :: Manipulation Schema (TuplePG (UserId, ByteString)) (RowPG (Only Text))
635635deactivateTokenQ = deleteFrom #tokens
636636 (#tokenUserId .== param @1 .&&
637637 #tokenDeactivationCode .== param @2)
638− (Returning (#tokenUserId `as` #fromOnly))
638+ (Returning (#tokenUserAgent `as` #fromOnly))
639639
640640getVerifiedTokenQ :: Query Schema (TuplePG (UserId, ByteString)) (RowPG Token)
641641getVerifiedTokenQ = selectStar (from (table #tokens) &
@@ -946,7 +946,7 @@ tryVerifyToken uId tokenData code = commitedTransactionallyUpdate $ do
946946 _ <- manipulateParams updateTokenQ newToken
947947 return Nothing
948948
949−tryActivateToken :: UserId -> ByteString -> StaticPQ (Maybe ByteString)
949+tryActivateToken :: UserId -> ByteString -> StaticPQ (Maybe Token)
950950tryActivateToken uId activationData = do
951951 let hData = hash activationData
952952 token' <- runQueryParams getTokenActivationQ (uId, hData) >>= firstRow
@@ -954,13 +954,11 @@ tryActivateToken uId activationData = do
954954 Nothing -> return Nothing
955955 Just token -> do
956956 if (tokenActivationCode token == Just hData)
957− then commitedTransactionallyUpdate (validateToken token) >> return (Just $ tokenValue token)
957+ then commitedTransactionallyUpdate (validateToken token) >> (return $ Just token)
958958 else return Nothing
959959
960−deactivateToken :: UserId -> ByteString -> StaticPQ Bool
961−deactivateToken uId dData = do
962− res <- manipulateParams deactivateTokenQ (uId, hash dData) >>= firstRow
963− return $ isJust (res :: Maybe (Only UserId))
960+deactivateToken :: UserId -> ByteString -> StaticPQ (Maybe Text)
961+deactivateToken uId dData = fmap fromOnly <$> (manipulateParams deactivateTokenQ (uId, hash dData) >>= firstRow)
964962
965963getVerifiedToken :: UserId -> ByteString -> StaticPQ (Maybe Token)
966964getVerifiedToken uId tokenData = runQueryParams getVerifiedTokenQ (uId, hash tokenData) >>= firstRow
denis/src/Server/App.hs+1−1
@@ -33,7 +33,7 @@ type DBConnection = Pool (K Connection Schema)
3333data Config = Config {
3434 getPool :: !DBConnection,
3535 cryptoState :: !CryptoRNGState,
36− -- selfRootUrl :: !String,
36+ selfRootUrl :: !String,
3737 mailConfig :: !MailConfig
3838}
3939
denis/src/Server/Auth.hs+62−9
@@ -43,16 +43,17 @@ import qualified GHC.Generics as GHC
4343import Data.Aeson
4444import qualified Data.Aeson as A
4545import Data.Binary.Builder (toLazyByteString)
46−import Servant.Docs (ToSample, samples, toSamples, singleSample)
46+import Servant.Docs
4747import qualified Data.Text as T
4848import qualified Data.Text.Encoding as T
4949import Text.Regex
50−import qualified Data.ByteString.Base64 as BS
50+import qualified Data.ByteString.Base64.URL as BS
5151import Data.Maybe
5252import Server.Auth.Token
5353import Server.Auth.Mail
5454import Web.UAParser
5555import qualified Data.Text.Lazy as TL
56+import Server.Auth.HTML
5657
5758-- MARK: Documentation
5859
@@ -74,6 +75,23 @@ instance ToSample TokenActivationData where
7475instance ToSample TokenActivationResult where
7576 toSamples _ = samples [TokenActivationResultYou, TokenActivationResultNotYou]
7677
78+instance ToParam (QueryParam' mods "data" String) where
79+ toParam _ =
80+ DocQueryParam "data"
81+ ["AAABBBC="]
82+ "The data recieved from the email."
83+ Normal
84+
85+instance ToParam (QueryParam' mods "id" UserId) where
86+ toParam _ =
87+ DocQueryParam "id"
88+ ["8"]
89+ "The id of the user."
90+ Normal
91+
92+instance ToSample Markup where
93+ toSamples _ = noSamples
94+
7795type LoginDescription = Description "Try to log in with the given email\n\nResponse fields:\n\n- `registered` -- User is registered and the cookie has been set. You now need to get a verification code from the user and pass it to `/verify`.\n- `canRegister` -- the email is valid, but the user needs to register. Prompt him for information and pass it to `/register`.\n- `invalid` -- the email is invalid."
7896
7997type LogoutDescription = Description "Log out with the given cookie"
@@ -112,7 +130,15 @@ type AuthenticationHandler =
112130 :> Post '[JSON] TokenActivationResult :<|>
113131 "deactivate" :> DeactivateDescription
114132 :> ReqBody '[JSON] TokenActivationData
115− :> PostNoContent '[JSON] NoContent
133+ :> PostNoContent '[JSON] NoContent :<|>
134+ "activateUser"
135+ :> QueryParam "id" UserId
136+ :> QueryParam "data" String
137+ :> Get '[HTML] Markup :<|>
138+ "deactivateUser"
139+ :> QueryParam "id" UserId
140+ :> QueryParam "data" String
141+ :> Get '[HTML] Markup
116142
117143data UserResponseStatus = UserIsRegistered | UserCanRegister | InvalidUser
118144 deriving (Show)
@@ -164,8 +190,35 @@ instance FromJSON BS.ByteString where
164190newtype AuthenticationCredits = AuthenticationCredits {authenticationEmail :: UserEmail}
165191 deriving (GHC.Generic, ToJSON, FromJSON)
166192
193+activateUser :: Maybe UserId -> Maybe String -> App Markup
194+activateUser Nothing _ = return $ page "The link is invalid."
195+activateUser _ Nothing = return $ page "The link is invalid."
196+activateUser (Just uId) (Just aData') = catchErrorPage $ do
197+ case BS.decode . C.pack $ aData' of
198+ Left _ -> return $ page $ "The link is invalid."
199+ Right aData -> do
200+ tokenData' <- runQerror $ tryActivateToken uId aData
201+ case tokenData' of
202+ Nothing -> return $ page "This activation link has been invalidated."
203+ Just Token{..} -> return $ genActivationPage "in" tokenUserAgent
204+
205+deactivateUser :: Maybe UserId -> Maybe String -> App Markup
206+deactivateUser Nothing _ = return $ page "The link is invalid."
207+deactivateUser _ Nothing = return $ page "The link is invalid."
208+deactivateUser (Just uId) (Just dData') = catchErrorPage $ do
209+ case BS.decode . C.pack $ dData' of
210+ Left _ -> return $ page $ "The link is invalid."
211+ Right dData -> do
212+ ua' <- runQerror $ deactivateToken uId dData
213+ case ua' of
214+ Nothing -> return $ page "This activation link has been invalidated."
215+ Just ua -> return $ genActivationPage "out" ua
216+
217+catchErrorPage :: App Markup -> App Markup
218+catchErrorPage = flip catchError $ \ (_ :: ServantErr) -> return errorPage
219+
167220authenticationAPI :: ServerT AuthenticationHandler App
168−authenticationAPI = logIn :<|> logOut :<|> verifyToken :<|> register :<|> checkCookie :<|> validateTokenApi :<|> deactivateTokenApi
221+authenticationAPI = logIn :<|> logOut :<|> verifyToken :<|> register :<|> checkCookie :<|> validateTokenApi :<|> deactivateTokenApi :<|> activateUser :<|> deactivateUser
169222
170223logIn :: AuthenticationCredits -> Maybe String -> App (Headers '[Header "Set-Cookie" String] UserLoginResponse)
171224logIn (AuthenticationCredits email') ua' = do
@@ -181,7 +234,7 @@ logIn (AuthenticationCredits email') ua' = do
181234 GeneratedToken token value code activation deactivation <- generateToken userId ua
182235 runQerror . createToken $ token
183236 let cookie = generateCookie token value
184− sendTokenVerificationEmail code (TL.fromStrict ua) email
237+ sendTokenVerificationEmail code userId (BS.encode activation) (BS.encode deactivation) (TL.fromStrict ua) email
185238 return . addHeader cookie $ UserLoginResponse UserIsRegistered
186239
187240generateCookie :: Token -> BS.ByteString -> String
@@ -232,18 +285,18 @@ validateTokenApi cookie' (TokenActivationData uId aData) = do
232285 tokenData' <- runQerror $ tryActivateToken uId aData
233286 case tokenData' of
234287 Nothing -> throwError err401
235− Just tokenData -> do
288+ Just Token{..} -> do
236289 case cookie' >>= getCookie of
237290 Nothing -> return TokenActivationResultNotYou
238291 Just (AuthenticationCookieData uId' cData) ->
239− if hash cData == tokenData && uId' == uId
292+ if hash cData == tokenValue && uId' == tokenUserId
240293 then return TokenActivationResultYou
241294 else return TokenActivationResultNotYou
242295
243296deactivateTokenApi :: TokenActivationData -> App NoContent
244297deactivateTokenApi (TokenActivationData uId dData) = do
245298 res <- runQerror $ deactivateToken uId dData
246− if res
299+ if isJust res
247300 then return NoContent
248301 else throwError err401
249302
@@ -265,7 +318,7 @@ register creation' ua' = do
265318 let gToken = genToken uId ua
266319 createToken . generatedToken $ gToken
267320 return gToken
268− sendTokenVerificationEmail code (TL.fromStrict ua) (userCreationUserEmail creation)
321+ sendTokenVerificationEmail code (tokenUserId token) (BS.encode activation) (BS.encode deactivation) (TL.fromStrict ua) (userCreationUserEmail creation)
269322 let cookie = generateCookie token value
270323 return $ addHeader cookie NoContent
271324
denis/src/Server/Auth/HTML.hsnew file+46
@@ -0,0 +1,46 @@
1+{-# LANGUAGE DataKinds,
2+ DeriveGeneric,
3+ FlexibleContexts,
4+ FlexibleInstances,
5+ MultiParamTypeClasses,
6+ OverloadedStrings,
7+ ScopedTypeVariables,
8+ TypeFamilies,
9+ TypeOperators,
10+ UndecidableInstances,
11+ DeriveAnyClass,
12+ RecordWildCards #-}
13+
14+module Server.Auth.HTML (
15+ HTML,
16+ genActivationPage,
17+ Markup,
18+ errorPage,
19+ page
20+ ) where
21+
22+import Servant.HTML.Blaze
23+import Text.Blaze.Html5 as H
24+import Text.Blaze.XHtml5.Attributes as A
25+import Data.Text (Text)
26+
27+genActivationPage :: Text -> Text -> Markup
28+genActivationPage verb ua = genPage $ do
29+ "You have successfully "
30+ u $ do
31+ "logged "
32+ toHtml verb
33+ " on "
34+ u $ toHtml ua
35+
36+
37+genPage :: Markup -> Markup
38+genPage pageContent = html $ body ! A.style "font-family: sans-serif; background-color: #eee" $ do
39+ H.div ! A.style "text-align: center; margin-top: 20vh; margin-left: 10%; margin-right: 10%" $ do
40+ H.div ! A.style "font-size: 40px; font-weight: bold;" $ pageContent
41+
42+errorPage :: Markup
43+errorPage = genPage $ "Sorry, something went wrong."
44+
45+page :: Text -> Markup
46+page t = genPage $ toHtml t
denis/src/Server/Auth/Mail.hs+29−7
@@ -23,22 +23,44 @@ import Control.Monad.IO.Class
2323import Network.Mail.Mime (Mail, Part)
2424import Data.Text (Text)
2525import qualified Data.Text.Lazy as TL
26+import Text.Blaze.Html5 as H
27+import Text.Blaze.XHtml5.Attributes as A
28+import Text.Blaze.Html.Renderer.Text
29+import Data.ByteString (ByteString)
30+import Data.User
31+import Data.ByteString.Char8 (unpack)
2632
27−sendTokenVerificationEmail :: TokenVerificationCode -> TL.Text -> Text -> App ()
28−sendTokenVerificationEmail code ua email = do
29− mailCfg <- mailConfig <$> ask
33+sendTokenVerificationEmail :: TokenVerificationCode -> UserId -> ByteString -> ByteString -> TL.Text -> Text -> App ()
34+sendTokenVerificationEmail userCode uId activation deactivation ua email = do
35+ cfg <- ask
36+ let mailCfg = mailConfig cfg
37+ let rootUrl = selfRootUrl cfg
3038 let mail = simpleMail
3139 (Address (Just "HSE Social Network") "noreply@hse.ru")
3240 [Address Nothing email] [] []
3341 "HSE Social Network Verification Code"
34− [genMail code ua]
42+ [genMail rootUrl userCode uId (unpack activation) (unpack deactivation) ua]
3543 sendMailWithConfig mailCfg mail
3644
3745sendMailWithConfig :: (MonadIO m) => MailConfig -> Mail -> m ()
3846sendMailWithConfig (MailConfig host port user pass) =
3947 liftIO . sendMailWithLogin' host (fromIntegral port) user pass
4048
41−genMail :: TokenVerificationCode -> TL.Text -> Part
42−genMail code ua = htmlPart $ "<html><body style=\"font-family: sans-serif;\"> <h1> <div align=\"center\" style=\"margin-top: 55px;\"> <a href=\"#\" style=\"font-size: 34px; font-weight: bold; display: inline-block; padding: 7px 13px 7px 13px; margin-bottom: 89px; text-decoration: underline; font-family: sans-serif; color: #555;\"> Log in </a> <br /> <div style=\"font-size: 34px; font-weight: bold;\"> <span style=\"color: #bbb; font-weight: bold;\">from: </span>" <> ua <> "</div> <br/> <div style=\"font-size: 55px; font-weight: bold; border-radius: 10px; background-color: #eee; display: inline-block; padding: 7px 13px 7px 13px; box-shadow: 0px 5px 21px #bbb; background-image: linear-gradient(#f9f9f9, #eee);\">" <> TL.pack [a, b] <> "<div style=\"display: inline-block; width: 13px;\"></div>" <> TL.pack [c, d] <> "<div style=\"display: inline-block; width: 13px;\"></div>" <> TL.pack [e, f] <> "</div></div><div align=\"center\" style=\"margin-top: 233px;\"> <a href=\"#\" style=\"text-decoration: underline; font-family: sans-serif; color: #555;font-size: 13px; margin-bottom: 21px;\">Revoke access</a> </div></body></html>"
49+genMail :: String -> TokenVerificationCode -> UserId -> String -> String -> TL.Text -> Part
50+genMail rootUrl userCode uId activation deactivation ua = htmlPart . renderHtml $ html $ body ! A.style "font-family: sans-serif;" $ do
51+ H.div ! A.style "text-align: center; margin-top: 55px;" $ do
52+ H.a ! href (stringValue $ rootUrl <> "/authentication/activateUser?id=" <> show uId <> "&data=" <> activation) ! A.style "font-size: 34px; font-weight: bold; display: inline-block; padding: 7px 13px 7px 13px; margin-bottom: 89px; text-decoration: underline; font-family: sans-serif; color: #555;" $ "Log in"
53+ br
54+ H.div ! A.style "font-size: 34px; font-weight: bold;" $ do
55+ H.span ! A.style "color: #bbb; font-weight: bold;" $ "from: "
56+ toHtml ua
57+ br
58+ H.div ! A.style "font-size: 55px; font-weight: bold; border-radius: 10px; background-color: #eee; display: inline-block; padding: 7px 13px 7px 13px; box-shadow: 0px 5px 21px #bbb; background-image: linear-gradient(#f9f9f9, #eee);" $ do
59+ toHtml [a', b']
60+ H.div ! A.style "display: inline-block; width: 13px;" $ mempty
61+ toHtml [c', d']
62+ H.div ! A.style "display: inline-block; width: 13px;" $ mempty
63+ toHtml [e', f']
64+ H.div ! A.style "text-align: center; margin-top: 233px;" $ H.a ! href (stringValue $ rootUrl <> "/authentication/deactivateUser?id=" <> show uId <> "&data=" <> deactivation) ! A.style "text-decoration: underline; font-family: sans-serif; color: #555;font-size: 13px; margin-bottom: 21px;" $ "Revoke access"
4365 where
44− [a, b, c, d, e, f] = reverse . take 6 . (++ repeat '0') . reverse . show $ code
66+ [a', b', c', d', e', f'] = reverse . take 6 . (++ repeat '0') . reverse . show $ userCode
denis/src/Server/Server.hs+2−2
@@ -45,9 +45,9 @@ getConfig = do
4545 databaseURL <- getDBString
4646 conn <- createConnectionPool databaseURL 3 0.5 10
4747 crypto <- newCryptoRNGState
48− -- (Jusr selfRoot) <- lookupEnv "SELF_ROOT_URL"
48+ selfRoot <- fromMaybe "http://localhost:2000" <$> lookupEnv "SELF_ROOT_URL"
4949 mail <- getMailConfig
50− return $ Config conn crypto mail
50+ return $ Config conn crypto selfRoot mail
5151
5252runServer :: IO ()
5353runServer = do