gdritter repos new-inf-blog / 4ea169f
almost all the way done Getty Ritter 5 years ago
12 changed file(s) with 469 addition(s) and 88 deletion(s). Collapse all Expand all
1818 Inf.DB
1919 Inf.DB.Monad
2020 Inf.Feed
21 Inf.Log
2122 Inf.Web
23 Inf.Web.Cookies
2224 default-language: Haskell2010
2325 default-extensions: ScopedTypeVariables
2426 ghc-options: -Wall
3840 , xml-conduit
3941 , bytestring
4042 , pwstore-fast
43 , unix
33 module Inf.DB where
44
55 import Control.Monad (forM)
6 import qualified Data.ByteString as BS
67 import qualified Data.Text as T
78 import qualified Data.Time as Time
89
1112
1213 submitPost :: User -> RawPost -> DB.DB ()
1314 submitPost uname rp = case rpId rp of
14 Just (PostId n) | uname == rpAuthor rp -> do
15 Just (PostId n) | uname == rpAuthor rp ->
1516 DB.execute
1617 "UPDATE posts \
1718 \ SET title = ?, author = ?, contents = ? \
1819 \ WHERE id = ?"
19 [ DB.f (rpTitle rp), DB.f (userName (rpAuthor rp)), DB.f (rpContents rp), DB.f n]
20 DB.execute
21 "UPDATE lookup SET slug = ? WHERE post_id = ?"
22 [DB.f (slugify (rpTitle rp)), DB.f n]
20 [ DB.f (rpTitle rp)
21 , DB.f (userName (rpAuthor rp))
22 , DB.f (rpContents rp)
23 , DB.f n
24 ]
2325 Just _ -> DB.raise DB.BadUserError
2426 Nothing -> do
2527 now <- DB.io Time.getCurrentTime
6971
7072 newestPost :: DB.DB (Maybe Post)
7173 newestPost = do
72 values <- DB.queryMb "SELECT * FROM posts ORDER BY time DESC LIMIT 1" []
73 forM values $ \(pId, title, contents, author, date, next, prev) -> do
74 values <- DB.queryMb
75 "SELECT p.title, p.contents, p.author, p.time, p.next, p.prev, l.slug \
76 \ FROM posts p, lookup l WHERE p.id == l.post_id ORDER BY time DESC LIMIT 1"
77 []
78 forM values $ \(pId, title, contents, author, date, next, prev, slug) -> do
7479 nextRef <- mapM postRefById next
7580 prevRef <- mapM postRefById prev
7681 pure Post
7782 { postId = pId
7883 , postDate = date
7984 , postTitle = title
85 , postSlug = slug
8086 , postContents = contents
8187 , postAuthor = author
8288 , postNext = nextRef
118124
119125 allPosts :: DB.DB [Post]
120126 allPosts = do
121 values <- DB.query "SELECT id, time, title, contents, author FROM posts" []
122 pure [ Post pId date title content author Nothing Nothing
123 | (pId, date, title, content, author) <- values
127 values <- DB.query
128 "SELECT p.id, p.time, p.title, p.contents, p.author, l.slug \
129 \ FROM posts p, lookup l WHERE p.id == l.post_id " []
130 pure [ Post pId date title slug content author Nothing Nothing
131 | (pId, date, title, content, author, slug) <- values
124132 ]
125133
126134 allPostRefs :: DB.DB [PostRef]
138146 | (year, month, slug, title, date) <- posts
139147 ]
140148
149
141150 postByDateAndSlug :: T.Text -> T.Text -> T.Text -> DB.DB Post
142151 postByDateAndSlug year month slug = do
143152 values <- DB.queryMb
149158
150159 postById :: PostId -> DB.DB Post
151160 postById pId = do
152 (title, contents, author, date, next, prev)
153 <- DB.queryOne "SELECT title, contents, author, time, next, prev \
154 \ FROM posts WHERE id = ?" [DB.f pId]
161 (title, contents, author, date, next, prev, slug)
162 <- DB.queryOne
163 "SELECT p.title, p.contents, p.author, p.time, p.next, p.prev, l.slug \
164 \ FROM posts p, lookup l WHERE p.id = l.post_id AND p.id = ?" [DB.f pId]
155165 nextRef <- mapM postRefById next
156166 prevRef <- mapM postRefById prev
157167 pure Post
158168 { postId = pId
159169 , postDate = date
160170 , postTitle = title
171 , postSlug = slug
161172 , postContents = contents
162173 , postAuthor = author
163174 , postNext = nextRef
164175 , postPrev = prevRef
165176 }
177
178
179 userAuthData :: T.Text -> DB.DB (Maybe BS.ByteString)
180 userAuthData user =
181 fmap DB.fromOnly <$>
182 DB.queryMb "SELECT password FROM snap_auth_user WHERE login = ?"
183 [DB.f user]
1 module Inf.Log
2 ( debug
3 , info
4 , warn
5 , error
6 ) where
7
8 import Control.Monad (when)
9 import qualified Data.Time as Time
10 import qualified System.IO.Unsafe as Unsafe
11 import qualified System.Posix.IO as Unix
12 import qualified System.Posix.Terminal as Unix
13 import Prelude hiding (error)
14
15 isTTY :: Bool
16 isTTY = Unsafe.unsafePerformIO (Unix.queryTerminal (Unix.stdOutput))
17
18 timeFmt :: String
19 timeFmt = "[%Y-%m-%dT%H:%M:%S]"
20
21 timestamp :: IO String
22 timestamp = do
23 now <- Time.getCurrentTime
24 pure (Time.formatTime Time.defaultTimeLocale timeFmt now)
25
26 logMsg :: String -> [String] -> IO ()
27 logMsg color msg = do
28 now <- timestamp
29 when isTTY $ putStr color
30 putStr (unwords (now:msg))
31 when isTTY $ putStr "\x1b[39m"
32 putStrLn ""
33
34 debug :: [String] -> IO ()
35 debug = logMsg "\x1b[97m"
36
37 info :: [String] -> IO ()
38 info = logMsg "\x1b[94m"
39
40 warn :: [String] -> IO ()
41 warn = logMsg "\x1b[93m"
42
43 error :: [String] -> IO ()
44 error = logMsg "\x1b[91m"
44 ( main
55 , page
66 , list
7 , login
8 , edit
9 , post
10 , delete
711
812 , dbException
913 , someException
1014
1115 , markdown
16
17 , InternalError(..)
1218 ) where
1319
1420 import qualified Control.Exception as Exn
1521 import Control.Monad ((=<<))
1622 import Data.Aeson ((.=))
1723 import qualified Data.Aeson as Aeson
24 import Data.Monoid ((<>))
1825 import qualified Data.Text as TS
1926 import qualified Data.Text.Lazy as T
2027 import qualified Text.Mustache as Stache
2633 import qualified Text.Pandoc.Writers.HTML as Pandoc
2734
2835 import Inf.Types
36
37 class InternalError t where
38 renderedError :: t -> IO T.Text
39 loggedError :: t -> String
40
41 instance InternalError DBException where
42 renderedError = dbException
43 loggedError = show
44
45 instance InternalError Exn.SomeException where
46 renderedError = someException
47 loggedError = show
48
49 instance InternalError Unimplemented where
50 renderedError (Unimplemented _) =
51 main Nothing "Unimplemented" =<<
52 page "IMPLEMENT ME"
53
54 loggedError (Unimplemented msg) = "Unimplemented: " ++ msg
2955
3056 markdownOpts :: Pandoc.ReaderOptions
3157 markdownOpts = Pandoc.def
4268 pandoc <- Pandoc.readMarkdown markdownOpts raw
4369 Pandoc.writeHtml5String htmlOpts pandoc
4470
45 main :: T.Text -> T.Text -> IO T.Text
46 main title contents = do
71 main :: Maybe TS.Text -> T.Text -> T.Text -> IO T.Text
72 main user title contents = do
4773 template <- Stache.compileMustacheFile "templates/main.mustache"
4874 let vals = Aeson.object
4975 [ "title" .= title
5076 , "contents" .= contents
77 , "user" .= case user of
78 Nothing -> Aeson.toJSON ()
79 Just u -> Aeson.object [ "name" .= u ]
5180 ]
5281 pure (Stache.renderMustache template vals)
82
83 login :: IO T.Text
84 login = do
85 template <- Stache.compileMustacheFile "templates/login.mustache"
86 pure (Stache.renderMustache template (Aeson.object []))
5387
5488 page :: TS.Text -> IO T.Text
5589 page contents = do
5791 mk <- markdown contents
5892 let vals = Aeson.object
5993 [ "contents" .= mk ]
94 pure (Stache.renderMustache template vals)
95
96 nullable :: Maybe a -> (a -> [(TS.Text, Aeson.Value)]) -> Aeson.Value
97 nullable Nothing _ = Aeson.toJSON ()
98 nullable (Just x) k = Aeson.object (k x)
99
100 post :: Maybe TS.Text -> Post -> IO T.Text
101 post user ps = do
102 template <- Stache.compileMustacheFile "templates/post.mustache"
103 mk <- markdown (postContents ps)
104 let vals = Aeson.object
105 [ "contents" .= mk
106 , "user" .= nullable user (\_ ->
107 [ "editurl" .= (postURL ps <> "/edit")
108 , "delurl" .= (postURL ps <> "/delete")
109 ])
110 , "next" .= nullable (postNext ps) (\nxt ->
111 [ "link" .= postRefURL nxt ])
112 , "prev" .= nullable (postPrev ps) (\nxt ->
113 [ "link" .= postRefURL nxt ])
114 ]
115 pure (Stache.renderMustache template vals)
116
117 edit :: TS.Text -> RawPost -> IO T.Text
118 edit url ps = do
119 template <- Stache.compileMustacheFile "templates/edit.mustache"
120 let vals = Aeson.object
121 [ "id" .= maybe "" (show . fromPostId) (rpId ps)
122 , "title" .= rpTitle ps
123 , "author" .= userName (rpAuthor ps)
124 , "contents" .= rpContents ps
125 , "url" .= url
126 ]
127 pure (Stache.renderMustache template vals)
128
129 delete :: Post -> IO T.Text
130 delete ps = do
131 template <- Stache.compileMustacheFile "templates/delete.mustache"
132 let vals = Aeson.object
133 [ "post_url" .= postURL ps
134 ]
60135 pure (Stache.renderMustache template vals)
61136
62137 list :: [PostRef] -> IO T.Text
75150
76151
77152 dbException :: DBException -> IO T.Text
78 dbException exn = main "Error" =<< page (TS.unlines
153 dbException exn = main Nothing "Error" =<< page (TS.unlines
79154 ( "## Internal server error"
80155 : case exn of
81156 NoSuchPost -> [ "database misconfiguration" ]
88163 ))
89164
90165 someException :: Exn.SomeException -> IO T.Text
91 someException exn = main "Internal Sever Error" =<< page (TS.unlines
166 someException exn = main Nothing "Internal Sever Error" =<< page (TS.unlines
92167 [ "The following unexpected error occurred:"
93168 , "```"
94169 , TS.pack (show exn)
4343 { postId :: PostId
4444 , postDate :: Time.UTCTime
4545 , postTitle :: T.Text
46 , postSlug :: T.Text
4647 , postContents :: T.Text
4748 , postAuthor :: User
4849 , postNext :: Maybe PostRef
100101
101102
102103 postURL :: Post -> T.Text
103 postURL Post { postDate = date, postTitle = title } = do
104 postURL Post { postDate = date, postSlug = slug } = do
104105 let (year, month, _) = Time.toGregorian (Time.utctDay date) in
105106 T.concat [ "/", T.pack (show year)
106107 , "/", T.pack (show month)
107 , "/", slugify title
108 , "/", slug
108109 ]
109110
110111
112113 postRefDateT PostRef { prDate = date } =
113114 T.pack (Time.formatTime
114115 Time.defaultTimeLocale
115 Time.rfc822DateFormat
116 "%Y-%m-%d"
116117 date)
118
117119
118120 postRefURL :: PostRef -> T.Text
119121 postRefURL PostRef { prYear = year, prMonth = month, prSlug = slug } =
140142 }
141143
142144
143
144145 data DBException
145146 = MissingPost PostId
146147 | MissingPage T.Text
151152 deriving (Eq, Show)
152153
153154 instance Exn.Exception DBException where
155
156 data Unimplemented = Unimplemented String
157 deriving (Eq, Show)
1 {-# LANGUAGE OverloadedStrings #-}
2
3 module Inf.Web.Cookies
4 ( Cookies
5 , getUser
6 , verifyUser
7 , authCookie
8 , dumpCookies
9 , parseCookies
10 ) where
11
12 import qualified Crypto.PasswordStore as Password
13 import qualified Data.ByteString as BS
14 import qualified Data.Text as TS
15 import qualified Data.Text.Encoding as EncodingS
16
17 import qualified Inf.DB as DB
18 import qualified Inf.DB.Monad as DB
19 import qualified Inf.Log as Log
20
21 type Cookies = [(BS.ByteString, BS.ByteString)]
22
23 dumpCookies :: Cookies -> BS.ByteString
24 dumpCookies cks = do
25 BS.intercalate "; " [ BS.concat [k, "=", v]
26 | (k, v) <- cks
27 ]
28
29 -- | Split a cookie string represented in the usual way into a list of
30 -- key/value pairs
31 parseCookies :: BS.ByteString -> Maybe Cookies
32 parseCookies inp = do
33 let (chunk, rest) = BS.breakSubstring "; " inp
34 x <- case BS.breakSubstring "=" chunk of
35 (_, "") -> Nothing
36 (k, v) -> pure (k, BS.tail v)
37 xs <- case rest of
38 "" -> pure []
39 rs -> parseCookies (BS.drop 2 rs)
40 pure (x:xs)
41
42 authCookie :: BS.ByteString -> BS.ByteString -> Cookies
43 authCookie user pass =
44 [("USERDATA", BS.concat [user, ":", pass])]
45
46 verifyUser
47 :: DB.Connection
48 -> BS.ByteString
49 -> BS.ByteString
50 -> IO (Maybe TS.Text)
51 verifyUser conn user passwd = do
52 let userTxt = EncodingS.decodeUtf8 user
53 authMb <- DB.runDB (DB.userAuthData userTxt) conn
54 case authMb of
55 Nothing -> do
56 Log.warn [ "unable to find login data for", TS.unpack userTxt ]
57 pure Nothing
58 Just auth
59 | Password.verifyPassword passwd auth ->
60 pure (Just userTxt)
61 | otherwise -> do
62 Log.error [ "bad password for", TS.unpack userTxt ]
63 pure Nothing
64
65 -- | Find and verify a USERDATA cookie
66 getUser :: DB.Connection -> Maybe BS.ByteString -> IO (Maybe TS.Text)
67 getUser conn c = do
68 let userDataMb = c >>= parseCookies >>= lookup "USERDATA"
69 case userDataMb of
70 Nothing -> pure Nothing
71 Just userData -> do
72 let (name, pass) = BS.breakSubstring ":" userData
73 verifyUser conn name (BS.tail pass)
88 , raise404
99 , raise500
1010 , redirect
11 , redirectWithCookies
1112 , atomFeed
13
14 , formData
15 , module Web
1216 ) where
1317
14 import Control.Monad ((>=>))
1518 import qualified Control.Exception as Exn
19 import qualified Data.ByteString.Char8 as BS8
1620 import qualified Data.ByteString.Lazy as LBS
1721 import qualified Data.Text as TS
1822 import qualified Data.Text.Encoding as EncodingS
1923 import qualified Data.Text.Lazy as T
2024 import qualified Data.Text.Lazy.Encoding as Encoding
2125 import qualified Network.HTTP.Types as HTTP
26 import qualified Network.HTTP.Types.Header as HTTP
2227 import qualified Network.Wai as W
2328 import qualified Network.Wai.Handler.Warp as W
2429 import qualified System.Environment as Env
2530 import qualified Text.Read as Read
2631
2732 import qualified Inf.DB.Monad as DB
33 import qualified Inf.Log as Log
2834 import qualified Inf.Templates as Template
29 import Inf.Types
35 import Inf.Web.Cookies as Web
3036
3137 type Routes = DB.Connection -> W.Request -> IO W.Response
3238
39 formData :: W.Request -> IO HTTP.Query
40 formData =
41 fmap (HTTP.parseQuery . LBS.toStrict) . W.lazyRequestBody
42
3343 run :: Routes -> IO ()
3444 run routes = do
35 portVar <- Env.lookupEnv "port"
45 portVar <- Env.lookupEnv "PORT"
3646 let port = case portVar of
3747 Just x
3848 | Just p <- Read.readMaybe x -> p
3949 _ -> 8080
40 conn <- DB.open "test.db"
50 dbVar <- Env.lookupEnv "DB"
51 let dbLocation = case dbVar of
52 Nothing -> "test.db"
53 Just loc -> loc
54 Log.info [ "opening database", show dbLocation ]
55 conn <- DB.open dbLocation
56 Log.info [ "running on port", show port ]
4157 W.run port $ \ r k -> do
58 Log.debug [ show (W.remoteHost r)
59 , ":"
60 , BS8.unpack (W.requestMethod r)
61 , BS8.unpack (W.rawPathInfo r)
62 ]
4263 resp <- routes conn r
43 `Exn.catch` (Template.dbException >=> raise500)
44 `Exn.catch` (Template.someException >=> raise500)
64 `Exn.catch` (\ (e :: DB.DBException) -> raise500 r e)
65 `Exn.catch` (\ (e :: Exn.SomeException) -> raise500 r e)
4566 k resp
4667
47 ok :: T.Text -> T.Text -> IO W.Response
48 ok title stuff = do
49 pg <- Template.main title stuff
68 ok :: Maybe TS.Text -> T.Text -> T.Text -> IO W.Response
69 ok user title stuff = do
70 pg <- Template.main user title stuff
5071 let headers = [(HTTP.hContentType, "text/html")]
51 pure (W.responseLBS HTTP.status200 [] (Encoding.encodeUtf8 pg))
72 pure (W.responseLBS HTTP.status200 headers (Encoding.encodeUtf8 pg))
5273
53 raise404 :: [TS.Text] -> IO W.Response
54 raise404 msgs = do
74 raise404 :: W.Request -> [TS.Text] -> IO W.Response
75 raise404 req msgs = do
76 Log.warn [ " raised 404 for URL"
77 , BS8.unpack (W.rawPathInfo req)
78 ]
5579 contents <- Template.page $ TS.unwords
5680 [ "No handler found for page "
5781 , "`/" <> TS.intercalate "/" msgs <> "`"
5882 ]
59 pg <- Template.main "404" contents
83 pg <- Template.main Nothing "404" contents
6084 pure (W.responseLBS HTTP.status404 [] (Encoding.encodeUtf8 pg))
6185
62 raise500 :: T.Text -> IO W.Response
63 raise500 msgs =
64 let body = Encoding.encodeUtf8 msgs
65 in pure (W.responseLBS HTTP.status500 [] body)
86 raise500 :: Template.InternalError e => W.Request -> e -> IO W.Response
87 raise500 req err = do
88 Log.error [ " raised 500 for URL"
89 , BS8.unpack (W.rawPathInfo req)
90 , ":"
91 , Template.loggedError err
92 ]
93 body <- Encoding.encodeUtf8 `fmap` Template.renderedError err
94 pure (W.responseLBS HTTP.status500 [] body)
6695
6796 redirect :: TS.Text -> IO W.Response
6897 redirect url =
6998 let headers = [(HTTP.hLocation, EncodingS.encodeUtf8 url)]
70 in pure (W.responseLBS HTTP.status307 headers "redirecting...")
99 in pure (W.responseLBS HTTP.status303 headers "redirecting...")
100
101 redirectWithCookies :: TS.Text -> Web.Cookies -> IO W.Response
102 redirectWithCookies url cookies =
103 let headers = [ (HTTP.hLocation, EncodingS.encodeUtf8 url)
104 , (HTTP.hSetCookie, Web.dumpCookies cookies)
105 ]
106 in pure (W.responseLBS HTTP.status303 headers "redirecting...")
71107
72108 atomFeed :: LBS.ByteString -> IO W.Response
73109 atomFeed feed =
74110 let headers = [(HTTP.hContentType, "application/atom+xml")]
75111 in pure (W.responseLBS HTTP.status200 headers feed)
76
77
78 getUser :: DB.Connection -> Maybe BS.ByteString -> IO (Maybe TS.Text)
79 getUser _ Nothing = pure Nothing
80 getUser _ (Just c) = maybe (pure Nothing) validateUser authCookie
81 where
82 cookies = TS.splitOn "; " (EncodingS.decodeUtf8 c)
83 authCookie = lookup "user" cookies
22
33 module Main where
44
5 import qualified Data.ByteString as BS
5 import Control.Monad (join)
66 import qualified Data.Text as TS
77 import qualified Data.Text.Encoding as EncodingS
88 import qualified Data.Text.Lazy as T
1414 import qualified Inf.DB as DB
1515 import qualified Inf.Feed as Feed
1616 import qualified Inf.Templates as Template
17 import qualified Inf.Log as Log
1718 import Inf.Types
1819 import qualified Inf.Web as Web
1920
2021 main :: IO ()
2122 main = Web.run route
2223
23 getUser :: DB.Connection -> Maybe BS.ByteString -> IO (Maybe TS.Text)
24 getUser _ Nothing = pure Nothing
25 getUser _ (Just c) =
26 let _cookies = TS.splitOn "; " (EncodingS.decodeUtf8 c)
27 in pure Nothing
28
2924 route :: Web.Routes
3025 route c req = do
31 _user <- getUser c (lookup HTTP.hCookie (W.requestHeaders req))
26 user <- Web.getUser c (lookup HTTP.hCookie (W.requestHeaders req))
3227 case (W.requestMethod req, W.pathInfo req) of
3328 ("GET", []) -> do
3429 post <- DB.runDB DB.newestPostRef c
3530 Web.redirect (postRefURL post)
3631
37 ("POST", []) -> undefined
32 ("POST", []) ->
33 case user of
34 Nothing -> do
35 Log.error [ "non-logged-in user attempted to post something" ]
36 Web.redirect "/"
37 Just u -> do
38 formData <- Web.formData req
39 let rpMb = do
40 title <- join (lookup "title" formData)
41 contents <- join (lookup "contents" formData)
42 pure RawPost
43 { rpId = Nothing
44 , rpTitle = EncodingS.decodeUtf8 title
45 , rpAuthor = User u
46 , rpContents = EncodingS.decodeUtf8 contents
47 }
48 case rpMb of
49 Nothing -> do
50 Log.error [ "bad input data somehow" ]
51 Web.redirect "/"
52 Just rp -> do
53 DB.runDB (DB.submitPost (User u) rp) c
54 Web.redirect "/newest"
3855
39 ("GET", ["auth"]) -> undefined
56 ("GET", ["auth"]) -> do
57 password <- Template.login
58 Web.ok user "login" password
4059
4160 ("GET", ["archive"]) -> do
4261 posts <- DB.runDB DB.allPostRefs c
4362 contents <- Template.list posts
44 Web.ok "Past Entries" contents
63 Web.ok user "Past Entries" contents
4564
46 ("GET", ["create"]) -> undefined
65 ("GET", ["create"]) ->
66 case user of
67 Nothing -> Web.redirect "/"
68 Just _ -> do
69 contents <- Template.edit "/" emptyRawPost
70 Web.ok user "New Post" contents
4771
4872 ("GET", [y, m, s]) -> do
4973 post <- DB.runDB (DB.postByDateAndSlug y m s) c
50 contents <- Template.page (postContents post)
51 Web.ok (T.fromStrict (postTitle post)) contents
74 contents <- Template.post user post
75 Web.ok user (T.fromStrict (postTitle post)) contents
5276
53 ("POST", [_y, _m, _s]) -> undefined
77 ("POST", [y, m, s]) ->
78 case user of
79 Nothing -> do
80 Log.error [ "non-logged-in user attempted to edit post" ]
81 Web.redirect "/"
82 Just u -> do
83 formData <- Web.formData req
84 oldPost <- DB.runDB (DB.postByDateAndSlug y m s) c
85 let rpMb = do
86 title <- join (lookup "title" formData)
87 contents <- join (lookup "contents" formData)
88 pure RawPost
89 { rpId = Just (postId oldPost)
90 , rpTitle = EncodingS.decodeUtf8 title
91 , rpAuthor = User u
92 , rpContents = EncodingS.decodeUtf8 contents
93 }
94 case rpMb of
95 Nothing -> do
96 Log.error [ "bad input data somehow" ]
97 Web.redirect "/"
98 Just rp -> do
99 DB.runDB (DB.submitPost (User u) rp) c
100 Web.redirect (postURL oldPost)
54101
55 ("GET", [_y, _m, _s, "edit"]) -> undefined
102 ("DELETE", [y, m, s]) ->
103 case user of
104 Nothing -> Web.redirect "/"
105 Just _ -> do
106 Log.warn [ "deleting post!" ]
107 flip DB.runDB c $ do
108 post <- DB.postByDateAndSlug y m s
109 DB.deletePost (postToRawPost post)
110 Web.redirect "/"
56111
57 ("GET", [_y, _m, _s, "delete"]) -> undefined
112 ("GET", [y, m, s, "edit"]) ->
113 case user of
114 Nothing -> do
115 Log.error [ "non-logged-in user attempted to edit post" ]
116 Web.redirect "/"
117 Just {} -> do
118 oldPost <- DB.runDB (DB.postByDateAndSlug y m s) c
119 contents <- Template.edit (postURL oldPost) (postToRawPost oldPost)
120 Web.ok user (T.fromStrict (postTitle oldPost)) contents
58121
59 ("GET", ["login"]) -> undefined
122 ("GET", [y, m, s, "delete"]) ->
123 case user of
124 Nothing -> Web.redirect "/"
125 Just _ -> do
126 post <- DB.runDB (DB.postByDateAndSlug y m s) c
127 contents <- Template.delete post
128 Web.ok user "Delete Post?" contents
60129
61 ("GET", ["logout"]) -> undefined
130 ("POST", ["login"]) -> do
131 formData <- Web.formData req
132 Log.warn [ "attempting login"
133 , case lookup "user" formData of
134 Just (Just u) -> "of user " ++ show u
135 _ -> "without a specified user"
136 ]
137 case (lookup "user" formData, lookup "passwd" formData) of
138 (Just (Just u), Just (Just p)) -> do
139 userMb <- Web.verifyUser c u p
140 case userMb of
141 Just _ ->
142 let cookies = Web.authCookie u p
143 in Web.redirectWithCookies "/" cookies
144 Nothing -> Web.raise500 req (Unimplemented "Bad Password")
145 _ -> Web.raise500 req (Unimplemented "Bad Login")
146
147 ("GET", ["logout"]) ->
148 Web.redirectWithCookies "/" [("USERDATA", "")]
62149
63150 ("GET", ["newest"]) -> do
64151 post <- DB.runDB DB.newestPostRef c
80167 ("GET", [pagename]) -> do
81168 pg <- DB.runDB (DB.staticPage pagename) c
82169 contents <- Template.page (pageText pg)
83 Web.ok (T.fromStrict pagename) contents
170 Web.ok user (T.fromStrict pagename) contents
84171
85 ("GET", [_page, "edit"]) -> undefined
172 ("GET", [_page, "edit"]) ->
173 case user of
174 Nothing -> Web.redirect "/"
175 Just _ -> do
176 contents <- Template.edit "/" emptyRawPost
177 Web.ok user "New Post" contents
86178
87 (_, path) -> Web.raise404 path
179 (_, path) -> Web.raise404 req path
44 margin-right: 0px;
55 background-color: #FFFFFF;
66 color: #222222;
7 font-size: large;
7 font-size: 18pt;
8 line-height: 1.5;
89 }
910
1011 a:link { color: #bbbbbb; }
1112 a:hover { color: #bbbbbb; }
1213 a:active { color: #999999; }
1314 a:visited { color: #999999; }
15
16 .postnav {
17 width: 50%;
18 margin-left: auto;
19 margin-right: auto;
20 padding: 20px;
21 overflow: auto;
22 }
1423
1524 .username {
1625 width: 100%;
125134 }
126135
127136 .new {
128 display: inline-block;
129 width: 50%;
130 text-align: left;
137 float: left;
138 background-color: #444;
139 color: #fff;
140 padding: 12px;
141 height: 100%;
131142 }
132143
144 .new a { #fff; }
145 .new a visited { #fff; }
146
133147 .old {
134 display: inline-block;
135 width: 50%;
136 text-align: right;
148 float: right;
149 background-color: #444;
150 color: #fff;
151 padding: 12px;
152 height: 100%;
153 }
154
155 .old a { #fff; }
156 .old a visited { #fff; }
157
158 .inactive {
159 color: #ddd;
137160 }
138161
139162 pre {
146169
147170 code {
148171 padding: 2px;
149 }
172 }
11 <div class="edit">
2 <form name="newpost" action="/" method="POST"
2 <form name="newpost" action="{{url}}" method="POST"
33 enctype="application/x-www-form-urlencoded;charset=UTF-8">
44 <input type="hidden" name="id" value="{{id}}"/>
55 <input type="text" name="title" value="{{title}}"/>
11 <div class="list">
22 {{#posts}}
3 <p><a href={{url}}>{{title}}</a> <span class="date">on {{date}}</span></p>
3 <p><a href={{url}}>{{title}}</a> <span class="date"> &mdash; {{date}}</span></p>
44 {{/posts}}
55 </div>
1 <div class="postbody">
2
1 <div class="post">
2 <div class="postbody">
3 {{{contents}}}
4 </div>
5 {{#user}}
6 <div class="editlink">
7 <a href="{{editurl}}">Edit this post</a>
8 </div>
9 <div class="editlink">
10 <a href="{{delurl}}">Delete this post</a>
11 </div>
12 {{/user}}
313 </div>
4 <div class="new">
5 {{#next}}
6 {{/next}}
14
15 <div class="postnav">
16 <span class="new">
17 {{#next}}
18 <a href="{{link}}">Newer</a>
19 {{/next}}
20 {{^next}}
21 <span class="inactive">Newer</span>
22 {{/next}}
23 </span>
24 <span class="old">
25 {{#prev}}
26 <a href="{{link}}">Older</a>
27 {{/prev}}
28 {{^prev}}
29 <span class="inactive">Older</span>
30 {{/prev}}
31 </span>
732 </div>
8 <div class="old">
9 {{#prev}}
10 {{/prev}}
11 </div>