Compare commits
	
		
			13 Commits
		
	
	
		
			v0.1.0.0
			...
			e459a318bd
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e459a318bd | |||
| 92e34fdfcc | |||
| 26af45713c | |||
| 5727ea5574 | |||
| 7928aa1cb6 | |||
| fb29a6e694 | |||
| 5961a99d77 | |||
| 6cabe97b30 | |||
| 8733c4d1d1 | |||
| eb770b91af | |||
| f5f6c9ced9 | |||
| fdbd24a4bf | |||
| 6865af361d | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,6 @@ | ||||
| dist/ | ||||
| config/config.dhall | ||||
| /ctags | ||||
| /TAGS | ||||
| /result* | ||||
| /backend/config | ||||
|   | ||||
							
								
								
									
										11
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| 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 ./release.nix -A backend --option trusted-public-keys "masser-ebook-manager.cachix.org-1:mtFSkQ2MO5MvjUpulZoFKjKUIa8g8CTcdPVuJaPKS1w= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" --option binary-caches "https://cache.nixos.org https://masser-ebook-manager.cachix.org" | ||||
							
								
								
									
										5
									
								
								backend/ChangeLog.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								backend/ChangeLog.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| # Revision history for backend | ||||
|  | ||||
| ## 0.1.0.0 -- YYYY-mm-dd | ||||
|  | ||||
| * First version. Released on an unsuspecting world. | ||||
							
								
								
									
										30
									
								
								backend/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								backend/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| Copyright (c) 2018, Mats Rauhala | ||||
|  | ||||
| All rights reserved. | ||||
|  | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
|  | ||||
|     * Redistributions of source code must retain the above copyright | ||||
|       notice, this list of conditions and the following disclaimer. | ||||
|  | ||||
|     * Redistributions in binary form must reproduce the above | ||||
|       copyright notice, this list of conditions and the following | ||||
|       disclaimer in the documentation and/or other materials provided | ||||
|       with the distribution. | ||||
|  | ||||
|     * Neither the name of Mats Rauhala nor the names of other | ||||
|       contributors may be used to endorse or promote products derived | ||||
|       from this software without specific prior written permission. | ||||
|  | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										2
									
								
								backend/Setup.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								backend/Setup.hs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| import Distribution.Simple | ||||
| main = defaultMain | ||||
| @@ -1,4 +1,7 @@ | ||||
| name:                ebook-manager | ||||
| -- Initial backend.cabal generated by cabal init.  For further | ||||
| -- documentation, see http://haskell.org/cabal/users-guide/ | ||||
| 
 | ||||
| name:                backend | ||||
| version:             0.1.0.0 | ||||
| -- synopsis: | ||||
| -- description: | ||||
| @@ -12,7 +15,7 @@ build-type:          Simple | ||||
| extra-source-files:  ChangeLog.md | ||||
| cabal-version:       >=1.10 | ||||
| 
 | ||||
| executable ebook-manager | ||||
| executable backend | ||||
|   main-is:             Main.hs | ||||
|   other-modules:       Devel.Main | ||||
|                      , API | ||||
| @@ -20,8 +23,6 @@ executable ebook-manager | ||||
|                      , API.Catalogue | ||||
|                      , API.Channels | ||||
|                      , API.Users | ||||
|                      , Configuration | ||||
|                      , Data.Versioned | ||||
|                      , Database | ||||
|                      , Database.Book | ||||
|                      , Database.Channel | ||||
| @@ -35,7 +36,66 @@ executable ebook-manager | ||||
|                      , Types | ||||
|                      , View | ||||
|   -- other-extensions: | ||||
|   build-depends:       base >=4.10 && <4.11 | ||||
|   build-depends:       base >=4.10 | ||||
|                      , exceptions | ||||
|                      , monad-control | ||||
|                      , common | ||||
|                      , aeson | ||||
|                      , asn1-data | ||||
|                      , asn1-types | ||||
|                      , bytestring | ||||
|                      , classy-prelude | ||||
|                      , cryptonite | ||||
|                      , dhall | ||||
|                      , directory | ||||
|                      , foreign-store | ||||
|                      , generic-lens | ||||
|                      , http-api-data | ||||
|                      , http-media | ||||
|                      , jose | ||||
|                      , lens | ||||
|                      , lucid | ||||
|                      , memory | ||||
|                      , monad-logger | ||||
|                      , mtl | ||||
|                      , pandoc | ||||
|                      , pandoc-types | ||||
|                      , pem | ||||
|                      , process | ||||
|                      , resource-pool | ||||
|                      , selda | ||||
|                      , selda-postgresql | ||||
|                      , servant | ||||
|                      , servant-auth | ||||
|                      , servant-auth-server | ||||
|                      , servant-auth-docs | ||||
|                      , servant-docs | ||||
|                      , servant-lucid | ||||
|                      , servant-multipart | ||||
|                      , servant-server | ||||
|                      , text | ||||
|                      , transformers | ||||
|                      , wai | ||||
|                      , warp | ||||
|                      , x509 | ||||
|                      , x509-store | ||||
|                      , xml-conduit | ||||
|                      , xml-hamlet | ||||
|   hs-source-dirs:      src | ||||
|   default-language:    Haskell2010 | ||||
|   default-extensions:  DeriveGeneric | ||||
|                      , NoImplicitPrelude | ||||
|                      , OverloadedStrings | ||||
|                      , RecordWildCards | ||||
| 
 | ||||
| test-suite spec | ||||
|   type:                exitcode-stdio-1.0 | ||||
|   main-is:             Spec.hs | ||||
|   hs-source-dirs:      src | ||||
|   build-depends:       base >=4.10 | ||||
|                      , exceptions | ||||
|                      , monad-control | ||||
|                      , common | ||||
|                      , aeson | ||||
|                      , asn1-data | ||||
|                      , asn1-types | ||||
| @@ -65,6 +125,7 @@ executable ebook-manager | ||||
|                      , servant-auth | ||||
|                      , servant-auth-server | ||||
|                      , servant-docs | ||||
|                      , servant-auth-docs | ||||
|                      , servant-lucid | ||||
|                      , servant-multipart | ||||
|                      , servant-server | ||||
| @@ -76,9 +137,12 @@ executable ebook-manager | ||||
|                      , x509-store | ||||
|                      , xml-conduit | ||||
|                      , xml-hamlet | ||||
|   hs-source-dirs:      src | ||||
|                      , validity | ||||
|                      , genvalidity-hspec | ||||
|                      , genvalidity-property | ||||
|                      , genvalidity-text | ||||
|                      , hspec | ||||
|   default-extensions:  DeriveGeneric | ||||
|                      , NoImplicitPrelude | ||||
|                      , OverloadedStrings | ||||
|                      , RecordWildCards | ||||
|   default-language:    Haskell2010 | ||||
|                      , RecordWildCards | ||||
| @@ -25,16 +25,14 @@ import qualified API.Catalogue as Catalogue | ||||
| 
 | ||||
| data Index = Index | ||||
| 
 | ||||
| type API = Get '[HTML] (AppView Index) | ||||
|       :<|> Users.API | ||||
| type API = Users.API | ||||
|       :<|> "api" :> "current" :> Channels.API | ||||
|       :<|> "api" :> "current" :> Books.API | ||||
|       :<|> "api" :> "1" :> Catalogue.VersionedAPI 1 | ||||
|       :<|> "api" :> "current" :> Catalogue.VersionedAPI 1 | ||||
| 
 | ||||
| handler :: ServerT API AppM | ||||
| handler = indexHandler | ||||
|     :<|> Users.handler | ||||
| handler = Users.handler | ||||
|     :<|> Channels.handler | ||||
|     :<|> Books.handler | ||||
|     :<|> Catalogue.handler | ||||
| @@ -1,53 +1,59 @@ | ||||
| {-# Language DuplicateRecordFields #-} | ||||
| {-# Language DataKinds #-} | ||||
| {-# Language TypeFamilies #-} | ||||
| {-# Language TypeOperators #-} | ||||
| {-# Language NoImplicitPrelude #-} | ||||
| {-# Language MultiParamTypeClasses #-} | ||||
| {-# Language OverloadedStrings #-} | ||||
| {-# Language TemplateHaskell #-} | ||||
| {-# Language QuasiQuotes #-} | ||||
| {-# Language RecordWildCards #-} | ||||
| {-# Language DeriveGeneric #-} | ||||
| {-# Language FlexibleInstances #-} | ||||
| {-# Language TypeApplications #-} | ||||
| {-# Language DataKinds #-} | ||||
| {-# Language NamedFieldPuns #-} | ||||
| {-# 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              #-} | ||||
| module API.Books where | ||||
| 
 | ||||
| import Servant hiding (contentType) | ||||
| import Types | ||||
| import ClassyPrelude | ||||
| import Server.Auth | ||||
| import Servant.Auth as SA | ||||
| import Data.Aeson | ||||
| import Database.Book | ||||
| import Database.Channel | ||||
| import Database.Tag | ||||
| import Database | ||||
| import Control.Lens | ||||
| import Data.Generics.Product | ||||
| import           ClassyPrelude | ||||
| import           Control.Lens | ||||
| import           Control.Monad.Catch       (MonadThrow, throwM) | ||||
| import           Control.Monad.Trans.Maybe | ||||
| import           Crypto.Hash               (digestFromByteString) | ||||
| import           Data.Aeson | ||||
| import           Data.ByteArray            (convert) | ||||
| import           Data.Generics.Product | ||||
| import           Database | ||||
| import           Database.Book | ||||
| import           Database.Channel | ||||
| import           Database.Tag | ||||
| import qualified Datastore                 as DS | ||||
| import           Servant                   hiding (contentType) | ||||
| import           Servant.Auth              as SA | ||||
| import qualified Servant.Docs              as Docs | ||||
| import           Server.Auth | ||||
| import           Types | ||||
| 
 | ||||
| import Control.Monad.Trans.Maybe | ||||
| 
 | ||||
| import qualified Datastore as DS | ||||
| import Data.ByteArray (convert) | ||||
| import Crypto.Hash (digestFromByteString) | ||||
| 
 | ||||
| data JsonBook = JsonBook { identifier :: BookID | ||||
| data JsonBook = JsonBook { identifier  :: BookID | ||||
|                          , contentType :: Text | ||||
|                          , title :: Text | ||||
|                          , title       :: Text | ||||
|                          , description :: Maybe Text | ||||
|                          , channels :: [Text] | ||||
|                          , tags :: [Text] } | ||||
|               deriving (Generic, Show) | ||||
|                          , channels    :: [Text] | ||||
|                          , tags        :: [Text] } | ||||
|               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 | ||||
|                          , title :: Text | ||||
|                          , title       :: Text | ||||
|                          , description :: Maybe Text | ||||
|                          , channels :: [Text] | ||||
|                          , tags :: [Text] } | ||||
|               deriving (Generic, Show) | ||||
|                          , channels    :: [Text] | ||||
|                          , tags        :: [Text] } | ||||
|               deriving (Generic, Show, Eq) | ||||
| 
 | ||||
| 
 | ||||
| instance ToJSON JsonBook | ||||
| @@ -57,13 +63,21 @@ instance FromJSON PostBook | ||||
| 
 | ||||
| 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] | ||||
|        :<|> "books" :> ReqBody '[JSON] PostBook :> Post '[JSON] JsonBook | ||||
|        :<|> "books" :> Capture "book_id" BookID :> "meta" :> ReqBody '[JSON] JsonBook :> Put '[JSON] JsonBook | ||||
|        :<|> "books" :> Capture "book_id" BookID :> ReqBody '[OctetStream] ByteString :> Put '[JSON] NoContent | ||||
|        :<|> "books" :> Capture "book_id" BookID :> ReqBody '[OctetStream] FileContent :> Put '[JSON] NoContent | ||||
|        :<|> GetBook | ||||
| 
 | ||||
| type GetBook = "books" :> Capture "book_id" BookID :> Get '[OctetStream] ByteString | ||||
| 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 | ||||
| 
 | ||||
| handler :: ServerT API AppM | ||||
| handler user = listBooksHandler user | ||||
| @@ -72,12 +86,12 @@ handler user = listBooksHandler user | ||||
|           :<|> putBookContentHandler user | ||||
|           :<|> getBookContentHandler user | ||||
| 
 | ||||
| getBookContentHandler :: AuthResult SafeUser -> BookID -> AppM ByteString | ||||
| getBookContentHandler :: AuthResult SafeUser -> BookID -> AppM FileContent | ||||
| 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) | ||||
|     MaybeT $ DS.get contentHash | ||||
|     FileContent <$> MaybeT (DS.get contentHash) | ||||
|   maybe (throwM err404) return content | ||||
| 
 | ||||
| requireBookOwner :: AuthResult SafeUser -> BookID -> (SafeUser -> AppM a) -> AppM a | ||||
| @@ -86,8 +100,9 @@ requireBookOwner auth bookId f = flip requireLoggedIn auth $ \u@SafeUser{usernam | ||||
|   unless exists $ throwM err404 | ||||
|   runDB (isBookOwner bookId username) >>= \o -> if o then f u else throwM err403 | ||||
| 
 | ||||
| putBookContentHandler :: AuthResult SafeUser -> BookID -> ByteString -> AppM NoContent | ||||
| putBookContentHandler auth bookId content = requireBookOwner auth bookId $ \SafeUser{username} -> do | ||||
| putBookContentHandler :: AuthResult SafeUser -> BookID -> FileContent -> AppM NoContent | ||||
| putBookContentHandler auth bookId fc = requireBookOwner auth bookId $ \SafeUser{username} -> do | ||||
|   let content = getFileContent fc | ||||
|   key <- HashDigest . convert <$> DS.put content | ||||
|   runDB (setContent bookId username key) | ||||
|   return NoContent | ||||
| @@ -16,17 +16,19 @@ | ||||
| {-# Language ScopedTypeVariables #-} | ||||
| module API.Catalogue (VersionedAPI, handler) where | ||||
| 
 | ||||
| import Types | ||||
| import Servant hiding (contentType) | ||||
| import ClassyPrelude | ||||
| import GHC.TypeLits | ||||
| import Server.Auth | ||||
| import Servant.Auth as SA | ||||
| import Servant.XML | ||||
| import qualified Database.Channel as Channel | ||||
| import Database.Book (Book(..)) | ||||
| import Database | ||||
| import qualified API.Books | ||||
| import           ClassyPrelude | ||||
| import           Database | ||||
| import           Database.Book (Book(..)) | ||||
| import qualified Database.Channel as Channel | ||||
| import           GHC.TypeLits | ||||
| import           Servant hiding (contentType) | ||||
| import           Servant.Auth as SA | ||||
| import qualified Servant.Docs as Docs | ||||
| import           Servant.XML | ||||
| import           Server.Auth | ||||
| import           System.IO.Unsafe (unsafePerformIO) | ||||
| import           Types | ||||
| 
 | ||||
| -- This is my first try on going to versioned apis, things might change | ||||
| -- I think my rule of thumb is that you can add new things as you want, but | ||||
| @@ -40,7 +42,7 @@ newtype Rel = Rel { unRel :: Text } deriving (IsString, Show) | ||||
| 
 | ||||
| data Pagination = Pagination { previous :: Maybe Rel | ||||
|                              , next :: Maybe Rel } | ||||
|                 deriving (Show) | ||||
|                 deriving (Show, Generic) | ||||
| 
 | ||||
| newtype SubSection = SubSection Rel deriving (Show) | ||||
| newtype Acquisition = Acquisition Rel deriving (Show) | ||||
| @@ -64,6 +66,20 @@ deriving instance Show (Entry 1) | ||||
| deriving instance Generic (Catalog 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 | ||||
|   toNode (SubSection rel) = [xml|<link type="application/atom+xml;profile=opds-catalog;kind=acquisition" rel="subsection" href="#{unRel rel}">|] | ||||
| 
 | ||||
| @@ -1,40 +1,47 @@ | ||||
| {-# Language DataKinds #-} | ||||
| {-# Language TypeFamilies #-} | ||||
| {-# Language TypeOperators #-} | ||||
| {-# Language NoImplicitPrelude #-} | ||||
| {-# Language MultiParamTypeClasses #-} | ||||
| {-# Language OverloadedStrings #-} | ||||
| {-# Language TemplateHaskell #-} | ||||
| {-# Language QuasiQuotes #-} | ||||
| {-# Language RecordWildCards #-} | ||||
| {-# Language DeriveGeneric #-} | ||||
| {-# Language FlexibleInstances #-} | ||||
| {-# Language TypeApplications #-} | ||||
| {-# Language DataKinds #-} | ||||
| {-# Language DuplicateRecordFields #-} | ||||
| {-# Language NamedFieldPuns #-} | ||||
| {-# LANGUAGE DataKinds             #-} | ||||
| {-# LANGUAGE DeriveGeneric         #-} | ||||
| {-# LANGUAGE DuplicateRecordFields #-} | ||||
| {-# LANGUAGE FlexibleInstances     #-} | ||||
| {-# LANGUAGE MultiParamTypeClasses #-} | ||||
| {-# LANGUAGE NamedFieldPuns        #-} | ||||
| {-# LANGUAGE NoImplicitPrelude     #-} | ||||
| {-# LANGUAGE OverloadedStrings     #-} | ||||
| {-# LANGUAGE QuasiQuotes           #-} | ||||
| {-# LANGUAGE RecordWildCards       #-} | ||||
| {-# LANGUAGE TemplateHaskell       #-} | ||||
| {-# LANGUAGE TypeApplications      #-} | ||||
| {-# LANGUAGE TypeFamilies          #-} | ||||
| {-# LANGUAGE TypeOperators         #-} | ||||
| module API.Channels (API, handler, JsonChannel(..)) where | ||||
| 
 | ||||
| import Servant | ||||
| import Types | ||||
| import ClassyPrelude | ||||
| import Server.Auth | ||||
| import Servant.Auth as SA | ||||
| import Control.Monad.Logger | ||||
| import Database | ||||
| import Database.Channel | ||||
| import Data.Aeson | ||||
| import Control.Lens | ||||
| import Data.Generics.Product | ||||
| import           ClassyPrelude | ||||
| import           Control.Lens | ||||
| import           Control.Monad.Catch   (MonadThrow, throwM) | ||||
| import           Control.Monad.Logger | ||||
| import           Data.Aeson | ||||
| import           Data.Generics.Product | ||||
| import           Database | ||||
| import           Database.Channel | ||||
| import           Servant | ||||
| import           Servant.Auth          as SA | ||||
| import qualified Servant.Docs          as Docs | ||||
| import           Server.Auth | ||||
| import           Types | ||||
| 
 | ||||
| data JsonChannel = JsonChannel { channel :: Text | ||||
| data JsonChannel = JsonChannel { channel    :: Text | ||||
|                                , visibility :: Visibility } | ||||
|                  deriving (Show, Generic) | ||||
| data UpdateChannel = UpdateChannel { identifier :: ChannelID | ||||
|                                    , channel :: Text | ||||
|                                    , channel    :: Text | ||||
|                                    , visibility :: Visibility } | ||||
|                  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 FromJSON JsonChannel | ||||
| instance ToJSON UpdateChannel | ||||
| @@ -42,6 +49,9 @@ instance FromJSON UpdateChannel | ||||
| 
 | ||||
| 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 | ||||
|           :<|> "channels" :> Capture "channel_id" ChannelID :> ReqBody '[JSON] UpdateChannel :> Put '[JSON] UpdateChannel | ||||
|           :<|> "channels" :> Get '[JSON] [JsonChannel] | ||||
| @@ -7,17 +7,19 @@ | ||||
| {-# Language TypeApplications #-} | ||||
| module API.Users  where | ||||
| 
 | ||||
| import Servant | ||||
| import ClassyPrelude | ||||
| import Types | ||||
| import Data.Aeson | ||||
| import Web.FormUrlEncoded | ||||
| import Database (runDB) | ||||
| import Database.User | ||||
| import Database.Schema | ||||
| import Server.Auth | ||||
| import Servant.Auth.Server as SAS | ||||
| import Servant.Auth as SA | ||||
| import           ClassyPrelude | ||||
| import           Control.Monad.Catch (throwM, MonadThrow) | ||||
| import           Data.Aeson | ||||
| import           Database (runDB) | ||||
| import           Database.Schema | ||||
| import           Database.User | ||||
| import           Servant | ||||
| import           Servant.Auth as SA | ||||
| import           Servant.Auth.Server as SAS | ||||
| import qualified Servant.Docs as Docs | ||||
| import           Server.Auth | ||||
| import           Types | ||||
| import           Web.FormUrlEncoded | ||||
| 
 | ||||
| 
 | ||||
| data RegisterForm = RegisterForm { username :: Username | ||||
| @@ -26,12 +28,17 @@ data RegisterForm = RegisterForm { username :: Username | ||||
|                                  , passwordAgain :: PlainPassword } | ||||
|                   deriving (Generic, Show) | ||||
| 
 | ||||
| instance Docs.ToSample RegisterForm | ||||
| 
 | ||||
| data LoginStatus = LoginStatus ( Maybe SafeUser ) deriving Generic | ||||
| 
 | ||||
| data RegisterStatus = RegisterStatus deriving Generic | ||||
| 
 | ||||
| instance Docs.ToSample RegisterStatus | ||||
| 
 | ||||
| instance ToJSON LoginStatus | ||||
| instance FromJSON LoginStatus | ||||
| instance Docs.ToSample LoginStatus | ||||
| 
 | ||||
| instance FromJSON RegisterForm | ||||
| instance ToJSON RegisterForm | ||||
| @@ -15,15 +15,17 @@ module Database | ||||
|   , SeldaT ) | ||||
|   where | ||||
| 
 | ||||
| import Data.Generics.Product | ||||
| import Control.Lens (view) | ||||
| import Data.Pool (Pool, withResource) | ||||
| import Database.Selda.Backend (SeldaConnection, runSeldaT, SeldaT) | ||||
| import Database.Selda (query, select, transaction) | ||||
| import Database.Selda.Generic (gen, fromRel, fromRels, toRel) | ||||
| import ClassyPrelude | ||||
| import Control.Lens (view) | ||||
| import Control.Monad.Catch (MonadMask) | ||||
| import Control.Monad.Trans.Control (MonadBaseControl) | ||||
| import Data.Generics.Product | ||||
| import Data.Pool (Pool, withResource) | ||||
| import Database.Selda (query, select, transaction) | ||||
| import Database.Selda.Backend (SeldaConnection, runSeldaT, SeldaT) | ||||
| import Database.Selda.Generic (gen, fromRel, fromRels, toRel) | ||||
| 
 | ||||
| type DBLike r m = (MonadIO m, MonadReader r m, MonadBaseControl IO m, MonadMask m, HasField' "database" r (Pool SeldaConnection)) | ||||
| type DBLike r m = (MonadBaseControl IO m, MonadIO m, MonadReader r m, HasField "database" r r (Pool SeldaConnection) (Pool SeldaConnection), MonadMask m) | ||||
| 
 | ||||
| runDB :: DBLike r m => SeldaT m a -> m a | ||||
| runDB q = do | ||||
| @@ -18,18 +18,17 @@ module Database.Book | ||||
|   , BookID) where | ||||
| 
 | ||||
| import ClassyPrelude | ||||
| import Database.Schema (books, users, Username, Book(..), BookID(..), UserID, HashDigest(..)) | ||||
| import Control.Lens (view) | ||||
| import Control.Monad.Catch (MonadCatch) | ||||
| import Data.Generics.Product | ||||
| import Database | ||||
| import Database.Channel (booksChannels, attachChannel, clearChannels) | ||||
| import Database.Schema (books, users, Username, Book(..), BookID(..), UserID, HashDigest(..)) | ||||
| import Database.Selda | ||||
| import Database.Selda.Generic | ||||
| 
 | ||||
| import Control.Lens (view) | ||||
| import Data.Generics.Product | ||||
| 
 | ||||
| import Database.Tag (booksTags, attachTag, clearTags) | ||||
| import Database.Channel (booksChannels, attachChannel, clearChannels) | ||||
| 
 | ||||
| usersBooks :: (MonadSelda m, MonadMask m, MonadIO m) => Username -> m [Book] | ||||
| usersBooks :: (MonadSelda m, MonadIO m) => Username -> m [Book] | ||||
| usersBooks username = fromRels <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -41,7 +40,7 @@ usersBooks username = fromRels <$> query q | ||||
|       return book | ||||
| 
 | ||||
| 
 | ||||
| getBook :: (MonadSelda m, MonadMask m, MonadIO m) => BookID -> Username -> m (Maybe Book) | ||||
| getBook :: (MonadSelda m, MonadIO m) => BookID -> Username -> m (Maybe Book) | ||||
| getBook identifier owner = listToMaybe . fromRels <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -56,7 +55,7 @@ data InsertBook = InsertBook { contentType :: Text | ||||
|                              , owner :: Username } | ||||
| 
 | ||||
| -- Always inserts | ||||
| insertBook :: (MonadSelda m, MonadMask m, MonadIO m) => InsertBook -> m (Maybe BookID) | ||||
| insertBook :: (MonadSelda m, MonadIO m) => InsertBook -> m (Maybe BookID) | ||||
| insertBook InsertBook{..} = do | ||||
|   mUserId <- query $ do | ||||
|     userId :*: _ :*: username' :*: _ <- select (gen users) | ||||
| @@ -75,7 +74,7 @@ data UpdateBook = UpdateBook { identifier :: BookID | ||||
|                              , channels :: [Text] } | ||||
|                 deriving (Show, Generic) | ||||
| 
 | ||||
| bookExists :: (MonadSelda m, MonadMask m, MonadIO m) => BookID -> m Bool | ||||
| bookExists :: (MonadSelda m, MonadIO m) => BookID -> m Bool | ||||
| bookExists identifier = not . null <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -83,7 +82,7 @@ bookExists identifier = not . null <$> query q | ||||
|       restrict (bookId .== literal identifier) | ||||
|       return bookId | ||||
| 
 | ||||
| isBookOwner :: (MonadSelda m, MonadIO m, MonadThrow m) => BookID -> Username -> m Bool | ||||
| isBookOwner :: (MonadSelda m, MonadIO m) => BookID -> Username -> m Bool | ||||
| isBookOwner identifier username = not . null <$> query (bookOwner' identifier username) | ||||
| 
 | ||||
| bookOwner' :: BookID -> Username -> Query s (Col s UserID :*: Col s BookID) | ||||
| @@ -95,7 +94,7 @@ bookOwner' identifier username = do | ||||
|   restrict (bookId .== literal identifier) | ||||
|   return (userId :*: bookId) | ||||
| 
 | ||||
| updateBook :: (MonadSelda m, MonadMask m, MonadIO m) => UpdateBook -> m (Maybe UpdateBook) | ||||
| updateBook :: (MonadCatch m, MonadSelda m, MonadIO m) => UpdateBook -> m (Maybe UpdateBook) | ||||
| updateBook UpdateBook{..} = do | ||||
|   clearTags identifier >> connectTags | ||||
|   clearChannels identifier >> connectChannels | ||||
| @@ -114,7 +113,7 @@ updateBook UpdateBook{..} = do | ||||
|     predicate (bookId :*: _) = bookId .== literal identifier | ||||
| 
 | ||||
| 
 | ||||
| getUpdateBook :: (MonadMask m, MonadIO m, MonadSelda m) => BookID -> Username -> m (Maybe UpdateBook) | ||||
| getUpdateBook :: (MonadIO m, MonadSelda m) => BookID -> Username -> m (Maybe UpdateBook) | ||||
| getUpdateBook bookId username = do | ||||
|   mBook <- getBook bookId username | ||||
|   forM mBook $ \Book{..} -> do | ||||
| @@ -122,7 +121,7 @@ getUpdateBook bookId username = do | ||||
|     tags <- map (view (field @"tag")) <$> booksTags bookId | ||||
|     return UpdateBook{owner=username,..} | ||||
| 
 | ||||
| setContent :: (MonadSelda m, MonadMask m, MonadIO m) => BookID -> Username -> HashDigest -> m () | ||||
| setContent :: (MonadSelda m, MonadIO m) => BookID -> Username -> HashDigest -> m () | ||||
| setContent identifier owner digest = do | ||||
|   mOwner <- query (bookOwner' identifier owner) | ||||
|   void $ forM (listToMaybe mOwner) $ \_ -> | ||||
| @@ -17,14 +17,15 @@ module Database.Channel | ||||
|   where | ||||
| 
 | ||||
| import ClassyPrelude | ||||
| import Database.Schema | ||||
| import Control.Monad.Catch (MonadMask) | ||||
| import Database | ||||
| import Database.Schema | ||||
| import Database.Selda | ||||
| import Database.Selda.Generic | ||||
| 
 | ||||
| import Control.Monad.Trans.Maybe | ||||
| 
 | ||||
| getChannel :: (MonadSelda m, MonadMask m, MonadIO m) => ChannelID -> m (Maybe Channel) | ||||
| getChannel :: (MonadSelda m, MonadIO m) => ChannelID -> m (Maybe Channel) | ||||
| getChannel identifier = listToMaybe . fromRels <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -32,10 +33,10 @@ getChannel identifier = listToMaybe . fromRels <$> query q | ||||
|       restrict (channelId .== literal identifier) | ||||
|       return ch | ||||
| 
 | ||||
| channelExists :: (MonadSelda m, MonadMask m, MonadIO m) => ChannelID -> m Bool | ||||
| channelExists :: (MonadSelda m, MonadIO m) => ChannelID -> m Bool | ||||
| channelExists identifier = not . null <$> getChannel identifier | ||||
| 
 | ||||
| isChannelOwner :: (MonadSelda m, MonadIO m, MonadMask m) => ChannelID -> Username -> m Bool | ||||
| isChannelOwner :: (MonadSelda m, MonadIO m) => ChannelID -> Username -> m Bool | ||||
| isChannelOwner identifier username = not . null <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -56,7 +57,7 @@ userChannels username = fromRels <$> query q | ||||
|       restrict (username' .== literal username) | ||||
|       return channel | ||||
| 
 | ||||
| updateChannelPrivacy :: (MonadMask m, MonadIO m, MonadSelda m) => ChannelID -> Visibility -> m (Maybe Channel) | ||||
| updateChannelPrivacy :: (MonadIO m, MonadSelda m) => ChannelID -> Visibility -> m (Maybe Channel) | ||||
| updateChannelPrivacy channelId visibility = do | ||||
|   void $ update (gen channels) predicate (\channel -> channel `with` [pVis := literal visibility]) | ||||
|   getChannel channelId | ||||
| @@ -81,7 +82,7 @@ insertChannel username channel visibility = runMaybeT $ do | ||||
|       restrict (user .== literal username) | ||||
|       return userId | ||||
| 
 | ||||
| channelBooks :: (MonadSelda m, MonadMask m, MonadIO m) => Username -> ChannelID -> m [Book] | ||||
| channelBooks :: (MonadSelda m, MonadIO m) => Username -> ChannelID -> m [Book] | ||||
| channelBooks username identifier = fromRels <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -94,7 +95,7 @@ channelBooks username identifier = fromRels <$> query q | ||||
|       restrict (bookId .== bookId') | ||||
|       return book | ||||
| 
 | ||||
| booksChannels :: (MonadSelda m, MonadMask m, MonadIO m) => BookID -> m [Channel] | ||||
| booksChannels :: (MonadSelda m, MonadIO m) => BookID -> m [Channel] | ||||
| booksChannels bookId = fromRels <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -104,7 +105,7 @@ booksChannels bookId = fromRels <$> query q | ||||
|       restrict (bookId' .== literal bookId) | ||||
|       return ch | ||||
| 
 | ||||
| attachChannel :: (MonadMask m, MonadIO m, MonadSelda m) => Username -> BookID -> Text -> m () | ||||
| attachChannel :: (MonadIO m, MonadSelda m) => Username -> BookID -> Text -> m () | ||||
| attachChannel username bookId channel = do | ||||
|   mCh <- fromRels <$> query channelQ | ||||
|   forM_ mCh $ \Channel{identifier} -> | ||||
| @@ -123,5 +124,5 @@ attachChannel username bookId channel = do | ||||
|       restrict (channel' .== literal channel) | ||||
|       return ch | ||||
| 
 | ||||
| clearChannels :: (MonadMask m, MonadIO m, MonadSelda m) => BookID -> m Int | ||||
| clearChannels :: (MonadIO m, MonadSelda m) => BookID -> m Int | ||||
| clearChannels bookId = deleteFrom (gen bookChannels) (\(_ :*: bookId') -> bookId' .== literal bookId) | ||||
| @@ -1,83 +1,92 @@ | ||||
| {-# Language NoImplicitPrelude #-} | ||||
| {-# Language DeriveGeneric #-} | ||||
| {-# Language OverloadedStrings #-} | ||||
| {-# Language DuplicateRecordFields #-} | ||||
| {-# Language GeneralizedNewtypeDeriving #-} | ||||
| {-# LANGUAGE DeriveGeneric              #-} | ||||
| {-# LANGUAGE DuplicateRecordFields      #-} | ||||
| {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||||
| {-# LANGUAGE NoImplicitPrelude          #-} | ||||
| {-# LANGUAGE OverloadedStrings          #-} | ||||
| module Database.Schema where | ||||
| 
 | ||||
| import ClassyPrelude | ||||
| import Database.Selda.Generic | ||||
| import Database.Selda | ||||
| import Database.Selda.Backend | ||||
| 
 | ||||
| import Data.Aeson | ||||
| import Web.HttpApiData | ||||
| import           ClassyPrelude | ||||
| import           Data.Aeson | ||||
| import           Database.Selda | ||||
| import           Database.Selda.Backend | ||||
| import           Database.Selda.Generic | ||||
| import qualified Servant.Docs           as Docs | ||||
| import           Web.HttpApiData | ||||
| 
 | ||||
| -- | User type | ||||
| newtype PlainPassword = PlainPassword Text deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Eq) | ||||
| newtype PlainPassword = PlainPassword Text deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Eq, Generic) | ||||
| newtype HashedPassword = HashedPassword {unHashed :: ByteString} | ||||
| data NoPassword = NoPassword | ||||
| 
 | ||||
| newtype Email = Email { unEmail :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData) | ||||
| newtype Email = Email { unEmail :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Generic, Eq, IsString) | ||||
| 
 | ||||
| newtype Username = Username { unUsername :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData) | ||||
| newtype Username = Username { unUsername :: Text } deriving (Show, ToJSON, FromJSON, ToHttpApiData, FromHttpApiData, Eq, Generic, IsString) | ||||
| 
 | ||||
| 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 | ||||
|   mkLit = LCustom . LBlob . unHashed | ||||
|   fromSql (SqlBlob x) = HashedPassword x | ||||
|   fromSql _ = error "fromSql: Bad hash" | ||||
|   fromSql _           = error "fromSql: Bad hash" | ||||
|   defaultValue = mkLit (HashedPassword "") -- Makes no sense | ||||
| 
 | ||||
| instance SqlType Email where | ||||
|   mkLit = LCustom . LText . unEmail | ||||
|   fromSql (SqlString x) = Email x | ||||
|   fromSql _ = error "fromSql: Bad email" | ||||
|   fromSql _             = error "fromSql: Bad email" | ||||
|   defaultValue = mkLit (Email "") | ||||
| 
 | ||||
| instance SqlType Username where | ||||
|   mkLit = LCustom . LText . unUsername | ||||
|   fromSql (SqlString x) = Username x | ||||
|   fromSql _ = error "fromSql: Bad username" | ||||
|   fromSql _             = error "fromSql: Bad username" | ||||
|   defaultValue = mkLit (Username "") | ||||
| 
 | ||||
| newtype UserID = UserID {unUserID :: Int} deriving (Show) | ||||
| 
 | ||||
| newtype BookID = BookID {unBookID :: Int} deriving (Show, ToJSON, FromJSON, FromHttpApiData, Eq, Ord, ToHttpApiData) | ||||
| newtype BookID = BookID {unBookID :: Int} deriving (Show, ToJSON, FromJSON, FromHttpApiData, Eq, Ord, ToHttpApiData, Generic, Num) | ||||
| 
 | ||||
| newtype ChannelID = ChannelID {unChannelID :: Int} deriving (Show, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON) | ||||
| newtype ChannelID = ChannelID {unChannelID :: Int} deriving (Show, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON, Eq, Generic, Num) | ||||
| 
 | ||||
| newtype TagID = TagID {unTagID :: Int} deriving (Show) | ||||
| 
 | ||||
| instance SqlType UserID where | ||||
|   mkLit = LCustom . LInt . unUserID | ||||
|   fromSql (SqlInt x) = UserID x | ||||
|   fromSql _ = error "fromSql: Bad userid" | ||||
|   fromSql _          = error "fromSql: Bad userid" | ||||
|   sqlType _ = TRowID | ||||
|   defaultValue = mkLit (UserID (-1)) | ||||
| instance SqlType BookID where | ||||
|   mkLit = LCustom . LInt . unBookID | ||||
|   fromSql (SqlInt x) = BookID x | ||||
|   fromSql _ = error "fromSql: Bad bookid" | ||||
|   fromSql _          = error "fromSql: Bad bookid" | ||||
|   defaultValue = mkLit (BookID (-1)) | ||||
| instance SqlType ChannelID where | ||||
|   mkLit = LCustom . LInt . unChannelID | ||||
|   fromSql (SqlInt x) = ChannelID x | ||||
|   fromSql _ = error "fromSql: Bad channelid" | ||||
|   fromSql _          = error "fromSql: Bad channelid" | ||||
|   defaultValue = mkLit (ChannelID (-1)) | ||||
| instance SqlType TagID where | ||||
|   mkLit = LCustom . LInt . unTagID | ||||
|   fromSql (SqlInt x) = TagID x | ||||
|   fromSql _ = error "fromSql: Bad tagid" | ||||
|   fromSql _          = error "fromSql: Bad tagid" | ||||
|   defaultValue = mkLit (TagID (-1)) | ||||
| 
 | ||||
| data User pass = User { identifier :: UserID | ||||
|                       , email :: Email | ||||
|                       , username :: Username | ||||
|                       , role :: Role | ||||
|                       , password :: pass } | ||||
|                       , email      :: Email | ||||
|                       , username   :: Username | ||||
|                       , role       :: Role | ||||
|                       , password   :: pass } | ||||
|           deriving (Show, Generic) | ||||
| 
 | ||||
| data Role = UserRole | AdminRole deriving (Show, Read, Enum, Bounded, Typeable, Generic) | ||||
| data Role = UserRole | AdminRole deriving (Show, Read, Enum, Bounded, Typeable, Generic, Eq) | ||||
| 
 | ||||
| instance ToJSON Role | ||||
| instance FromJSON Role | ||||
| @@ -98,18 +107,18 @@ users = genTable "users" [ (email :: User HashedPassword -> Email) :- uniqueGen | ||||
| -- | Book type | ||||
| newtype HashDigest = HashDigest { unHex :: ByteString } deriving Show | ||||
| -- XXX: Add an identifier for the book | ||||
| data Book = Book { identifier :: BookID | ||||
| data Book = Book { identifier  :: BookID | ||||
|                  , contentHash :: Maybe HashDigest | ||||
|                  , contentType :: Text | ||||
|                  , title :: Text | ||||
|                  , title       :: Text | ||||
|                  , description :: Maybe Text | ||||
|                  , owner :: UserID } | ||||
|                  , owner       :: UserID } | ||||
|           deriving (Show, Generic) | ||||
| 
 | ||||
| instance SqlType HashDigest where | ||||
|   mkLit = LCustom . LBlob . unHex | ||||
|   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 | ||||
| 
 | ||||
| books :: GenTable Book | ||||
| @@ -120,12 +129,12 @@ books = genTable "books" [ (identifier :: Book -> BookID) :- autoPrimaryGen | ||||
| 
 | ||||
| -- | Categorizing books | ||||
| data Tag = Tag { identifier :: TagID | ||||
|                , tag :: Text | ||||
|                , owner :: UserID } | ||||
|                , tag        :: Text | ||||
|                , owner      :: UserID } | ||||
|          deriving (Show, Generic) | ||||
| 
 | ||||
| data Visibility = Public | Private | Followers | ||||
|                 deriving (Show, Read, Generic) | ||||
|                 deriving (Show, Read, Generic, Eq) | ||||
| 
 | ||||
| instance ToJSON Visibility | ||||
| instance FromJSON Visibility | ||||
| @@ -137,8 +146,8 @@ instance SqlType Visibility where | ||||
|   defaultValue = mkLit Private | ||||
| 
 | ||||
| data Channel = Channel { identifier :: ChannelID | ||||
|                        , channel :: Text | ||||
|                        , owner :: UserID | ||||
|                        , channel    :: Text | ||||
|                        , owner      :: UserID | ||||
|                        , visibility :: Visibility } | ||||
|              deriving (Show, Generic) | ||||
| 
 | ||||
| @@ -154,12 +163,12 @@ channels = genTable "channels" [ (identifier :: Channel -> ChannelID) :- autoPri | ||||
|   where | ||||
|     i :*: _ = selectors (gen users) | ||||
| 
 | ||||
| data BookTag = BookTag { tag :: TagID | ||||
| data BookTag = BookTag { tag  :: TagID | ||||
|                        , book :: BookID } | ||||
|              deriving (Show, Generic) | ||||
| 
 | ||||
| data BookChannel = BookChannel { channel :: ChannelID | ||||
|                                , book :: BookID } | ||||
|                                , book    :: BookID } | ||||
|                  deriving (Show, Generic) | ||||
| 
 | ||||
| bookTags :: GenTable BookTag | ||||
| @@ -12,13 +12,14 @@ module Database.Tag | ||||
|   , Tag(..) ) where | ||||
| 
 | ||||
| import ClassyPrelude | ||||
| import Database.Schema | ||||
| import Control.Monad.Catch (MonadCatch) | ||||
| import Control.Monad.Trans.Maybe | ||||
| import Database | ||||
| import Database.Schema | ||||
| import Database.Selda | ||||
| import Database.Selda.Generic | ||||
| import Control.Monad.Trans.Maybe | ||||
| 
 | ||||
| upsertTag :: (MonadMask m, MonadIO m, MonadSelda m) => Username -> Text -> m (Maybe Tag) | ||||
| upsertTag :: (MonadCatch m, MonadIO m, MonadSelda m) => Username -> Text -> m (Maybe Tag) | ||||
| upsertTag username tag = runMaybeT $ do | ||||
|   userId <- MaybeT (listToMaybe <$> query userQ) | ||||
|   void $ lift $ upsert (gen tags) (predicate userId) id [toRel (Tag def tag userId)] | ||||
| @@ -34,7 +35,7 @@ upsertTag username tag = runMaybeT $ do | ||||
|       restrict (username' .== literal username) | ||||
|       return userId | ||||
| 
 | ||||
| booksTags :: (MonadMask m, MonadIO m, MonadSelda m) => BookID -> m [Tag] | ||||
| booksTags :: (MonadIO m, MonadSelda m) => BookID -> m [Tag] | ||||
| booksTags bookId = fromRels <$> query q | ||||
|   where | ||||
|     q = do | ||||
| @@ -44,7 +45,7 @@ booksTags bookId = fromRels <$> query q | ||||
|       restrict (bookId' .== literal bookId) | ||||
|       return tag | ||||
| 
 | ||||
| attachTag :: (MonadMask m, MonadIO m, MonadSelda m) => Username -> BookID -> Text -> m () | ||||
| attachTag :: (MonadCatch m, MonadIO m, MonadSelda m) => Username -> BookID -> Text -> m () | ||||
| attachTag username bookId tag = do | ||||
|   maybeT <- upsertTag username tag | ||||
|   forM_ maybeT $ \Tag{identifier} -> do | ||||
| @@ -56,6 +57,6 @@ attachTag username bookId tag = do | ||||
|       restrict (tagId' .== literal tagId .&& bookId' .== literal bookId) | ||||
|       return tagId' | ||||
| 
 | ||||
| clearTags :: (MonadMask m, MonadIO m, MonadSelda m) => BookID -> m Int | ||||
| clearTags :: (MonadIO m, MonadSelda m) => BookID -> m Int | ||||
| clearTags bookId = deleteFrom (gen bookTags) (\(_ :*: bookId') -> bookId' .== literal bookId) | ||||
| 
 | ||||
| @@ -5,20 +5,21 @@ | ||||
| module Database.User where | ||||
| 
 | ||||
| import ClassyPrelude | ||||
| import Control.Lens (view, over, _Just) | ||||
| import Control.Monad (mfilter) | ||||
| import Control.Monad.Catch (MonadMask) | ||||
| import Control.Monad.Logger | ||||
| import Crypto.KDF.BCrypt | ||||
| import Crypto.Random.Types (MonadRandom) | ||||
| import Data.Generics.Product | ||||
| import Database | ||||
| import Database.Schema | ||||
| import Database.Selda | ||||
| import Control.Lens (view, over, _Just) | ||||
| import Data.Generics.Product | ||||
| import Crypto.KDF.BCrypt | ||||
| import Crypto.Random.Types (MonadRandom) | ||||
| import Control.Monad.Logger | ||||
| import Control.Monad (mfilter) | ||||
| 
 | ||||
| data UserExistsError = UserExistsError | ||||
| 
 | ||||
| 
 | ||||
| insertUser :: (MonadLogger m, MonadIO m, MonadMask m, MonadRandom m) => Username -> Email -> PlainPassword -> SeldaT m (Either UserExistsError (User NoPassword)) | ||||
| insertUser :: (MonadMask m, MonadLogger m, MonadIO m, MonadRandom m) => Username -> Email -> PlainPassword -> SeldaT m (Either UserExistsError (User NoPassword)) | ||||
| insertUser username email (PlainPassword password) = | ||||
|   getUser' username >>= maybe insert' (const (return $ Left UserExistsError)) | ||||
|   where | ||||
| @@ -5,6 +5,7 @@ | ||||
| {-# Language FlexibleContexts #-} | ||||
| {-# Language TypeSynonymInstances #-} | ||||
| {-# Language FlexibleInstances #-} | ||||
| {-# Language ScopedTypeVariables #-} | ||||
| module Datastore where | ||||
| 
 | ||||
| import ClassyPrelude | ||||
| @@ -28,26 +29,26 @@ instance MonadDS AppM where | ||||
|   get = getLocal | ||||
| 
 | ||||
| putLocal :: ( MonadIO m | ||||
|             , HasField' "config" r config | ||||
|             , HasField' "store" config store | ||||
|             , HasField' "path" store Text | ||||
|             , HasField "config" r r config config | ||||
|             , HasField "store" config config store store | ||||
|             , HasType Text store | ||||
|             , MonadReader r m) | ||||
|             => ByteString -> m (Digest SHA256) | ||||
| putLocal bs = do | ||||
|   store <- unpack <$> view (field @"config" . field @"store" . field @"path") | ||||
|   store :: FilePath <- unpack <$> view (field @"config" . field @"store" . typed @Text) | ||||
|   liftIO $ createDirectoryIfMissing True store | ||||
|   let key = hashWith SHA256 bs | ||||
|   writeFile (store </> show key) bs | ||||
|   return key | ||||
| 
 | ||||
| getLocal :: ( MonadIO m | ||||
|             , HasField' "config" r config | ||||
|             , HasField' "store" config store | ||||
|             , HasField' "path" store Text | ||||
|             , HasField "config" r r config config | ||||
|             , HasField "store" config config store store | ||||
|             , HasType Text store | ||||
|             , MonadReader r m) | ||||
|             => Digest SHA256 -> m (Maybe ByteString) | ||||
| getLocal key = do | ||||
|   store <- unpack <$> view (field @"config" . field @"store" . field @"path") | ||||
|   store <- unpack <$> view (field @"config" . field @"store" . typed @Text) | ||||
|   liftIO $ createDirectoryIfMissing True store | ||||
|   let file = store </> show key | ||||
|   exists <- liftIO $ doesFileExist file | ||||
| @@ -3,19 +3,18 @@ | ||||
| {-# Language FlexibleContexts #-} | ||||
| module Devel.Main where | ||||
| 
 | ||||
| import Prelude | ||||
| import Control.Monad.Trans.Reader (runReaderT) | ||||
| import Main (withApp, defaultMain) | ||||
| import Control.Concurrent | ||||
| import Control.Monad (void) | ||||
| import Control.Monad.Trans.Reader (runReaderT) | ||||
| import Data.IORef (IORef, newIORef, readIORef, writeIORef) | ||||
| import Foreign.Store (Store(..), lookupStore, readStore, storeAction, withStore) | ||||
| import GHC.Word (Word32) | ||||
| import Dhall (input, auto) | ||||
| 
 | ||||
| import Database | ||||
| import Database.Schema | ||||
| import Database.Selda (tryCreateTable) | ||||
| import Database | ||||
| import Dhall (input, auto) | ||||
| import Foreign.Store (Store(..), lookupStore, readStore, storeAction, withStore) | ||||
| import GHC.Word (Word32) | ||||
| import Main (withApp, defaultMain) | ||||
| import Prelude | ||||
| 
 | ||||
| update :: IO () | ||||
| update = do | ||||
| @@ -37,7 +36,7 @@ update = do | ||||
| 
 | ||||
| develMain :: IO () | ||||
| develMain = do | ||||
|   conf <- input auto "./config/devel.dhall" | ||||
|   conf <- input auto "../config/devel.dhall" | ||||
|   withApp conf $ \app -> do | ||||
|     void $ runReaderT (runDB migrate) app | ||||
|     defaultMain app | ||||
| @@ -25,6 +25,7 @@ withApp :: Config -> (App -> IO ()) -> IO () | ||||
| withApp config f = do | ||||
|   let pgHost = view (field @"database" . field @"host") config | ||||
|       pgPort = 5432 | ||||
|       pgSchema = Nothing | ||||
|       pgDatabase = view (field @"database" . field @"database") config | ||||
|       pgUsername = Just (view (field @"database" . field @"username") config) | ||||
|       pgPassword = Just (view (field @"database" . field @"password") config) | ||||
							
								
								
									
										51
									
								
								backend/src/Server.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								backend/src/Server.hs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| {-# LANGUAGE DataKinds             #-} | ||||
| {-# LANGUAGE DeriveGeneric         #-} | ||||
| {-# LANGUAGE FlexibleInstances     #-} | ||||
| {-# LANGUAGE MultiParamTypeClasses #-} | ||||
| {-# LANGUAGE NoImplicitPrelude     #-} | ||||
| {-# LANGUAGE OverloadedStrings     #-} | ||||
| {-# LANGUAGE QuasiQuotes           #-} | ||||
| {-# LANGUAGE RecordWildCards       #-} | ||||
| {-# LANGUAGE ScopedTypeVariables   #-} | ||||
| {-# LANGUAGE TemplateHaskell       #-} | ||||
| {-# LANGUAGE TypeApplications      #-} | ||||
| {-# LANGUAGE TypeFamilies          #-} | ||||
| {-# LANGUAGE TypeOperators         #-} | ||||
| module Server where | ||||
|  | ||||
| import qualified API                   as API | ||||
| import           ClassyPrelude         hiding (Handler) | ||||
| import           Control.Lens | ||||
| import           Control.Monad.Except | ||||
| import           Control.Monad.Logger | ||||
| import           Data.Generics.Product | ||||
| import           Servant | ||||
| import           Servant.Auth.Docs     () | ||||
| import           Servant.Auth.Server   as SAS | ||||
| import qualified Servant.Docs          as Docs | ||||
| import           Servant.HTML.Lucid    (HTML) | ||||
| import           Server.Auth           (SafeUser) | ||||
| import           Server.Auth           (authCheck) | ||||
| import           Types | ||||
|  | ||||
| type API = API.API | ||||
|   :<|> "help" :> Get '[PlainText, HTML] String | ||||
|   :<|> "static" :> Raw | ||||
|  | ||||
| type Ctx = '[BasicAuthData -> IO (AuthResult SafeUser), CookieSettings, JWTSettings] | ||||
|  | ||||
| server :: App -> Application | ||||
| server app = serveWithContext api cfg (hoistServerWithContext (Proxy @ API.API) (Proxy @ Ctx) server' API.handler :<|> serveDocs :<|> serveDirectoryFileServer "static") | ||||
|   where | ||||
|     apiDocs :: Docs.API | ||||
|     apiDocs = Docs.docs (Proxy @API.API) | ||||
|     serveDocs = pure $ Docs.markdown apiDocs | ||||
|     myKey = view (field @"jwk") app | ||||
|     jwtCfg = defaultJWTSettings myKey | ||||
|     authCfg = authCheck app | ||||
|     cookieSettings = SAS.defaultCookieSettings{cookieIsSecure=SAS.NotSecure} | ||||
|     cfg = jwtCfg :. cookieSettings :. authCfg :. EmptyContext | ||||
|     server' :: AppM a -> Servant.Handler a | ||||
|     server' = Handler . ExceptT . try . (`runReaderT` app) . (runFileLoggingT "logs/server.log") | ||||
|     api :: Proxy API | ||||
|     api = Proxy | ||||
| @@ -1,11 +1,11 @@ | ||||
| {-# Language DataKinds #-} | ||||
| {-# Language TypeFamilies #-} | ||||
| {-# Language OverloadedStrings #-} | ||||
| {-# Language NoImplicitPrelude #-} | ||||
| {-# Language TypeOperators #-} | ||||
| {-# Language DuplicateRecordFields #-} | ||||
| {-# Language TypeApplications #-} | ||||
| {-# Language TemplateHaskell #-} | ||||
| {-# LANGUAGE DataKinds             #-} | ||||
| {-# LANGUAGE DuplicateRecordFields #-} | ||||
| {-# LANGUAGE NoImplicitPrelude     #-} | ||||
| {-# LANGUAGE OverloadedStrings     #-} | ||||
| {-# LANGUAGE TemplateHaskell       #-} | ||||
| {-# LANGUAGE TypeApplications      #-} | ||||
| {-# LANGUAGE TypeFamilies          #-} | ||||
| {-# LANGUAGE TypeOperators         #-} | ||||
| module Server.Auth | ||||
|   ( SafeUser(..) | ||||
|   , authCheck | ||||
| @@ -13,28 +13,33 @@ module Server.Auth | ||||
|   , requireLoggedIn) | ||||
|   where | ||||
| 
 | ||||
| import ClassyPrelude | ||||
| import Servant.Auth.Server as SAS | ||||
| import Data.Aeson | ||||
| import Database.Schema | ||||
| import Database.User | ||||
| import Database | ||||
| import Types | ||||
| import Control.Lens (view) | ||||
| import Data.Generics.Product | ||||
| import Servant (err401) | ||||
| import Control.Monad.Logger | ||||
| import           ClassyPrelude | ||||
| import           Control.Lens          (view) | ||||
| import           Control.Monad.Catch   (MonadThrow, throwM) | ||||
| import           Control.Monad.Logger | ||||
| import           Data.Aeson | ||||
| import           Data.Generics.Product | ||||
| import           Database | ||||
| import           Database.Schema | ||||
| import           Database.User | ||||
| import           Servant               (err401) | ||||
| import           Servant.Auth.Server   as SAS | ||||
| import qualified Servant.Docs          as Docs | ||||
| import           Types | ||||
| 
 | ||||
| -- generic-lens can convert similar types to this | ||||
| -- 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. | ||||
| -- | ||||
| -- 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 | ||||
|                          , role :: Role } | ||||
|                          , role     :: Role } | ||||
|               deriving (Show, Generic) | ||||
| 
 | ||||
| instance Docs.ToSample SafeUser where | ||||
|   toSamples _ = [("User", SafeUser "user@example.com" "user" UserRole )] | ||||
| 
 | ||||
| instance ToJSON SafeUser where | ||||
| instance FromJSON SafeUser where | ||||
| instance ToJWT SafeUser where | ||||
| @@ -53,6 +58,6 @@ authCheck app (BasicAuthData username password) = flip runReaderT app $ | ||||
|     password' = PlainPassword $ decodeUtf8 password | ||||
|     authenticated = SAS.Authenticated . view (super @SafeUser) | ||||
| 
 | ||||
| requireLoggedIn :: (MonadLogger m, MonadThrow m, Monad m) => (SafeUser -> m a) -> AuthResult SafeUser -> m a | ||||
| requireLoggedIn :: (MonadThrow m, MonadLogger m, Monad m) => (SafeUser -> m a) -> AuthResult SafeUser -> m a | ||||
| requireLoggedIn f (Authenticated user) = f user | ||||
| requireLoggedIn _ u = $logError (pack (show u)) >> throwM err401 | ||||
							
								
								
									
										65
									
								
								backend/src/Spec.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								backend/src/Spec.hs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| {-# Language TypeApplications #-} | ||||
| module Main where | ||||
|  | ||||
| import           API.Books | ||||
| import qualified Data.Aeson as A | ||||
| import           Data.Char (isPrint) | ||||
| import           Data.GenValidity.Text () | ||||
| import qualified Data.Text as T | ||||
| import           Database.Schema | ||||
| import           Prelude | ||||
| import           Test.Hspec | ||||
| import           Test.Validity | ||||
|  | ||||
| instance GenUnchecked PlainPassword | ||||
| instance GenValid PlainPassword | ||||
| instance GenInvalid PlainPassword | ||||
| instance Validity PlainPassword | ||||
| instance GenUnchecked Email | ||||
| instance GenValid Email | ||||
| instance GenInvalid Email | ||||
| instance Validity Email | ||||
| instance GenUnchecked Username | ||||
| instance GenValid Username | ||||
| instance GenInvalid Username | ||||
| instance Validity Username | ||||
| instance GenUnchecked BookID | ||||
| instance GenValid BookID | ||||
| instance GenInvalid BookID | ||||
| instance Validity BookID | ||||
| instance GenUnchecked ChannelID | ||||
| instance GenValid ChannelID | ||||
| instance GenInvalid ChannelID | ||||
| instance Validity ChannelID | ||||
| instance GenUnchecked Role | ||||
| instance GenValid Role | ||||
| instance GenInvalid Role | ||||
| instance Validity Role | ||||
| instance GenUnchecked Visibility | ||||
| instance GenValid Visibility | ||||
| instance GenInvalid Visibility | ||||
| instance Validity Visibility | ||||
| instance GenUnchecked JsonBook | ||||
| instance GenValid JsonBook | ||||
| instance GenInvalid JsonBook | ||||
| instance Validity JsonBook | ||||
| instance GenUnchecked PostBook | ||||
| instance GenValid PostBook | ||||
| instance GenInvalid PostBook | ||||
| instance Validity PostBook | ||||
|  | ||||
| spec :: Spec | ||||
| spec = do | ||||
|   describe "JSON encoding" $ do | ||||
|     it "Works for PlainPassword" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @PlainPassword) A.decode | ||||
|     it "Works for Email" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Email) A.decode | ||||
|     it "Username" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Username) A.decode | ||||
|     it "Works for BookID" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @BookID) A.decode | ||||
|     it "Works for ChannelID" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @ChannelID) A.decode | ||||
|     it "Works for Role" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Role) A.decode | ||||
|     it "Works for Visibility" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Visibility) A.decode | ||||
|     it "Works for JsonBook" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @JsonBook) A.decode | ||||
|     it "Works for PostBook" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @PostBook) A.decode | ||||
|  | ||||
| main :: IO () | ||||
| main = hspec spec | ||||
							
								
								
									
										30
									
								
								common/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								common/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| Copyright (c) 2018, Mats Rauhala | ||||
|  | ||||
| All rights reserved. | ||||
|  | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
|  | ||||
|     * Redistributions of source code must retain the above copyright | ||||
|       notice, this list of conditions and the following disclaimer. | ||||
|  | ||||
|     * Redistributions in binary form must reproduce the above | ||||
|       copyright notice, this list of conditions and the following | ||||
|       disclaimer in the documentation and/or other materials provided | ||||
|       with the distribution. | ||||
|  | ||||
|     * Neither the name of Mats Rauhala nor the names of other | ||||
|       contributors may be used to endorse or promote products derived | ||||
|       from this software without specific prior written permission. | ||||
|  | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										51
									
								
								common/common.cabal
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								common/common.cabal
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| name:                common | ||||
| version:             0.1.0.0 | ||||
| -- synopsis: | ||||
| -- description: | ||||
| license:             BSD3 | ||||
| license-file:        LICENSE | ||||
| author:              Mats Rauhala | ||||
| maintainer:          mats.rauhala@iki.fi | ||||
| -- copyright: | ||||
| category:            Web | ||||
| build-type:          Simple | ||||
| extra-source-files:  ChangeLog.md | ||||
| cabal-version:       >=1.10 | ||||
|  | ||||
| library | ||||
|   exposed-modules:     Configuration | ||||
|                      , Data.Versioned | ||||
|   -- other-extensions: | ||||
|   build-depends:       base >=4.10 | ||||
|                      , classy-prelude | ||||
|                      , dhall | ||||
|                      , foreign-store | ||||
|                      , generic-lens | ||||
|                      , lens | ||||
|                      , mtl | ||||
|                      , text | ||||
|                      , transformers | ||||
|   hs-source-dirs:      src | ||||
|   default-extensions:  DeriveGeneric | ||||
|                      , NoImplicitPrelude | ||||
|                      , OverloadedStrings | ||||
|                      , RecordWildCards | ||||
|   default-language:    Haskell2010 | ||||
|  | ||||
| test-suite spec | ||||
|   type:                exitcode-stdio-1.0 | ||||
|   main-is:             Spec.hs | ||||
|   hs-source-dirs:      src | ||||
|   build-depends:       base >=4.10 | ||||
|                      , classy-prelude | ||||
|                      , dhall | ||||
|                      , foreign-store | ||||
|                      , generic-lens | ||||
|                      , lens | ||||
|                      , mtl | ||||
|                      , text | ||||
|                      , transformers | ||||
|                      , validity | ||||
|                      , genvalidity-hspec | ||||
|                      , genvalidity-property | ||||
|                      , hspec | ||||
| @@ -12,7 +12,9 @@ data Pg = Pg { username :: Text | ||||
|              , database :: Text } | ||||
|         deriving (Show, Generic) | ||||
| 
 | ||||
| newtype Store = Store { path :: Text } deriving (Show, Generic) | ||||
| data Store = Filestore { path :: Text } | ||||
|            | IPFS { common :: Text } | ||||
|            deriving (Show, Generic) | ||||
| 
 | ||||
| data Config = Config { database :: Pg | ||||
|                      , store :: Store } | ||||
							
								
								
									
										9
									
								
								common/src/Spec.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								common/src/Spec.hs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| module Main where | ||||
|  | ||||
| import Test.Hspec | ||||
|  | ||||
| spec :: Spec | ||||
| spec = describe "test" $ it "verifies tests work" $ True == True | ||||
|  | ||||
| main :: IO () | ||||
| main = hspec spec | ||||
							
								
								
									
										23
									
								
								default.nix
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								default.nix
									
									
									
									
									
								
							| @@ -1,10 +1,15 @@ | ||||
| { mkDerivation, base, stdenv }: | ||||
| mkDerivation { | ||||
|   pname = "ebook-manager"; | ||||
|   version = "0.1.0.0"; | ||||
|   src = ./.; | ||||
|   isLibrary = false; | ||||
|   isExecutable = true; | ||||
|   executableHaskellDepends = [ base ]; | ||||
|   license = stdenv.lib.licenses.bsd3; | ||||
| { nixpkgs, haskellPackages }: | ||||
|  | ||||
| (import ./project.nix nixpkgs) { | ||||
|   packages = { | ||||
|     common = ./common; | ||||
|     backend = ./backend; | ||||
|   }; | ||||
|   overrides = self: super: { | ||||
|     generic-lens = nixpkgs.haskell.lib.dontCheck super.generic-lens; | ||||
|   }; | ||||
|   tools = with haskellPackages; [ | ||||
|     ghcid | ||||
|     hasktags | ||||
|   ]; | ||||
| } | ||||
|   | ||||
							
								
								
									
										1
									
								
								migrations/V1.2__unique_tags.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								migrations/V1.2__unique_tags.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| create unique index tag_owner on tags (tag, owner); | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "url": "https://github.com/nixos/nixpkgs.git", | ||||
|   "rev": "83a5765b1fea2472ec9cf9d179d3efd18b45c77e", | ||||
|   "rev": "e0d250e5cf6d179e1ccc775472d89718f61fcfd1", | ||||
|   "date": "2018-01-08T11:52:28+01:00", | ||||
|   "sha256": "01rb61dkbzjbwnb3p8lgs03a94f4584199dlr0cwdmqzaxnp506h", | ||||
|   "sha256": "1iqpjz4czcpghbv924a5h4jvfmj6c8q6sl3b1z7blz3mi740aivs", | ||||
|   "fetchSubmodules": true | ||||
| } | ||||
|   | ||||
							
								
								
									
										39
									
								
								project.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								project.nix
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| nixpkgs: | ||||
|  | ||||
| let | ||||
|  | ||||
|   inherit (nixpkgs.lib) mapAttrs mapAttrsToList escapeShellArg optionalString concatStringsSep concatMapStringsSep; | ||||
|  | ||||
| in | ||||
|  | ||||
| { packages | ||||
| , overrides ? _ : _ : {} | ||||
| , tools ? [] | ||||
| }: | ||||
|  | ||||
| let | ||||
|  | ||||
|   overrides' = nixpkgs.lib.foldr nixpkgs.lib.composeExtensions (_: _: {}) [ | ||||
|     (self: super: mapAttrs (name: path: self.callCabal2nix name path {}) packages) | ||||
|     overrides | ||||
|   ]; | ||||
|   haskellPackages = nixpkgs.haskellPackages.override { overrides = overrides'; }; | ||||
|   packages' = mapAttrs (name: _: haskellPackages."${name}") packages; | ||||
|   mkShell = name: pkg: | ||||
|   let | ||||
|     n =  "${name}-shell"; | ||||
|     deps = haskellPackages.ghcWithHoogle (pkgs: pkg.buildInputs ++ pkg.propagatedBuildInputs); | ||||
|   in | ||||
|   { | ||||
|     name = "${n}"; | ||||
|     value = nixpkgs.buildEnv { | ||||
|       name = "${n}"; | ||||
|       paths = tools; | ||||
|       buildInputs = tools ++ [deps]; | ||||
|     }; | ||||
|   }; | ||||
|   shells = nixpkgs.lib.listToAttrs (mapAttrsToList mkShell packages'); | ||||
|  | ||||
| in | ||||
|  | ||||
| packages' // shells | ||||
							
								
								
									
										15
									
								
								release.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								release.nix
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| { nixpkgs ? import <nixpkgs> {} }: | ||||
|  | ||||
| let | ||||
|  | ||||
|   pinnedVersion = nixpkgs.lib.importJSON ./nixpkgs-version.json; | ||||
|   pinnedPkgs = import (nixpkgs.fetchFromGitHub { | ||||
|     owner = "NixOS"; | ||||
|     repo = "nixpkgs"; | ||||
|     inherit (pinnedVersion) rev sha256; | ||||
|   }) {}; | ||||
|   inherit (pinnedPkgs) pkgs; | ||||
|  | ||||
|   in | ||||
|  | ||||
| import ./default.nix { nixpkgs = pinnedPkgs; haskellPackages = pinnedPkgs.haskellPackages; } | ||||
| @@ -1,40 +0,0 @@ | ||||
| {-# Language DataKinds #-} | ||||
| {-# Language TypeFamilies #-} | ||||
| {-# Language TypeOperators #-} | ||||
| {-# Language NoImplicitPrelude #-} | ||||
| {-# Language MultiParamTypeClasses #-} | ||||
| {-# Language OverloadedStrings #-} | ||||
| {-# Language TemplateHaskell #-} | ||||
| {-# Language QuasiQuotes #-} | ||||
| {-# Language RecordWildCards #-} | ||||
| {-# Language DeriveGeneric #-} | ||||
| {-# Language FlexibleInstances #-} | ||||
| {-# Language TypeApplications #-} | ||||
| module Server where | ||||
|  | ||||
| import qualified API as API | ||||
| import Server.Auth (authCheck) | ||||
| import Servant | ||||
| import Types | ||||
| import ClassyPrelude hiding (Handler) | ||||
| import Control.Monad.Logger | ||||
| import Control.Monad.Except | ||||
| import Servant.Auth.Server as SAS | ||||
| import Control.Lens | ||||
| import Data.Generics.Product | ||||
|  | ||||
| type API = API.API :<|> "static" :> Raw | ||||
|  | ||||
|  | ||||
| server :: App -> Application | ||||
| server app = serveWithContext api cfg (enter server' API.handler :<|> serveDirectoryFileServer "static") | ||||
|   where | ||||
|     myKey = view (field @"jwk") app | ||||
|     jwtCfg = defaultJWTSettings myKey | ||||
|     authCfg = authCheck app | ||||
|     cookieSettings = SAS.defaultCookieSettings{cookieIsSecure=SAS.NotSecure} | ||||
|     cfg = jwtCfg :. cookieSettings :. authCfg :. EmptyContext | ||||
|     server' :: AppM :~> Servant.Handler | ||||
|     server' = NT (Handler . ExceptT . try . (`runReaderT` app) . (runFileLoggingT "logs/server.log")) | ||||
|     api :: Proxy API | ||||
|     api = Proxy | ||||
		Reference in New Issue
	
	Block a user