-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.hs
217 lines (187 loc) · 7.04 KB
/
main.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DeriveGeneric #-}
import Control.Monad.Logger (runNoLoggingT)
import Control.Monad (forever)
import Control.Concurrent.STM
import Conduit (($$), mapM_C)
import Data.Aeson
import Data.Text (Text, pack)
import Data.Text.Lazy (toStrict, fromStrict)
import Data.Typeable (Typeable)
import Database.Persist.Sqlite
import GHC.Generics
import Network.HTTP.Client.Conduit (Manager, newManager)
import System.Environment (getEnv)
import Text.Blaze.Html.Renderer.Text (renderHtml)
import Text.Markdown
import Text.Hamlet (hamletFile)
import Text.Julius (juliusFile)
import Text.Lucius (luciusFile)
import Yesod
import Yesod.Auth
import qualified Yesod.Auth.HashDB as HDB
import Yesod.WebSockets
data WsMsg = WsMsg
{ msgName :: Text
, msgContent :: Text
, msgClear :: Bool
} deriving (Generic, Show)
instance ToJSON WsMsg
instance FromJSON WsMsg
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
name Text -- userName
password Text -- userPassword
UniqueUser name
deriving Typeable
|]
mkYesod "App" [parseRoutes|
/ HomeR GET
/auth AuthR Auth getAuth
/register RegisterR POST -- TODO make this a dispatch of HashDB
/admin AdminR POST
/chat ChatR GET
|]
data App = App
{ envApproot :: Text
, sqlBackEnd :: SqlBackend
, httpManager :: Manager
, chatChannel :: TChan Text
}
instance Yesod App where
approot = ApprootMaster envApproot
authRoute _ = Just $ AuthR LoginR
isAuthorized AdminR _ = isAdmin
isAuthorized ChatR _ = isLoggedIn
isAuthorized (AuthR LoginR) _ = isNotLoggedIn
isAuthorized _ _ = return Authorized
defaultLayout = layout
where layout widget = do
ma <- maybeAuth
let mName = (userName . entityVal) <$> ma
pc <- widgetToPageContent widget
mmsg <- getMessage
withUrlRenderer $(hamletFile "layout.hamlet")
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
instance YesodPersist App where
type YesodPersistBackend App = SqlBackend
runDB f = do
App _ conn _ _ <- getYesod
runSqlConn f conn
instance YesodAuthPersist App where
type AuthEntity App = User
instance YesodAuth App where
type AuthId App = UserId
loginDest _ = HomeR
logoutDest _ = HomeR
authPlugins _ = [ HDB.authHashDBWithForm loginWidget (Just . UniqueUser) ]
where loginWidget action = $(whamletFile "loginform.hamlet")
getAuthId = HDB.getAuthIdHashDB AuthR (Just . UniqueUser)
authHttpManager = httpManager
instance HDB.HashDBUser User where
userPasswordHash = Just . userPassword
setPasswordHash h u = u { userPassword = h }
chatHandler :: Text -> WebSocketsT Handler ()
chatHandler name = do
App _ _ _ writeChan <- getYesod
readChan <- liftAtomically $ dupTChan writeChan
race_
(forever $ liftAtomically (readTChan readChan) >>= sendTextData)
(sourceWS $$ mapM_C (liftAtomically . writeTChan writeChan . buildMsg name))
registerSucc :: User -> Handler () -- Post/Redirect/Get
registerSucc user = do
isOk <- addUserToDB user
if isOk
then setMessage "Success!, please login." >> redirect (AuthR LoginR)
else setMessage "User name already exist, please try again." >> redirect HomeR
registerFail :: Handler ()
registerFail = setMessage "Critical Error!" >> redirect HomeR
postRegisterR :: Handler ()
postRegisterR = do
((result, widget), enctype) <- runFormPost registerForm
case result of
FormSuccess user -> registerSucc user
_ -> registerFail
getHomeR :: Handler Html
getHomeR = do
ma <- maybeAuth
let mName = (userName . entityVal) <$> ma
(widget, enctype) <- generateFormPost registerForm
defaultLayout $(whamletFile "home.hamlet")
getChatR :: Handler Html
getChatR = do
name <- requireAuth
webSockets (chatHandler $ nameFromEntity name)
defaultLayout $ do
toWidget $(whamletFile "chat.hamlet")
toWidget $(luciusFile "chat.lucius")
toWidget $(juliusFile "chat.julius")
postAdminR :: Handler ()
postAdminR = do
App _ _ _ chan <- getYesod
let msg = toJsonText (WsMsg "" "" True)
liftAtomically (writeTChan chan msg)
redirect ChatR
registerForm :: Html -> MForm Handler (FormResult User, Widget)
registerForm = renderDivs $ User
<$> areq textField "Username" Nothing
<*> areq passwordField "Password" Nothing
addUserToDB :: User -> Handler Bool
addUserToDB user = do
maybeUser <- runDB $ getBy (UniqueUser (userName user))
case maybeUser of
Nothing -> runDB $ do
newUser <- HDB.setPassword (userPassword user) user
_ <- insert newUser -- insert gives user ID, not needed here
return True
Just _ -> return False
isLoggedIn :: Handler AuthResult
isLoggedIn = do
mu <- maybeAuthId
return $ case mu of
Nothing -> Unauthorized "You must be logged in to access this page."
_ -> Authorized
isNotLoggedIn :: Handler AuthResult
isNotLoggedIn = do
auth <- isLoggedIn
return $ case auth of
Authorized -> Unauthorized "Please logout first."
_ -> Authorized
isAdmin :: Handler AuthResult
isAdmin = do
mu <- maybeAuthId
return $ case mu of
Nothing -> AuthenticationRequired
Just x -> if keyIsAdmin x
then Authorized
else Unauthorized "You must be an admin."
main :: IO ()
main = runNoLoggingT $ withSqliteConn "user.db3" $ \conn -> liftIO $ do
root <- pack <$> getEnv "APPROOT" -- TODO sanity check
chan <- atomically newBroadcastTChan
man <- newManager
runSqlConn (runMigration migrateAll) conn
warp 3000 (App root conn man chan)
-- helper functions
keyIsAdmin :: UserId -> Bool
keyIsAdmin x = fromSqlKey x == 1
nameFromEntity :: Entity User -> Text
nameFromEntity = userName . entityVal
liftAtomically :: MonadIO m => STM a -> m a
liftAtomically = liftIO . atomically
buildMsg :: Text -> Text -> Text
buildMsg name content =
toJsonText (WsMsg (f name) (f content) False)
where f = toStrict
. renderHtml
. markdown def
. fromStrict