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|
{visits}
- -|] + visits <- show . length <$> getVisits + return [hsx| +{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|