1 {-# LANGUAGE OverloadedStrings #-}
4 import Web.Scotty.Cookie
5 import Web.Scotty.Trans
6 import qualified Data.Text.Lazy as T
7 import qualified Data.Text.Lazy.IO as T
10 import qualified Data.Map.Lazy as Map
15 import Control.Monad.IO.Class
16 import Control.Monad.Trans.Class
17 import qualified Control.Monad.Trans.State as S
18 import qualified Text.MMark as M
19 import qualified Text.MMark.Extension.Common as M
21 import Text.Read (readMaybe)
24 import System.Directory
25 import System.FilePath
27 import Network.HTTP.Types
29 type Sessions = Map.Map T.Text T.Text
30 type ActionM = ActionT T.Text (S.StateT Sessions IO)
32 -- | Checks if the user is logged in
33 loggedIn :: ActionM Bool
35 mCookie <- getCookie "session"
37 Nothing -> return False
38 Just cookie -> Map.member (T.fromStrict cookie) <$> lift S.get
40 -- | Returns the current username
41 currentUser :: ActionM (Maybe T.Text)
43 mCookie <- getCookie "session"
45 Nothing -> return Nothing
46 Just cookie -> Map.lookup (T.fromStrict cookie) <$> lift S.get
48 -- | Makes a function that carries over the state from one
49 -- request to the next
50 mkPreserve :: s -> IO ((S.StateT s IO) a -> IO a)
55 (x, s') <- S.runStateT f s
60 logins :: Map.Map T.Text T.Text
70 preserve <- mkPreserve mempty
71 scottyT 3000 preserve $ do
72 get "/style.css" $ file "style.css"
77 then template "Create" $ do
78 h1_ "Create a new post"
79 form_ [action_ "create", method_ "post"] $ do
80 input_ [name_ "title", type_ "text", placeholder_ "Title"]
82 textarea_ [name_ "body", placeholder_ "# Your markdown here"] ""
84 input_ [type_ "submit", value_ "Post"]
86 text "You're not logged in"
87 status unauthorized401
93 redirect (linkToPost p)
96 username <- param "username"
97 password <- param "password"
98 if Map.lookup username logins /= Just password
100 text "Invalid username and/or password"
101 status unauthorized401
103 session <- T.pack . show <$> liftIO (randomIO :: IO Int)
104 lift $ S.modify (Map.insert session username)
105 setSimpleCookie "session" (T.toStrict session)
108 post "/logout" $ deleteCookie "session" >> redirect "/"
112 files <- listDirectory "posts"
113 posts <- catMaybes <$> mapM load files
114 return $ take 5 $ sortOn (Down . date) posts
115 template "Cool blog" $ sequence (mapMaybe render posts)
117 get "/posts/:post" $ do
119 mPost <- liftIO (load name)
121 Nothing -> status notFound404
124 Nothing -> status internalServerError500
125 Just content -> template (title post) content
127 template :: T.Text -> Html a -> ActionM ()
128 template title content = do
130 html $ renderText $ html_ $ do
131 head_ $ style >> title_ (toHtml title)
134 a_ [href_ "/"] "🏠 Cool Blog"
137 " " >> a_ [href_ "/create"] "Create a Post"
138 form_ [action_ "/logout", method_ "post", class_ "login-form"] $
139 input_ [type_ "submit", value_ "Log Out"]
140 else form_ [action_ "/login", method_ "post", class_ "login-form"] $ do
141 input_ [name_ "username", placeholder_ "Username", type_ "text"]
142 input_ [name_ "password", placeholder_ "Password", type_ "password"]
143 input_ [type_ "submit", value_ "Log In"]
145 where style = link_ [Attribute "rel" "stylesheet", Attribute "href" "/style.css"]
152 } deriving (Read, Show)
154 load :: String -> IO (Maybe Post)
156 let file = "posts" </> name
157 guard =<< doesFileExist file
158 readMaybe <$> readFile file
160 render :: Post -> Maybe (Html ())
161 render p@(Post markdown title date author) =
162 case M.parse (pathToPost p) (T.toStrict markdown) of
165 article_ [id_ (T.toStrict title)] $ do
167 a_ [href_ (T.toStrict $ linkToPost p)] $
169 address_ $ "By " >> toHtml author
170 time_ [datetime_ (T.toStrict $ T.pack (show date))] $ toHtml dateStr
171 M.render (M.useExtensions extensions doc)
172 where extensions = [M.ghcSyntaxHighlighter, M.skylighting, M.footnotes]
173 dateStr = T.pack $ formatTime defaultTimeLocale "%a %e %B %Y" date
175 linkToPost :: Post -> T.Text
176 linkToPost p = T.pack $ "/posts" </> kebab (T.unpack (title p))
178 pathToPost :: Post -> FilePath
179 pathToPost p = "posts" </> kebab (T.unpack (title p))
181 -- | Creates and saves a new post
183 -- ^ The title of the post
185 -- ^ The markdown body
187 -- ^ The freshly created post
188 createPost title md = do
193 Nothing -> raise "Not logged in"
194 curTime <- liftIO getCurrentTime
195 let p = Post md title curTime author
196 liftIO $ writeFile (pathToPost p) (show p)