diff --git a/app/Api/Api.hs b/app/Api/Api.hs index 8567a0b..5568d0a 100644 --- a/app/Api/Api.hs +++ b/app/Api/Api.hs @@ -49,133 +49,100 @@ jsonHeaders :: [Header] jsonHeaders = [("Content-Type", "application/json")] messageResponse :: String -> String -messageResponse value = - j2s - [aesonQQ|{ +messageResponse value = j2s [aesonQQ|{ "message":#{value} }|] apiMap :: [APIRoute] -apiMap = - [ - ( "POST" - , - [ - ( "/visits/new" - , \r -> do +apiMap = [ + ("POST", [ + ("/visits/new", \r -> do body <- getRequestBodyChunk r result <- uuidExists $ unpackBS body - res <- - if result - then do - time <- fmap round getPOSIXTime :: IO Int - uuid <- nextRandom - runDb $ insertEntity $ Visit 0 time $ toString uuid - return $ toString uuid - else - return $ unpackBS body + res <- if result then do + time <- fmap round getPOSIXTime :: IO Int + uuid <- nextRandom + runDb $ insertEntity $ Visit 0 time $ toString uuid + return $ toString uuid + else + return $ unpackBS body return (status200, j2s [aesonQQ|{"uuid":#{res}}|], jsonHeaders) - ) - , - ( "/admin/login" - , \r -> do + ), + ("/admin/login", \r -> do body <- getRequestBodyChunk r let credentials = getDefault T.EmptyCredentials (decode (fromStrict body) :: Maybe T.Credentials) case credentials of - (T.Credentials username password) -> do - let pass = mkPassword $ pack password - rows <- map (\(Entity _ e) -> e) <$> (runDb $ selectList [Filter UserName (FilterValue username) (BackendSpecificFilter "LIKE")] [] :: IO [Entity User]) - case rows of - [user] -> case checkPassword pass (PasswordHash $ pack (userPassword user)) of - PasswordCheckSuccess -> do - rows <- map (\(Entity _ e) -> e) <$> (runDb $ selectList [Filter TokenName (FilterValue username) (BackendSpecificFilter "LIKE")] [] :: IO [Entity Token]) - response <- - if null rows - then do - token <- stringRandomIO "[0-9a-zA-Z]{4}-[0-9a-ZA-Z]{10}-[0-9a-zA-Z]{15}" - runDb $ insertEntity $ Token 0 (unpack token) username - return $ unpack token - else return $ tokenToken $ head rows - return (status200, j2s [aesonQQ|{"token":#{response}}|], jsonHeaders) - PasswordCheckFail -> return (status400, messageResponse "Error, Wrong username or password", jsonHeaders) - _ -> return (status400, messageResponse "Error, no user exists", jsonHeaders) - _ -> return (status400, messageResponse "Error, Invalid request", jsonHeaders) - ) - , - ( "/brainfuck" - , \r -> do + (T.Credentials username password) -> do + let pass = mkPassword $ pack password + rows <- map (\(Entity _ e) -> e) <$> (runDb $ selectList [Filter UserName (FilterValue username) (BackendSpecificFilter "LIKE")] [] :: IO [Entity User]) + case rows of + [user] -> case checkPassword pass (PasswordHash $ pack (userPassword user)) of + PasswordCheckSuccess -> do + rows <- map (\(Entity _ e) -> e) <$> (runDb $ selectList [Filter TokenName (FilterValue username) (BackendSpecificFilter "LIKE")] [] :: IO [Entity Token]) + response <- if null rows then do + token <- stringRandomIO "[0-9a-zA-Z]{4}-[0-9a-ZA-Z]{10}-[0-9a-zA-Z]{15}" + runDb $ insertEntity $ Token 0 (unpack token) username + return $ unpack token + else return $ tokenToken $ head rows + return (status200, j2s [aesonQQ|{"token":#{response}}|], jsonHeaders) + PasswordCheckFail -> return (status400, messageResponse "Error, Wrong username or password", jsonHeaders) + _ -> return (status400, messageResponse "Error, no user exists", jsonHeaders) + _ -> return (status400, messageResponse "Error, Invalid request", jsonHeaders) + ), + ("/brainfuck", \r -> do input <- getRequestBodyChunk r let result = code $ unpackBS input return (status200, result, [("Content-Disposition", "attachment; filename=\"brainfuck.c\"")]) ) - ] - ) - , - ( "GET" - , - [ - ( "/visits/get" - , \_ -> do + ]), + ("GET", [ + ("/visits/get", \_ -> do visits <- show . length <$> getVisits return (status200, j2s [aesonQQ|{"visits":#{visits}}|], jsonHeaders) - ) - , - ( "/guestbook/get" - , \_ -> do + ), + ("/guestbook/get", \_ -> do entries <- getGuestbook return (status200, j2s [aesonQQ|{"entries":#{unpackBS $ toStrict $ encode $ show entries}}|], jsonHeaders) ) - ] - ) - , - ( "PUT" - , - [ - ( "/guestbook/add" - , \r -> do + ]), + ("PUT", [ + ("/guestbook/add", \r -> do body <- getRequestBodyChunk r let entry = getDefault T.EmptyGuestbook (decode (fromStrict body) :: Maybe T.GuestbookEntry) case entry of - (T.GuestbookEntry "" _ _) -> return (status400, messageResponse "Error, name cannot be empty", jsonHeaders) - (T.GuestbookEntry _ "" _) -> return (status400, messageResponse "Error, content cannot be empty", jsonHeaders) - (T.GuestbookEntry name content parentId) -> do - time <- fmap round getPOSIXTime :: IO Int - runDb $ insertEntity $ GuestbookEntry 0 time name content parentId - return (status200, messageResponse "Success", jsonHeaders) - ) - , - ( "/snake/add" - , \r -> do + (T.GuestbookEntry "" _ _) -> + return (status400, messageResponse "Error, name cannot be empty", jsonHeaders) + (T.GuestbookEntry _ "" _) -> + return (status400, messageResponse "Error, content cannot be empty", jsonHeaders) + (T.GuestbookEntry name content parentId) -> do + time <- fmap round getPOSIXTime :: IO Int + runDb $ insertEntity $ GuestbookEntry 0 time name content parentId + return (status200, messageResponse "Success", jsonHeaders) + ), + ("/snake/add", \r -> do body <- getRequestBodyChunk r let entry = getDefault T.EmptyLeaderboard (decode (fromStrict body) :: Maybe T.LeaderboardEntry) case entry of - T.EmptyLeaderboard -> return (status400, messageResponse "Error, leaderboard empty", jsonHeaders) - (T.LeaderboardEntry name score speed fruits) -> do - time <- fmap round getPOSIXTime :: IO Int - runDb $ insertEntity $ Snake 0 time name score speed fruits - return (status200, messageResponse "Success", jsonHeaders) + T.EmptyLeaderboard -> return (status400, messageResponse "Error, leaderboard empty", jsonHeaders) + (T.LeaderboardEntry name score speed fruits) -> do + time <- fmap round getPOSIXTime :: IO Int + runDb $ insertEntity $ Snake 0 time name score speed fruits + return (status200, messageResponse "Success", jsonHeaders) ) - ] - ) - , - ( "DELETE" - , [] - ) - ] + ]), + ("DELETE", []) + ] api :: Request -> APIResponse api request = do - let method = unpackBS $ requestMethod request - let path = pathInfo request - case find (\(name, _) -> name == method) apiMap of - (Just (_, endpoints)) -> case find - ( \(regex, _) -> - ( case matchRegex (mkRegex ("api" ++ regex)) $ unpack (intercalate "/" path) of - Nothing -> False - _ -> True - ) - ) - endpoints of - (Just (_, f)) -> f request - Nothing -> return (status400, messageResponse "Error, no endpoint found", defaultHeaders) - Nothing -> return (status400, messageResponse "Error, no endpoint found", defaultHeaders) + let method = unpackBS $ requestMethod request + let path = pathInfo request + case find (\(name, _) -> name == method) apiMap of + (Just (_, endpoints)) -> case find (checkEndpoint path) endpoints of + (Just (_, f)) -> f request + Nothing -> return (status400, messageResponse "Error, no endpoint found", defaultHeaders) + Nothing -> return (status400, messageResponse "Error, no endpoint found", defaultHeaders) + where + checkEndpoint path (regex, _) = case matchRegex (mkRegex ("api" ++ regex)) $ unpack (intercalate "/" path) of + Nothing -> False + _ -> True diff --git a/app/Footer.hs b/app/Footer.hs index 76be0ee..bb1c392 100644 --- a/app/Footer.hs +++ b/app/Footer.hs @@ -4,8 +4,7 @@ import IHP.HSX.QQ (hsx) import Text.Blaze.Html (Html) footer :: Html -footer = - [hsx| +footer = [hsx|
|] diff --git a/app/Header.hs b/app/Header.hs index 0eb93cd..e7c57f4 100644 --- a/app/Header.hs +++ b/app/Header.hs @@ -5,8 +5,7 @@ import Text.Blaze.Html (Html) makeLinks :: [(String, String)] -> Html makeLinks [] = [hsx||] -makeLinks [(display, url)] = - [hsx| +makeLinks [(display, url)] = [hsx| {display} | |] -makeLinks ((display, url) : xs) = - [hsx| +makeLinks ((display, url) : xs) = [hsx| {display} | {makeLinks xs} |] header :: [(String, String)] -> Html -header links = - [hsx| +header links = [hsx|
{makeLinks links}
diff --git a/app/Index.hs b/app/Index.hs index d37a7bf..59addfb 100644 --- a/app/Index.hs +++ b/app/Index.hs @@ -10,9 +10,7 @@ import Page (Page, PageSetting (Description, EmbedImage, EmbedText, Route)) import Section (section) intro :: Html -intro = - section - [hsx| +intro = section [hsx| Welcome to my home page, returning visitors might notice that the same layout that i always use is still present, and yes the site is still written in Haskell.

I was using the whole IHP development suite to work on the site originally, but i have a problem with that framework: its huge and very overkill for what i'm trying to do. @@ -42,30 +40,29 @@ intro = page :: IO Html page = do - visits <- show . length <$> getVisits - return - [hsx| -

Skademaskinen

- -
-
- {intro} - Visitors:

{visits}

- -|] + visits <- show . length <$> getVisits + return [hsx| +

Skademaskinen

+ +
+
+ {intro} + Visitors:

{visits}

+ + |] settings :: [PageSetting] -settings = - [ Route "/" - , Description "This is the front page for Skademaskinen, a server built as a passion project." - , EmbedImage "/static/icon.png" - , EmbedText "Skademaskinen - Index" - ] +settings = [ + Route "/", + Description "This is the front page for Skademaskinen, a server built as a passion project.", + EmbedImage "/static/icon.png", + EmbedText "Skademaskinen - Index" + ] index :: Page index = (settings, const $ layout <$> page) diff --git a/app/Layout.hs b/app/Layout.hs index 176adcd..1f070c7 100644 --- a/app/Layout.hs +++ b/app/Layout.hs @@ -7,8 +7,7 @@ import Footer (footer) import Header (header) layout :: Html -> Html -layout content = - [hsx| +layout content = [hsx| diff --git a/app/Main.hs b/app/Main.hs index eef9699..a504a97 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -49,94 +49,72 @@ serve content = responseBuilder status200 [("Content-Type", "text/html")] $ copy autoContentType :: String -> (HeaderName, ByteString) autoContentType path = ("Content-Type", mime extension) - where - extension = last (splitOn "." path) - mime "js" = "application/javascript" - mime "png" = "image/png" - mime "svg" = "image/svg+xml" - mime "css" = "text/css" - mime _ = "text/plain" + where + extension = last (splitOn "." path) + mime "js" = "application/javascript" + mime "png" = "image/png" + mime "svg" = "image/svg+xml" + mime "css" = "text/css" + mime _ = "text/plain" serveFile :: String -> IO Response serveFile path = do - info "Serving file" - exists <- doesFileExist path - if exists - then do - info "File exists" - - return $ responseFile status200 [autoContentType path] path Nothing + info "Serving file" + exists <- doesFileExist path + if exists then do + info "File exists" + return $ responseFile status200 [autoContentType path] path Nothing else do - warning "No file found!" - return $ responseLBS status404 [("Content-Type", "text/json")] $ encode [aesonQQ|{"error":"Error, file not found"}|] + warning "No file found!" + return $ responseLBS status404 [("Content-Type", "text/json")] $ encode [aesonQQ|{"error":"Error, file not found"}|] app :: Request -> (Response -> IO b) -> IO b app request respond = do - let xs = map unpack $ pathInfo request - let x = if null xs then "" else head xs - let args = "/" ++ intercalate "/" xs - response <- - if x == "static" - then do + let xs = map unpack $ pathInfo request + let x = if null xs then "" else head xs + let args = "/" ++ intercalate "/" xs + response <- if x == "static" then do -- If the requested content is a file serveFile $ intercalate "/" xs - else - if x == "favicon.ico" - then do - -- If the requested file is the icon file - serveFile "static/favicon.ico" - else - if x == "api" - then do - -- If the request is to the API - (status, value, headers) <- api request - return $ responseBuilder status headers $ copyByteString (fromString value) - else do - -- If the content is to the HTML Frontend - let (settings, page) = findPage args - result <- page request - let image = - if embedImage settings /= "" - then - [hsx| + else if x == "favicon.ico" then do + -- If the requested file is the icon file + serveFile "static/favicon.ico" + else if x == "api" then do + -- If the request is to the API + (status, value, headers) <- api request + return $ responseBuilder status headers $ copyByteString (fromString value) + else do + -- If the content is to the HTML Frontend + let (settings, page) = findPage args + result <- page request + let image = if embedImage settings /= "" then [hsx| - |] - else [hsx||] - let text = - if embedText settings /= "" - then - [hsx| + |] else [hsx||] + let text = if embedText settings /= "" then [hsx| - |] - else [hsx||] - let desc = - if description settings /= "" - then - [hsx| + |] else [hsx||] + let desc = if description settings /= "" then [hsx| - |] - else [hsx||] + |] else [hsx||] - return $ serve (mconcat [result, image, text, desc]) + return $ serve (mconcat [result, image, text, desc]) - logger request response - respond response + logger request response + respond response main :: IO () main = do - port <- getPort - info $ "Listening on " ++ show port - putStrLn $ "+" ++ mconcat (replicate 65 "-") ++ "+" - putStrLn $ tableify ["METHOD", "STATUS", "PATH"] - putStrLn $ "+" ++ mconcat (replicate 65 "-") ++ "+" - migrate <- getMigrate - if migrate - then - doMigration + port <- getPort + info $ "Listening on " ++ show port + putStrLn $ "+" ++ mconcat (replicate 65 "-") ++ "+" + putStrLn $ tableify ["METHOD", "STATUS", "PATH"] + putStrLn $ "+" ++ mconcat (replicate 65 "-") ++ "+" + migrate <- getMigrate + if migrate then + doMigration else do - cliState <- getCliState - if cliState - then do - forkIO $ run port app - repl + cliState <- getCliState + if cliState then do + forkIO $ run port app + repl else run port app diff --git a/app/Pages/Admin/Admin.hs b/app/Pages/Admin/Admin.hs index 9fb0ad3..7dee943 100644 --- a/app/Pages/Admin/Admin.hs +++ b/app/Pages/Admin/Admin.hs @@ -10,15 +10,13 @@ import Page (Page, PageSetting (Description, Route), getArgs) import Text.Blaze.Html (Html) nameRow :: [String] -> Html -nameRow names = - [hsx| +nameRow names = [hsx| {mconcat $ map element names} |] - where - element name = - [hsx| + where + element name = [hsx| {name} @@ -26,18 +24,16 @@ nameRow names = visitsTable :: IO Html visitsTable = do - rows <- getVisits - return - [hsx| + rows <- getVisits + return [hsx|

Visits

{nameRow ["id", "timestamp", "uuid"]} {mconcat $ map makeRow rows}
|] - where - makeRow (Visit id timestamp uuid) = - [hsx| + where + makeRow (Visit id timestamp uuid) = [hsx| {id} {timestamp} @@ -47,18 +43,16 @@ visitsTable = do guestbookTable :: IO Html guestbookTable = do - rows <- getGuestbookEntries - return - [hsx| + rows <- getGuestbookEntries + return [hsx|

Guestbook

{nameRow ["id", "timestamp", "name", "content", "parentId"]} {mconcat $ map makeRow rows}
|] - where - makeRow (GuestbookEntry id timestamp name content parent) = - [hsx| + where + makeRow (GuestbookEntry id timestamp name content parent) = [hsx| {id} {timestamp} @@ -70,18 +64,16 @@ guestbookTable = do snakeTable :: IO Html snakeTable = do - rows <- getLeaderboard - return - [hsx| + rows <- getLeaderboard + return [hsx|

Snake Leaderboard

{nameRow ["id", "timestamp", "name", "score", "speed", "fruits"]} {mconcat $ map makeRow rows}
|] - where - makeRow (Snake id timestamp name score speed fruits) = - [hsx| + where + makeRow (Snake id timestamp name score speed fruits) = [hsx| {id} {timestamp} @@ -94,18 +86,16 @@ snakeTable = do usersTable :: IO Html usersTable = do - rows <- getUsers - return - [hsx| + rows <- getUsers + return [hsx|

Users

{nameRow ["id", "username", "password"]} {mconcat $ map makeRow rows}
|] - where - makeRow (User id username password) = - [hsx| + where + makeRow (User id username password) = [hsx| {id} {username} @@ -115,18 +105,16 @@ usersTable = do validTokensTable :: IO Html validTokensTable = do - rows <- getTokens - return - [hsx| + rows <- getTokens + return [hsx|

Valid Tokens

{nameRow ["id", "token", "username"]} {mconcat $ map makeRow rows}
|] - where - makeRow (Token id token username) = - [hsx| + where + makeRow (Token id token username) = [hsx| {id} {token} @@ -136,43 +124,40 @@ validTokensTable = do page :: [String] -> IO Html page ["summary", token] = do - validity <- validateToken token - if validity - then do - username <- tokenToUsername token - return - [hsx| + validity <- validateToken token + if validity then do + username <- tokenToUsername token + return [hsx|

Welcome {username}!

Schema: - {codeBlock "txt" prettyPrintSchema} -

Database tables

- Visits
- Guestbook
- Snake
- Users
- Valid tokens
- All + {codeBlock "txt" prettyPrintSchema} +

Database tables

+ Visits
+ Guestbook
+ Snake
+ Users
+ Valid tokens
+ All |] else - page [] + page [] page ["dump", table, token] = do - validity <- validateToken token - if validity - then showTable table + validity <- validateToken token + if validity then + showTable table else - page [] - where - showTable "visits" = visitsTable - showTable "guestbook" = guestbookTable - showTable "snake" = snakeTable - showTable "users" = usersTable - showTable "tokens" = validTokensTable - showTable "all" = mconcat [visitsTable, guestbookTable, snakeTable, usersTable, validTokensTable] - showTable _ = [hsx||] + page [] + where + showTable "visits" = visitsTable + showTable "guestbook" = guestbookTable + showTable "snake" = snakeTable + showTable "users" = usersTable + showTable "tokens" = validTokensTable + showTable "all" = mconcat [visitsTable, guestbookTable, snakeTable, usersTable, validTokensTable] + showTable _ = [hsx||] page x = do - print x - return - [hsx| + print x + return [hsx| |] initStorage :: String -> Html -initStorage label = - [hsx| +initStorage label = [hsx| |] fetchGet :: String -> String -> (String -> Html) -> Html -fetchGet label url callback = - [hsx| - {initStorage label} - {callback label} +fetchGet label url callback = [hsx| + {initStorage label} + {callback label} @@ -50,9 +46,8 @@ alertRequestApi url = fetchGet "temp" url callback showText :: String -> String -> Html showText label url = fetchGet label url callback - where - callback label = - [hsx| + where + callback label = [hsx|