Compare commits

..

No commits in common. "26af45713c5112fab46b31555cf635f0707ffb4b" and "7928aa1cb62ca568ceae885731224fa90614c314" have entirely different histories.

11 changed files with 168 additions and 251 deletions

View File

@ -1,16 +0,0 @@
language: nix
os:
- linux
before_script:
- mkdir -m 0755 -p /nix/var/nix/{profiles,gcroots}/per-user/$USER
- mkdir -p ~/.config/nixpkgs
script:
- nix build -f release.nix backend
cache:
directories:
- nix

View File

@ -68,7 +68,6 @@ executable backend
, servant , servant
, servant-auth , servant-auth
, servant-auth-server , servant-auth-server
, servant-auth-docs
, servant-docs , servant-docs
, servant-lucid , servant-lucid
, servant-multipart , servant-multipart
@ -125,7 +124,6 @@ test-suite spec
, servant-auth , servant-auth
, servant-auth-server , servant-auth-server
, servant-docs , servant-docs
, servant-auth-docs
, servant-lucid , servant-lucid
, servant-multipart , servant-multipart
, servant-server , servant-server

View File

@ -25,14 +25,16 @@ import qualified API.Catalogue as Catalogue
data Index = Index data Index = Index
type API = Users.API type API = Get '[HTML] (AppView Index)
:<|> Users.API
:<|> "api" :> "current" :> Channels.API :<|> "api" :> "current" :> Channels.API
:<|> "api" :> "current" :> Books.API :<|> "api" :> "current" :> Books.API
:<|> "api" :> "1" :> Catalogue.VersionedAPI 1 :<|> "api" :> "1" :> Catalogue.VersionedAPI 1
:<|> "api" :> "current" :> Catalogue.VersionedAPI 1 :<|> "api" :> "current" :> Catalogue.VersionedAPI 1
handler :: ServerT API AppM handler :: ServerT API AppM
handler = Users.handler handler = indexHandler
:<|> Users.handler
:<|> Channels.handler :<|> Channels.handler
:<|> Books.handler :<|> Books.handler
:<|> Catalogue.handler :<|> Catalogue.handler

View File

@ -1,58 +1,53 @@
{-# LANGUAGE DataKinds #-} {-# Language DuplicateRecordFields #-}
{-# LANGUAGE DeriveGeneric #-} {-# Language DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# Language TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-} {-# Language TypeOperators #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# Language NoImplicitPrelude #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# Language MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-} {-# Language OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# Language TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-} {-# Language QuasiQuotes #-}
{-# LANGUAGE QuasiQuotes #-} {-# Language RecordWildCards #-}
{-# LANGUAGE RecordWildCards #-} {-# Language DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-} {-# Language FlexibleInstances #-}
{-# LANGUAGE TypeApplications #-} {-# Language TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# Language DataKinds #-}
{-# LANGUAGE TypeOperators #-} {-# Language NamedFieldPuns #-}
module API.Books where module API.Books where
import ClassyPrelude import ClassyPrelude
import Control.Lens import Control.Lens
import Control.Monad.Catch (MonadThrow, throwM) import Control.Monad.Catch (throwM, MonadThrow)
import Control.Monad.Trans.Maybe
import Crypto.Hash (digestFromByteString)
import Data.Aeson import Data.Aeson
import Data.ByteArray (convert)
import Data.Generics.Product import Data.Generics.Product
import Database import Database
import Database.Book import Database.Book
import Database.Channel import Database.Channel
import Database.Tag import Database.Tag
import qualified Datastore as DS import Servant hiding (contentType)
import Servant hiding (contentType) import Servant.Auth as SA
import Servant.Auth as SA
import qualified Servant.Docs as Docs
import Server.Auth import Server.Auth
import Types import Types
data JsonBook = JsonBook { identifier :: BookID import Control.Monad.Trans.Maybe
import qualified Datastore as DS
import Data.ByteArray (convert)
import Crypto.Hash (digestFromByteString)
data JsonBook = JsonBook { identifier :: BookID
, contentType :: Text , contentType :: Text
, title :: Text , title :: Text
, description :: Maybe Text , description :: Maybe Text
, channels :: [Text] , channels :: [Text]
, tags :: [Text] } , tags :: [Text] }
deriving (Generic, Show, Eq) deriving (Generic, Show, Eq)
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") [] [])]
data PostBook = PostBook { contentType :: Text data PostBook = PostBook { contentType :: Text
, title :: Text , title :: Text
, description :: Maybe Text , description :: Maybe Text
, channels :: [Text] , channels :: [Text]
, tags :: [Text] } , tags :: [Text] }
deriving (Generic, Show, Eq) deriving (Generic, Show, Eq)
@ -63,21 +58,13 @@ instance FromJSON PostBook
type API = Auth '[SA.BasicAuth, SA.JWT] SafeUser :> BaseAPI type API = Auth '[SA.BasicAuth, SA.JWT] SafeUser :> BaseAPI
instance Docs.ToCapture (Capture "book_id" BookID) where
toCapture _ = Docs.DocCapture "book_id" "The book id"
type BaseAPI = "books" :> Get '[JSON] [JsonBook] type BaseAPI = "books" :> Get '[JSON] [JsonBook]
:<|> "books" :> ReqBody '[JSON] PostBook :> Post '[JSON] JsonBook :<|> "books" :> ReqBody '[JSON] PostBook :> Post '[JSON] JsonBook
:<|> "books" :> Capture "book_id" BookID :> "meta" :> ReqBody '[JSON] JsonBook :> Put '[JSON] JsonBook :<|> "books" :> Capture "book_id" BookID :> "meta" :> ReqBody '[JSON] JsonBook :> Put '[JSON] JsonBook
:<|> "books" :> Capture "book_id" BookID :> ReqBody '[OctetStream] FileContent :> Put '[JSON] NoContent :<|> "books" :> Capture "book_id" BookID :> ReqBody '[OctetStream] ByteString :> Put '[JSON] NoContent
:<|> GetBook :<|> GetBook
newtype FileContent = FileContent { getFileContent :: ByteString } deriving (MimeUnrender OctetStream, MimeRender OctetStream ) type GetBook = "books" :> Capture "book_id" BookID :> Get '[OctetStream] ByteString
instance Docs.ToSample FileContent where
toSamples _ = [("File contents", FileContent "bytes here and there")]
type GetBook = "books" :> Capture "book_id" BookID :> Get '[OctetStream] FileContent
handler :: ServerT API AppM handler :: ServerT API AppM
handler user = listBooksHandler user handler user = listBooksHandler user
@ -86,12 +73,12 @@ handler user = listBooksHandler user
:<|> putBookContentHandler user :<|> putBookContentHandler user
:<|> getBookContentHandler user :<|> getBookContentHandler user
getBookContentHandler :: AuthResult SafeUser -> BookID -> AppM FileContent getBookContentHandler :: AuthResult SafeUser -> BookID -> AppM ByteString
getBookContentHandler auth bookId = requireBookOwner auth bookId $ \SafeUser{username} -> do getBookContentHandler auth bookId = requireBookOwner auth bookId $ \SafeUser{username} -> do
content <- runMaybeT $ do content <- runMaybeT $ do
Book{contentHash=mHash} <- MaybeT $ runDB (getBook bookId username) Book{contentHash=mHash} <- MaybeT $ runDB (getBook bookId username)
contentHash <- MaybeT $ return (mHash >>= digestFromByteString . unHex) contentHash <- MaybeT $ return (mHash >>= digestFromByteString . unHex)
FileContent <$> MaybeT (DS.get contentHash) MaybeT $ DS.get contentHash
maybe (throwM err404) return content maybe (throwM err404) return content
requireBookOwner :: AuthResult SafeUser -> BookID -> (SafeUser -> AppM a) -> AppM a requireBookOwner :: AuthResult SafeUser -> BookID -> (SafeUser -> AppM a) -> AppM a
@ -100,9 +87,8 @@ requireBookOwner auth bookId f = flip requireLoggedIn auth $ \u@SafeUser{usernam
unless exists $ throwM err404 unless exists $ throwM err404
runDB (isBookOwner bookId username) >>= \o -> if o then f u else throwM err403 runDB (isBookOwner bookId username) >>= \o -> if o then f u else throwM err403
putBookContentHandler :: AuthResult SafeUser -> BookID -> FileContent -> AppM NoContent putBookContentHandler :: AuthResult SafeUser -> BookID -> ByteString -> AppM NoContent
putBookContentHandler auth bookId fc = requireBookOwner auth bookId $ \SafeUser{username} -> do putBookContentHandler auth bookId content = requireBookOwner auth bookId $ \SafeUser{username} -> do
let content = getFileContent fc
key <- HashDigest . convert <$> DS.put content key <- HashDigest . convert <$> DS.put content
runDB (setContent bookId username key) runDB (setContent bookId username key)
return NoContent return NoContent

View File

@ -24,10 +24,8 @@ import qualified Database.Channel as Channel
import GHC.TypeLits import GHC.TypeLits
import Servant hiding (contentType) import Servant hiding (contentType)
import Servant.Auth as SA import Servant.Auth as SA
import qualified Servant.Docs as Docs
import Servant.XML import Servant.XML
import Server.Auth import Server.Auth
import System.IO.Unsafe (unsafePerformIO)
import Types import Types
-- This is my first try on going to versioned apis, things might change -- This is my first try on going to versioned apis, things might change
@ -42,7 +40,7 @@ newtype Rel = Rel { unRel :: Text } deriving (IsString, Show)
data Pagination = Pagination { previous :: Maybe Rel data Pagination = Pagination { previous :: Maybe Rel
, next :: Maybe Rel } , next :: Maybe Rel }
deriving (Show, Generic) deriving (Show)
newtype SubSection = SubSection Rel deriving (Show) newtype SubSection = SubSection Rel deriving (Show)
newtype Acquisition = Acquisition Rel deriving (Show) newtype Acquisition = Acquisition Rel deriving (Show)
@ -66,20 +64,6 @@ deriving instance Show (Entry 1)
deriving instance Generic (Catalog 1) deriving instance Generic (Catalog 1)
deriving instance Generic (Entry 1) deriving instance Generic (Entry 1)
instance Docs.ToSample (Entry 1) where
toSamples _ = [("Entry", EntryV1 "title" "identifier" docsTime "content" (Left (SubSection (Rel "sub"))))]
instance Docs.ToSample UTCTime where
toSamples _ = [("time", docsTime)]
instance Docs.ToSample Rel where
toSamples _ = [("Relative link", Rel "next")]
instance Docs.ToSample Pagination
instance Docs.ToSample (Catalog 1) -- where
-- toSamples _ = [("catalog", CatalogV1 docsTime (Rel "prev") (Rel "next") (Pagination (Just "previous") (Just "next")) [])]
docsTime :: UTCTime
docsTime = unsafePerformIO getCurrentTime
instance ToNode SubSection where instance ToNode SubSection where
toNode (SubSection rel) = [xml|<link type="application/atom+xml;profile=opds-catalog;kind=acquisition" rel="subsection" href="#{unRel rel}">|] toNode (SubSection rel) = [xml|<link type="application/atom+xml;profile=opds-catalog;kind=acquisition" rel="subsection" href="#{unRel rel}">|]

View File

@ -1,47 +1,41 @@
{-# LANGUAGE DataKinds #-} {-# Language DataKinds #-}
{-# LANGUAGE DeriveGeneric #-} {-# Language TypeFamilies #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# Language TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-} {-# Language NoImplicitPrelude #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# Language MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-} {-# Language OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# Language TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-} {-# Language QuasiQuotes #-}
{-# LANGUAGE QuasiQuotes #-} {-# Language RecordWildCards #-}
{-# LANGUAGE RecordWildCards #-} {-# Language DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-} {-# Language FlexibleInstances #-}
{-# LANGUAGE TypeApplications #-} {-# Language TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# Language DataKinds #-}
{-# LANGUAGE TypeOperators #-} {-# Language DuplicateRecordFields #-}
{-# Language NamedFieldPuns #-}
module API.Channels (API, handler, JsonChannel(..)) where module API.Channels (API, handler, JsonChannel(..)) where
import ClassyPrelude import ClassyPrelude
import Control.Lens import Control.Lens
import Control.Monad.Catch (MonadThrow, throwM) import Control.Monad.Catch (throwM, MonadThrow)
import Control.Monad.Logger import Control.Monad.Logger
import Data.Aeson import Data.Aeson
import Data.Generics.Product import Data.Generics.Product
import Database import Database
import Database.Channel import Database.Channel
import Servant import Servant
import Servant.Auth as SA import Servant.Auth as SA
import qualified Servant.Docs as Docs import Server.Auth
import Server.Auth import Types
import Types
data JsonChannel = JsonChannel { channel :: Text data JsonChannel = JsonChannel { channel :: Text
, visibility :: Visibility } , visibility :: Visibility }
deriving (Show, Generic) deriving (Show, Generic)
data UpdateChannel = UpdateChannel { identifier :: ChannelID data UpdateChannel = UpdateChannel { identifier :: ChannelID
, channel :: Text , channel :: Text
, visibility :: Visibility } , visibility :: Visibility }
deriving (Show, Generic) deriving (Show, Generic)
instance Docs.ToSample JsonChannel where
toSamples _ = [("Channel", JsonChannel "channel" Private)]
instance Docs.ToSample UpdateChannel where
toSamples _ = [("Channel", UpdateChannel 13 "channel" Private)]
instance ToJSON JsonChannel instance ToJSON JsonChannel
instance FromJSON JsonChannel instance FromJSON JsonChannel
instance ToJSON UpdateChannel instance ToJSON UpdateChannel
@ -49,9 +43,6 @@ instance FromJSON UpdateChannel
type API = Auth '[SA.BasicAuth, SA.Cookie, SA.JWT] SafeUser :> BaseAPI type API = Auth '[SA.BasicAuth, SA.Cookie, SA.JWT] SafeUser :> BaseAPI
instance Docs.ToCapture (Capture "channel_id" ChannelID) where
toCapture _ = Docs.DocCapture "channel_id" "The channel id"
type BaseAPI = "channels" :> ReqBody '[JSON] JsonChannel :> Post '[JSON] UpdateChannel type BaseAPI = "channels" :> ReqBody '[JSON] JsonChannel :> Post '[JSON] UpdateChannel
:<|> "channels" :> Capture "channel_id" ChannelID :> ReqBody '[JSON] UpdateChannel :> Put '[JSON] UpdateChannel :<|> "channels" :> Capture "channel_id" ChannelID :> ReqBody '[JSON] UpdateChannel :> Put '[JSON] UpdateChannel
:<|> "channels" :> Get '[JSON] [JsonChannel] :<|> "channels" :> Get '[JSON] [JsonChannel]

View File

@ -7,19 +7,18 @@
{-# Language TypeApplications #-} {-# Language TypeApplications #-}
module API.Users where module API.Users where
import ClassyPrelude import ClassyPrelude
import Control.Monad.Catch (throwM, MonadThrow) import Control.Monad.Catch (throwM, MonadThrow)
import Data.Aeson import Data.Aeson
import Database (runDB) import Database (runDB)
import Database.Schema import Database.Schema
import Database.User import Database.User
import Servant import Servant
import Servant.Auth as SA import Servant.Auth as SA
import Servant.Auth.Server as SAS import Servant.Auth.Server as SAS
import qualified Servant.Docs as Docs import Server.Auth
import Server.Auth import Types
import Types import Web.FormUrlEncoded
import Web.FormUrlEncoded
data RegisterForm = RegisterForm { username :: Username data RegisterForm = RegisterForm { username :: Username
@ -28,17 +27,12 @@ data RegisterForm = RegisterForm { username :: Username
, passwordAgain :: PlainPassword } , passwordAgain :: PlainPassword }
deriving (Generic, Show) deriving (Generic, Show)
instance Docs.ToSample RegisterForm
data LoginStatus = LoginStatus ( Maybe SafeUser ) deriving Generic data LoginStatus = LoginStatus ( Maybe SafeUser ) deriving Generic
data RegisterStatus = RegisterStatus deriving Generic data RegisterStatus = RegisterStatus deriving Generic
instance Docs.ToSample RegisterStatus
instance ToJSON LoginStatus instance ToJSON LoginStatus
instance FromJSON LoginStatus instance FromJSON LoginStatus
instance Docs.ToSample LoginStatus
instance FromJSON RegisterForm instance FromJSON RegisterForm
instance ToJSON RegisterForm instance ToJSON RegisterForm

View File

@ -1,89 +1,80 @@
{-# LANGUAGE DeriveGeneric #-} {-# Language NoImplicitPrelude #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# Language DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# Language OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# Language DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-} {-# Language GeneralizedNewtypeDeriving #-}
module Database.Schema where module Database.Schema where
import ClassyPrelude import ClassyPrelude
import Data.Aeson import Database.Selda.Generic
import Database.Selda import Database.Selda
import Database.Selda.Backend import Database.Selda.Backend
import Database.Selda.Generic
import qualified Servant.Docs as Docs import Data.Aeson
import Web.HttpApiData import Web.HttpApiData
-- | User type -- | User type
newtype PlainPassword = PlainPassword Text deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Eq, Generic) newtype PlainPassword = PlainPassword Text deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Eq, Generic)
newtype HashedPassword = HashedPassword {unHashed :: ByteString} newtype HashedPassword = HashedPassword {unHashed :: ByteString}
data NoPassword = NoPassword data NoPassword = NoPassword
newtype Email = Email { unEmail :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Generic, Eq, IsString) newtype Email = Email { unEmail :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Generic, Eq)
newtype Username = Username { unUsername :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Eq, Generic, IsString) newtype Username = Username { unUsername :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Eq, Generic)
instance Docs.ToSample Username where
toSamples _ = [("Username", Username "user123")]
instance Docs.ToSample Email where
toSamples _ = [("Email", Email "first.last@example.com")]
instance Docs.ToSample PlainPassword where
toSamples _ = [("Password", PlainPassword "password123")]
instance SqlType HashedPassword where instance SqlType HashedPassword where
mkLit = LCustom . LBlob . unHashed mkLit = LCustom . LBlob . unHashed
fromSql (SqlBlob x) = HashedPassword x fromSql (SqlBlob x) = HashedPassword x
fromSql _ = error "fromSql: Bad hash" fromSql _ = error "fromSql: Bad hash"
defaultValue = mkLit (HashedPassword "") -- Makes no sense defaultValue = mkLit (HashedPassword "") -- Makes no sense
instance SqlType Email where instance SqlType Email where
mkLit = LCustom . LText . unEmail mkLit = LCustom . LText . unEmail
fromSql (SqlString x) = Email x fromSql (SqlString x) = Email x
fromSql _ = error "fromSql: Bad email" fromSql _ = error "fromSql: Bad email"
defaultValue = mkLit (Email "") defaultValue = mkLit (Email "")
instance SqlType Username where instance SqlType Username where
mkLit = LCustom . LText . unUsername mkLit = LCustom . LText . unUsername
fromSql (SqlString x) = Username x fromSql (SqlString x) = Username x
fromSql _ = error "fromSql: Bad username" fromSql _ = error "fromSql: Bad username"
defaultValue = mkLit (Username "") defaultValue = mkLit (Username "")
newtype UserID = UserID {unUserID :: Int} deriving (Show) newtype UserID = UserID {unUserID :: Int} deriving (Show)
newtype BookID = BookID {unBookID :: Int} deriving (Show, ToJSON, FromJSON, FromHttpApiData, Eq, Ord, ToHttpApiData, Generic, Num) newtype BookID = BookID {unBookID :: Int} deriving (Show, ToJSON, FromJSON, FromHttpApiData, Eq, Ord, ToHttpApiData, Generic)
newtype ChannelID = ChannelID {unChannelID :: Int} deriving (Show, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON, Eq, Generic, Num) newtype ChannelID = ChannelID {unChannelID :: Int} deriving (Show, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON, Eq, Generic)
newtype TagID = TagID {unTagID :: Int} deriving (Show) newtype TagID = TagID {unTagID :: Int} deriving (Show)
instance SqlType UserID where instance SqlType UserID where
mkLit = LCustom . LInt . unUserID mkLit = LCustom . LInt . unUserID
fromSql (SqlInt x) = UserID x fromSql (SqlInt x) = UserID x
fromSql _ = error "fromSql: Bad userid" fromSql _ = error "fromSql: Bad userid"
sqlType _ = TRowID sqlType _ = TRowID
defaultValue = mkLit (UserID (-1)) defaultValue = mkLit (UserID (-1))
instance SqlType BookID where instance SqlType BookID where
mkLit = LCustom . LInt . unBookID mkLit = LCustom . LInt . unBookID
fromSql (SqlInt x) = BookID x fromSql (SqlInt x) = BookID x
fromSql _ = error "fromSql: Bad bookid" fromSql _ = error "fromSql: Bad bookid"
defaultValue = mkLit (BookID (-1)) defaultValue = mkLit (BookID (-1))
instance SqlType ChannelID where instance SqlType ChannelID where
mkLit = LCustom . LInt . unChannelID mkLit = LCustom . LInt . unChannelID
fromSql (SqlInt x) = ChannelID x fromSql (SqlInt x) = ChannelID x
fromSql _ = error "fromSql: Bad channelid" fromSql _ = error "fromSql: Bad channelid"
defaultValue = mkLit (ChannelID (-1)) defaultValue = mkLit (ChannelID (-1))
instance SqlType TagID where instance SqlType TagID where
mkLit = LCustom . LInt . unTagID mkLit = LCustom . LInt . unTagID
fromSql (SqlInt x) = TagID x fromSql (SqlInt x) = TagID x
fromSql _ = error "fromSql: Bad tagid" fromSql _ = error "fromSql: Bad tagid"
defaultValue = mkLit (TagID (-1)) defaultValue = mkLit (TagID (-1))
data User pass = User { identifier :: UserID data User pass = User { identifier :: UserID
, email :: Email , email :: Email
, username :: Username , username :: Username
, role :: Role , role :: Role
, password :: pass } , password :: pass }
deriving (Show, Generic) deriving (Show, Generic)
data Role = UserRole | AdminRole deriving (Show, Read, Enum, Bounded, Typeable, Generic, Eq) data Role = UserRole | AdminRole deriving (Show, Read, Enum, Bounded, Typeable, Generic, Eq)
@ -107,18 +98,18 @@ users = genTable "users" [ (email :: User HashedPassword -> Email) :- uniqueGen
-- | Book type -- | Book type
newtype HashDigest = HashDigest { unHex :: ByteString } deriving Show newtype HashDigest = HashDigest { unHex :: ByteString } deriving Show
-- XXX: Add an identifier for the book -- XXX: Add an identifier for the book
data Book = Book { identifier :: BookID data Book = Book { identifier :: BookID
, contentHash :: Maybe HashDigest , contentHash :: Maybe HashDigest
, contentType :: Text , contentType :: Text
, title :: Text , title :: Text
, description :: Maybe Text , description :: Maybe Text
, owner :: UserID } , owner :: UserID }
deriving (Show, Generic) deriving (Show, Generic)
instance SqlType HashDigest where instance SqlType HashDigest where
mkLit = LCustom . LBlob . unHex mkLit = LCustom . LBlob . unHex
fromSql (SqlBlob x) = HashDigest x fromSql (SqlBlob x) = HashDigest x
fromSql _ = error "fromSql: Not a valid hash digest" fromSql _ = error "fromSql: Not a valid hash digest"
defaultValue = mkLit (HashDigest "") -- Doesn't really make sense defaultValue = mkLit (HashDigest "") -- Doesn't really make sense
books :: GenTable Book books :: GenTable Book
@ -129,8 +120,8 @@ books = genTable "books" [ (identifier :: Book -> BookID) :- autoPrimaryGen
-- | Categorizing books -- | Categorizing books
data Tag = Tag { identifier :: TagID data Tag = Tag { identifier :: TagID
, tag :: Text , tag :: Text
, owner :: UserID } , owner :: UserID }
deriving (Show, Generic) deriving (Show, Generic)
data Visibility = Public | Private | Followers data Visibility = Public | Private | Followers
@ -146,8 +137,8 @@ instance SqlType Visibility where
defaultValue = mkLit Private defaultValue = mkLit Private
data Channel = Channel { identifier :: ChannelID data Channel = Channel { identifier :: ChannelID
, channel :: Text , channel :: Text
, owner :: UserID , owner :: UserID
, visibility :: Visibility } , visibility :: Visibility }
deriving (Show, Generic) deriving (Show, Generic)
@ -163,12 +154,12 @@ channels = genTable "channels" [ (identifier :: Channel -> ChannelID) :- autoPri
where where
i :*: _ = selectors (gen users) i :*: _ = selectors (gen users)
data BookTag = BookTag { tag :: TagID data BookTag = BookTag { tag :: TagID
, book :: BookID } , book :: BookID }
deriving (Show, Generic) deriving (Show, Generic)
data BookChannel = BookChannel { channel :: ChannelID data BookChannel = BookChannel { channel :: ChannelID
, book :: BookID } , book :: BookID }
deriving (Show, Generic) deriving (Show, Generic)
bookTags :: GenTable BookTag bookTags :: GenTable BookTag

View File

@ -1,45 +1,37 @@
{-# LANGUAGE DataKinds #-} {-# Language DataKinds #-}
{-# LANGUAGE DeriveGeneric #-} {-# Language TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-} {-# Language TypeOperators #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# Language NoImplicitPrelude #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# Language MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# Language OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# Language TemplateHaskell #-}
{-# LANGUAGE RecordWildCards #-} {-# Language QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# Language RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-} {-# Language DeriveGeneric #-}
{-# LANGUAGE TypeApplications #-} {-# Language FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-} {-# Language TypeApplications #-}
{-# LANGUAGE TypeOperators #-} {-# Language ScopedTypeVariables #-}
module Server where module Server where
import qualified API as API import qualified API as API
import ClassyPrelude hiding (Handler) import Server.Auth (authCheck)
import Control.Lens import Servant
import Control.Monad.Except import Types
import Control.Monad.Logger import ClassyPrelude hiding (Handler)
import Data.Generics.Product import Control.Monad.Logger
import Servant import Control.Monad.Except
import Servant.Auth.Docs () import Servant.Auth.Server as SAS
import Servant.Auth.Server as SAS import Control.Lens
import qualified Servant.Docs as Docs import Data.Generics.Product
import Servant.HTML.Lucid (HTML) import Server.Auth (SafeUser)
import Server.Auth (SafeUser)
import Server.Auth (authCheck)
import Types
type API = API.API type API = API.API :<|> "static" :> Raw
:<|> "help" :> Get '[PlainText, HTML] String
:<|> "static" :> Raw
type Ctx = '[BasicAuthData -> IO (AuthResult SafeUser), CookieSettings, JWTSettings] type Ctx = '[BasicAuthData -> IO (AuthResult SafeUser), CookieSettings, JWTSettings]
server :: App -> Application server :: App -> Application
server app = serveWithContext api cfg (hoistServerWithContext (Proxy @ API.API) (Proxy @ Ctx) server' API.handler :<|> serveDocs :<|> serveDirectoryFileServer "static") server app = serveWithContext api cfg (hoistServerWithContext (Proxy @ API.API) (Proxy @ Ctx) server' API.handler :<|> serveDirectoryFileServer "static")
where where
apiDocs :: Docs.API
apiDocs = Docs.docs (Proxy @API.API)
serveDocs = pure $ Docs.markdown apiDocs
myKey = view (field @"jwk") app myKey = view (field @"jwk") app
jwtCfg = defaultJWTSettings myKey jwtCfg = defaultJWTSettings myKey
authCfg = authCheck app authCfg = authCheck app

View File

@ -1,11 +1,11 @@
{-# LANGUAGE DataKinds #-} {-# Language DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# Language TypeFamilies #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# Language OverloadedStrings #-}
{-# LANGUAGE OverloadedStrings #-} {-# Language NoImplicitPrelude #-}
{-# LANGUAGE TemplateHaskell #-} {-# Language TypeOperators #-}
{-# LANGUAGE TypeApplications #-} {-# Language DuplicateRecordFields #-}
{-# LANGUAGE TypeFamilies #-} {-# Language TypeApplications #-}
{-# LANGUAGE TypeOperators #-} {-# Language TemplateHaskell #-}
module Server.Auth module Server.Auth
( SafeUser(..) ( SafeUser(..)
, authCheck , authCheck
@ -13,33 +13,29 @@ module Server.Auth
, requireLoggedIn) , requireLoggedIn)
where where
import ClassyPrelude import ClassyPrelude
import Control.Lens (view) import Control.Lens (view)
import Control.Monad.Catch (MonadThrow, throwM) import Control.Monad.Logger
import Control.Monad.Logger import Control.Monad.Catch (throwM, MonadThrow)
import Data.Aeson import Data.Aeson
import Data.Generics.Product import Data.Generics.Product
import Database import Database
import Database.Schema import Database.Schema
import Database.User import Database.User
import Servant (err401) import Servant (err401)
import Servant.Auth.Server as SAS import Servant.Auth.Server as SAS
import qualified Servant.Docs as Docs import Types
import Types
-- generic-lens can convert similar types to this -- generic-lens can convert similar types to this
-- I'm trying out servant-auth-server which uses a jwt style login. IIRC anyone -- I'm trying out servant-auth-server which uses a jwt style login. IIRC anyone
-- can open the jwt token and view what's inside, you just can't modify it. -- can open the jwt token and view what's inside, you just can't modify it.
-- --
-- Is it a problem that a human readable username and email are visible? -- Is it a problem that a human readable username and email are visible?
data SafeUser = SafeUser { email :: Email data SafeUser = SafeUser { email :: Email
, username :: Username , username :: Username
, role :: Role } , role :: Role }
deriving (Show, Generic) deriving (Show, Generic)
instance Docs.ToSample SafeUser where
toSamples _ = [("User", SafeUser "user@example.com" "user" UserRole )]
instance ToJSON SafeUser where instance ToJSON SafeUser where
instance FromJSON SafeUser where instance FromJSON SafeUser where
instance ToJWT SafeUser where instance ToJWT SafeUser where

View File

@ -1 +0,0 @@
create unique index tag_owner on tags (tag, owner);