ebook-manager/backend/src/API/Books.hs

130 lines
5.6 KiB
Haskell
Raw Normal View History

2018-11-12 21:32:42 +02:00
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
2018-08-05 23:42:37 +03:00
module API.Books where
2018-10-17 23:51:30 +03:00
import ClassyPrelude
import Control.Lens
2018-11-12 21:32:42 +02:00
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.Trans.Maybe
import Crypto.Hash (digestFromByteString)
2018-10-17 23:51:30 +03:00
import Data.Aeson
2018-11-12 21:32:42 +02:00
import Data.ByteArray (convert)
2018-10-17 23:51:30 +03:00
import Data.Generics.Product
import Database
import Database.Book
import Database.Channel
import Database.Tag
2018-11-12 21:32:42 +02:00
import qualified Datastore as DS
import Servant hiding (contentType)
import Servant.Auth as SA
import qualified Servant.Docs as Docs
2018-10-17 23:51:30 +03:00
import Server.Auth
import Types
2018-08-05 23:42:37 +03:00
2018-11-12 21:32:42 +02:00
data JsonBook = JsonBook { identifier :: BookID
2018-08-07 23:25:21 +03:00
, contentType :: Text
2018-11-12 21:32:42 +02:00
, title :: Text
2018-08-07 23:25:21 +03:00
, description :: Maybe Text
2018-11-12 21:32:42 +02:00
, channels :: [Text]
, tags :: [Text] }
2018-10-26 23:59:06 +03:00
deriving (Generic, Show, Eq)
2018-08-07 23:25:21 +03:00
2018-11-12 21:32:42 +02:00
instance Docs.ToSample JsonBook where
toSamples _ = [("Book", JsonBook 13 "epub" "title" (Just "Description") [] [])]
instance Docs.ToSample PostBook where
toSamples _ = [("Book", PostBook "epub" "title" (Just "Description") [] [])]
2018-08-07 23:25:21 +03:00
data PostBook = PostBook { contentType :: Text
2018-11-12 21:32:42 +02:00
, title :: Text
2018-08-05 23:42:37 +03:00
, description :: Maybe Text
2018-11-12 21:32:42 +02:00
, channels :: [Text]
, tags :: [Text] }
2018-10-26 23:59:06 +03:00
deriving (Generic, Show, Eq)
2018-08-05 23:42:37 +03:00
2018-08-06 00:09:41 +03:00
2018-08-05 23:42:37 +03:00
instance ToJSON JsonBook
instance FromJSON JsonBook
2018-08-07 23:25:21 +03:00
instance ToJSON PostBook
instance FromJSON PostBook
2018-08-05 23:42:37 +03:00
type API = Auth '[SA.BasicAuth, SA.JWT] SafeUser :> BaseAPI
2018-11-12 21:32:42 +02:00
instance Docs.ToCapture (Capture "book_id" BookID) where
toCapture _ = Docs.DocCapture "book_id" "The book id"
2018-08-05 23:42:37 +03:00
type BaseAPI = "books" :> Get '[JSON] [JsonBook]
2018-08-07 23:25:21 +03:00
:<|> "books" :> ReqBody '[JSON] PostBook :> Post '[JSON] JsonBook
2018-08-08 23:56:16 +03:00
:<|> "books" :> Capture "book_id" BookID :> "meta" :> ReqBody '[JSON] JsonBook :> Put '[JSON] JsonBook
2018-11-12 21:32:42 +02:00
:<|> "books" :> Capture "book_id" BookID :> ReqBody '[OctetStream] FileContent :> Put '[JSON] NoContent
:<|> GetBook
2018-11-12 21:32:42 +02:00
newtype FileContent = FileContent { getFileContent :: ByteString } deriving (MimeUnrender OctetStream, MimeRender OctetStream )
instance Docs.ToSample FileContent where
toSamples _ = [("File contents", FileContent "bytes here and there")]
type GetBook = "books" :> Capture "book_id" BookID :> Get '[OctetStream] FileContent
2018-08-05 23:42:37 +03:00
handler :: ServerT API AppM
2018-08-08 23:56:16 +03:00
handler user = listBooksHandler user
:<|> postBookMetaHandler user
:<|> putBookMetaHandler user
:<|> putBookContentHandler user
:<|> getBookContentHandler user
2018-11-12 21:32:42 +02:00
getBookContentHandler :: AuthResult SafeUser -> BookID -> AppM FileContent
2018-08-08 23:56:16 +03:00
getBookContentHandler auth bookId = requireBookOwner auth bookId $ \SafeUser{username} -> do
content <- runMaybeT $ do
Book{contentHash=mHash} <- MaybeT $ runDB (getBook bookId username)
contentHash <- MaybeT $ return (mHash >>= digestFromByteString . unHex)
2018-11-12 21:32:42 +02:00
FileContent <$> MaybeT (DS.get contentHash)
2018-08-08 23:56:16 +03:00
maybe (throwM err404) return content
requireBookOwner :: AuthResult SafeUser -> BookID -> (SafeUser -> AppM a) -> AppM a
requireBookOwner auth bookId f = flip requireLoggedIn auth $ \u@SafeUser{username} -> do
exists <- runDB $ bookExists bookId
unless exists $ throwM err404
runDB (isBookOwner bookId username) >>= \o -> if o then f u else throwM err403
2018-11-12 21:32:42 +02:00
putBookContentHandler :: AuthResult SafeUser -> BookID -> FileContent -> AppM NoContent
putBookContentHandler auth bookId fc = requireBookOwner auth bookId $ \SafeUser{username} -> do
let content = getFileContent fc
2018-08-08 23:56:16 +03:00
key <- HashDigest . convert <$> DS.put content
runDB (setContent bookId username key)
return NoContent
2018-08-07 23:25:21 +03:00
postBookMetaHandler :: AuthResult SafeUser -> PostBook -> AppM JsonBook
postBookMetaHandler auth PostBook{..} = flip requireLoggedIn auth $ \SafeUser{username} -> do
2018-08-08 21:58:36 +03:00
mIdentifier <- runDB $ insertBook InsertBook{owner=username,..}
2018-08-07 23:25:21 +03:00
maybe (throwM err403{errBody="Could not insert book"}) (\identifier -> pure JsonBook{..}) mIdentifier
putBookMetaHandler :: AuthResult SafeUser -> BookID -> JsonBook -> AppM JsonBook
2018-08-15 22:10:15 +03:00
putBookMetaHandler auth bookId JsonBook{..}
2018-08-08 23:56:16 +03:00
| bookId == identifier = requireBookOwner auth bookId $ \SafeUser{username=owner} ->
2018-08-15 22:10:15 +03:00
maybe (throwM err403) (return . view (super @JsonBook)) =<< runDB (updateBook UpdateBook{..})
2018-08-08 22:21:15 +03:00
| otherwise = throwM err403
2018-08-05 23:42:37 +03:00
listBooksHandler :: AuthResult SafeUser -> AppM [JsonBook]
listBooksHandler = requireLoggedIn $ \user -> do
runDB (usersBooks (view (field @"username") user) >>= mapM augment)
where
2018-08-08 21:58:36 +03:00
augment Book{identifier=bookId,contentType,title,description} = do
2018-08-14 22:19:55 +03:00
channels <- fmap (view (field @"channel")) <$> booksChannels bookId
tags <- fmap (view (field @"tag")) <$> booksTags bookId
2018-08-07 23:25:21 +03:00
pure JsonBook{identifier=bookId,..}