14 Commits

Author SHA1 Message Date
d9d24e031e Deprecation notice 2019-05-29 23:19:23 +03:00
5efb2deab6 Something that looks like a form 2019-01-22 23:35:26 +02:00
3e359afcbe Simple routing 2019-01-22 22:40:44 +02:00
ff231322c7 Everything under the same path 2019-01-22 16:16:36 +02:00
6ec2303b9f Configurable port 2019-01-22 00:08:51 +02:00
e56aa4f9c8 Fix type 2019-01-22 00:01:53 +02:00
2c369943e7 Migrations configuration 2019-01-21 22:35:05 +02:00
86085e146c Less logging 2019-01-21 21:51:17 +02:00
bd5feb8353 Fix warnings 2019-01-21 21:47:58 +02:00
0c0606506a Dummy readme 2019-01-21 21:32:10 +02:00
908db84232 Fix tests 2019-01-21 21:31:13 +02:00
c45ea0ecc8 wip 2018-12-20 00:33:32 +02:00
6767813879 wip 2018-12-20 00:29:25 +02:00
8818b6f495 Separate backend and frontend 2018-12-19 23:18:13 +02:00
29 changed files with 694 additions and 150 deletions

233
.stylish-haskell.yaml Normal file
View File

@ -0,0 +1,233 @@
# stylish-haskell configuration file
# ==================================
# The stylish-haskell tool is mainly configured by specifying steps. These steps
# are a list, so they have an order, and one specific step may appear more than
# once (if needed). Each file is processed by these steps in the given order.
steps:
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
# by default.
# - unicode_syntax:
# # In order to make this work, we also need to insert the UnicodeSyntax
# # language pragma. If this flag is set to true, we insert it when it's
# # not already present. You may want to disable it if you configure
# # language extensions using some other method than pragmas. Default:
# # true.
# add_language_pragma: true
# Align the right hand side of some elements. This is quite conservative
# and only applies to statements where each element occupies a single
# line.
- simple_align:
cases: true
top_level_patterns: true
records: true
# Import cleanup
- imports:
# There are different ways we can align names and lists.
#
# - global: Align the import names and import list throughout the entire
# file.
#
# - file: Like global, but don't add padding when there are no qualified
# imports in the file.
#
# - group: Only align the imports per group (a group is formed by adjacent
# import lines).
#
# - none: Do not perform any alignment.
#
# Default: global.
align: global
# The following options affect only import list alignment.
#
# List align has following options:
#
# - after_alias: Import list is aligned with end of import including
# 'as' and 'hiding' keywords.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_alias: Import list is aligned with start of alias or hiding.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - new_line: Import list starts always on new line.
#
# > import qualified Data.List as List
# > (concat, foldl, foldr, head, init, last, length)
#
# Default: after_alias
list_align: after_alias
# Right-pad the module names to align imports in a group:
#
# - true: a little more readable
#
# > import qualified Data.List as List (concat, foldl, foldr,
# > init, last, length)
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
# > init, last, length)
#
# - false: diff-safe
#
# > import qualified Data.List as List (concat, foldl, foldr, init,
# > last, length)
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
# > init, last, length)
#
# Default: true
pad_module_names: true
# Long list align style takes effect when import is too long. This is
# determined by 'columns' setting.
#
# - inline: This option will put as much specs on same line as possible.
#
# - new_line: Import list will start on new line.
#
# - new_line_multiline: Import list will start on new line when it's
# short enough to fit to single line. Otherwise it'll be multiline.
#
# - multiline: One line per import list entry.
# Type with constructor list acts like single import.
#
# > import qualified Data.Map as M
# > ( empty
# > , singleton
# > , ...
# > , delete
# > )
#
# Default: inline
long_list_align: inline
# Align empty list (importing instances)
#
# Empty list align has following options
#
# - inherit: inherit list_align setting
#
# - right_after: () is right after the module name:
#
# > import Vector.Instances ()
#
# Default: inherit
empty_list_align: inherit
# List padding determines indentation of import list on lines after import.
# This option affects 'long_list_align'.
#
# - <integer>: constant value
#
# - module_name: align under start of module name.
# Useful for 'file' and 'group' align settings.
list_padding: 4
# Separate lists option affects formatting of import list for type
# or class. The only difference is single space between type and list
# of constructors, selectors and class functions.
#
# - true: There is single space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
#
# - false: There is no space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
#
# Default: true
separate_lists: true
# Space surround option affects formatting of import lists on a single
# line. The only difference is single space after the initial
# parenthesis and a single space before the terminal parenthesis.
#
# - true: There is single space associated with the enclosing
# parenthesis.
#
# > import Data.Foo ( foo )
#
# - false: There is no space associated with the enclosing parenthesis
#
# > import Data.Foo (foo)
#
# Default: false
space_surround: false
# Language pragmas
- language_pragmas:
# We can generate different styles of language pragma lists.
#
# - vertical: Vertical-spaced language pragmas, one per line.
#
# - compact: A more compact style.
#
# - compact_line: Similar to compact, but wrap each line with
# `{-#LANGUAGE #-}'.
#
# Default: vertical.
style: vertical
# Align affects alignment of closing pragma brackets.
#
# - true: Brackets are aligned in same column.
#
# - false: Brackets are not aligned together. There is only one space
# between actual import and closing bracket.
#
# Default: true
align: true
# stylish-haskell can detect redundancy of some language pragmas. If this
# is set to true, it will remove those redundant pragmas. Default: true.
remove_redundant: true
# Replace tabs by spaces. This is disabled by default.
# - tabs:
# # Number of spaces to use for each tab. Default: 8, as specified by the
# # Haskell report.
# spaces: 8
# Remove trailing whitespace
- trailing_whitespace: {}
# Squash multiple spaces between the left and right hand sides of some
# elements into single spaces. Basically, this undoes the effect of
# simple_align but is a bit less conservative.
# - squash: {}
# A common setting is the number of columns (parts of) code will be wrapped
# to. Different steps take this into account. Default: 80.
columns: 80
# By default, line endings are converted according to the OS. You can override
# preferred format here.
#
# - native: Native newline format. CRLF on Windows, LF on other OSes.
#
# - lf: Convert to LF ("\n").
#
# - crlf: Convert to CRLF ("\r\n").
#
# Default: native.
newline: native
# Sometimes, language extensions are specified in a cabal file or from the
# command line instead of using language pragmas in the file. stylish-haskell
# needs to be aware of these, so it can parse the file correctly.
#
# No language extensions are enabled by default.
language_extensions:
- DeriveGeneric
- NoImplicitPrelude
- OverloadedStrings
- RecordWildCards
# - TemplateHaskell
# - QuasiQuotes

View File

@ -8,4 +8,5 @@ before_script:
- 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"
- nix build -f ./release.nix --option trusted-public-keys "masser-ebook-manager.cachix.org-1:mtFSkQ2MO5MvjUpulZoFKjKUIa8g8CTcdPVuJaPKS1w= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" --option binary-caches "https://masser-ebook-manager.cachix.org https://cache.nixos.org" ghc.backend ghc.frontend
- nix build -f ./release.nix --option trusted-public-keys "masser-ebook-manager.cachix.org-1:mtFSkQ2MO5MvjUpulZoFKjKUIa8g8CTcdPVuJaPKS1w= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" --option binary-caches "https://masser-ebook-manager.cachix.org https://cache.nixos.org" ghcjs.frontend

2
README.md Normal file
View File

@ -0,0 +1,2 @@
**DEPRECATED**
[![Build Status](https://travis-ci.org/MasseR/ebook-manager.svg?branch=master)](https://travis-ci.org/MasseR/ebook-manager)

View File

@ -17,12 +17,14 @@ cabal-version: >=1.10
executable backend
main-is: Main.hs
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
other-modules: Devel.Main
, API
, API.Books
, API.Catalogue
, API.Channels
, API.Users
, Configuration
, Database
, Database.Book
, Database.Channel
@ -93,25 +95,30 @@ test-suite spec
main-is: Spec.hs
hs-source-dirs: src
build-depends: base >=4.10
, exceptions
, monad-control
, common
, aeson
, asn1-data
, asn1-types
, bytestring
, classy-prelude
, common
, cryptonite
, dhall
, directory
, exceptions
, foreign-store
, generic-lens
, genvalidity-hspec
, genvalidity-hspec-aeson
, genvalidity-property
, genvalidity-text
, hspec
, http-api-data
, http-media
, jose
, lens
, lucid
, memory
, monad-control
, monad-logger
, mtl
, pandoc
@ -123,25 +130,21 @@ test-suite spec
, selda-postgresql
, servant
, servant-auth
, servant-auth-docs
, servant-auth-server
, servant-docs
, servant-auth-docs
, servant-lucid
, servant-multipart
, servant-server
, text
, transformers
, validity
, wai
, warp
, x509
, x509-store
, xml-conduit
, xml-hamlet
, validity
, genvalidity-hspec
, genvalidity-property
, genvalidity-text
, hspec
default-extensions: DeriveGeneric
, NoImplicitPrelude
, OverloadedStrings

View File

@ -1,31 +1,21 @@
{-# Language DataKinds #-}
{-# Language TypeFamilies #-}
{-# Language TypeOperators #-}
{-# Language NoImplicitPrelude #-}
{-# Language MultiParamTypeClasses #-}
{-# Language OverloadedStrings #-}
{-# Language TemplateHaskell #-}
{-# Language QuasiQuotes #-}
{-# Language RecordWildCards #-}
{-# Language DeriveGeneric #-}
{-# Language FlexibleInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module API (API, handler) where
import Servant
import Servant.HTML.Lucid (HTML)
import Types
import View
import qualified API.Users as Users
import qualified API.Channels as Channels
import qualified API.Books as Books
import qualified API.Catalogue as Catalogue
import qualified API.Channels as Channels
import qualified API.Users as Users
data Index = Index
type API = Users.API
type API = "api" :> Users.API
:<|> "api" :> "current" :> Channels.API
:<|> "api" :> "current" :> Books.API
:<|> "api" :> "1" :> Catalogue.VersionedAPI 1
@ -38,11 +28,3 @@ handler = Users.handler
:<|> Catalogue.handler
:<|> Catalogue.handler
instance ToHtml Index where
toHtml _ = do
h1_ [class_ "title"] "Home page"
p_ [class_ "subtitle"] "Hello world"
toHtmlRaw = toHtml
indexHandler :: AppM (AppView Index)
indexHandler = mkView "Home" Index

View File

@ -17,7 +17,7 @@ module API.Books where
import ClassyPrelude
import Control.Lens
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.Catch (throwM)
import Control.Monad.Trans.Maybe
import Crypto.Hash (digestFromByteString)
import Data.Aeson
@ -63,8 +63,6 @@ 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

View File

@ -17,7 +17,7 @@
module API.Catalogue (VersionedAPI, handler) where
import qualified API.Books
import ClassyPrelude
import ClassyPrelude hiding (link)
import Database
import Database.Book (Book(..))
import qualified Database.Channel as Channel
@ -46,15 +46,16 @@ data Pagination = Pagination { previous :: Maybe Rel
newtype SubSection = SubSection Rel deriving (Show)
newtype Acquisition = Acquisition Rel deriving (Show)
newtype Time = Time { getTime :: UTCTime } deriving Show
data instance Entry 1 = EntryV1 { title :: Text
, identifier :: Text
, updated :: UTCTime
, updated :: Time
, content :: Text
, link :: Either SubSection Acquisition
}
data instance Catalog 1 = CatalogV1 { updated :: UTCTime
data instance Catalog 1 = CatalogV1 { updated :: Time
, self :: Rel
, start :: Rel
, pagination :: Pagination
@ -68,7 +69,7 @@ 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
instance Docs.ToSample Time where
toSamples _ = [("time", docsTime)]
instance Docs.ToSample Rel where
toSamples _ = [("Relative link", Rel "next")]
@ -76,8 +77,8 @@ 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
docsTime :: Time
docsTime = Time $ unsafePerformIO getCurrentTime
instance ToNode SubSection where
@ -91,7 +92,7 @@ instance ToNode (Entry 1) where
<entry>
<title>#{title}
<id>#{identifier}
<updated>#{iso8601 updated}
<updated>#{iso8601 $ getTime updated}
<content>#{content}
^{either toNode toNode link}
|]
@ -101,7 +102,7 @@ instance ToNode (Catalog 1) where
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
<id>#{unRel self}
<title>Give me a title
<updated>#{iso8601 updated}
<updated>#{iso8601 $ getTime updated}
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="self" href="#{unRel self}">
<link type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="start" href="#{unRel start}">
$maybe n <- (next pagination)
@ -125,7 +126,7 @@ relUrl x = Rel ("/api/current/" <> (pack . uriPath . linkURI $ x))
getBooksV1 :: Channel.ChannelID -> SafeUser -> AppM (Catalog 1)
getBooksV1 channelID SafeUser{username} = do
updated <- liftIO getCurrentTime
updated <- Time <$> liftIO getCurrentTime
let self = relUrl selfUrl
start = relUrl startUrl
selfUrl = safeLink (Proxy @(BaseAPI 1)) (Proxy @(ChannelCatalog 1)) channelID
@ -142,7 +143,7 @@ getBooksV1 channelID SafeUser{username} = do
getChannelsV1 :: SafeUser -> AppM (Catalog 1)
getChannelsV1 SafeUser{username} = do
updated <- liftIO getCurrentTime
updated <- Time <$> liftIO getCurrentTime
let self = relUrl selfUrl
-- I'm not sure if this safe link approach is really useable with this
-- api hierarchy since I can't access the topmost api from here. Also
@ -153,7 +154,7 @@ getChannelsV1 SafeUser{username} = do
entries <- map (fromChannel updated) <$> runDB (Channel.userChannels username)
pure CatalogV1{..}
where
fromChannel :: UTCTime -> Channel.Channel -> Entry 1
fromChannel :: Time -> Channel.Channel -> Entry 1
fromChannel updated Channel.Channel{..} =
let url = safeLink (Proxy @(BaseAPI 1)) (Proxy @(ChannelCatalog 1)) identifier
self = relUrl url

View File

@ -16,7 +16,7 @@ module API.Channels (API, handler, JsonChannel(..)) where
import ClassyPrelude
import Control.Lens
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Monad.Catch (throwM)
import Control.Monad.Logger
import Data.Aeson
import Data.Generics.Product
@ -49,9 +49,6 @@ 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]

View File

@ -1,14 +1,14 @@
{-# Language DataKinds #-}
{-# Language TypeFamilies #-}
{-# Language OverloadedStrings #-}
{-# Language NoImplicitPrelude #-}
{-# Language TypeOperators #-}
{-# Language DuplicateRecordFields #-}
{-# Language TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module API.Users where
import ClassyPrelude
import Control.Monad.Catch (throwM, MonadThrow)
import Control.Monad.Catch (throwM)
import Data.Aeson
import Database (runDB)
import Database.Schema

View File

@ -9,7 +9,8 @@ import Dhall (Interpret)
data Pg = Pg { username :: Text
, password :: Text
, host :: Text
, database :: Text }
, database :: Text
, migrations :: Text }
deriving (Show, Generic)
data Store = Filestore { path :: Text }
@ -17,9 +18,11 @@ data Store = Filestore { path :: Text }
deriving (Show, Generic)
data Config = Config { database :: Pg
, store :: Store }
, store :: Store
, port :: Integer }
deriving (Show, Generic)
instance Interpret Pg
instance Interpret Store
instance Interpret Config

View File

@ -3,6 +3,9 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
module Database.Schema where
import ClassyPrelude
@ -11,6 +14,7 @@ import Database.Selda
import Database.Selda.Backend
import Database.Selda.Generic
import qualified Servant.Docs as Docs
import Servant (Capture)
import Web.HttpApiData
-- | User type
@ -53,8 +57,14 @@ newtype UserID = UserID {unUserID :: Int} deriving (Show)
newtype BookID = BookID {unBookID :: Int} deriving (Show, ToJSON, FromJSON, FromHttpApiData, Eq, Ord, ToHttpApiData, Generic, Num)
instance Docs.ToCapture (Capture "book_id" BookID) where
toCapture _ = Docs.DocCapture "book_id" "The book id"
newtype ChannelID = ChannelID {unChannelID :: Int} deriving (Show, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON, Eq, Generic, Num)
instance Docs.ToCapture (Capture "channel_id" ChannelID) where
toCapture _ = Docs.DocCapture "channel_id" "The channel id"
newtype TagID = TagID {unTagID :: Int} deriving (Show)
instance SqlType UserID where

View File

@ -1,25 +1,27 @@
{-# Language OverloadedStrings #-}
{-# Language RecordWildCards #-}
{-# Language DuplicateRecordFields #-}
{-# Language TypeApplications #-}
{-# Language DataKinds #-}
{-# Language NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Server (server)
import Network.Wai.Handler.Warp (run)
import Types
import Configuration
import Dhall (input, auto)
import ClassyPrelude
import Control.Lens (view)
import Configuration
import Control.Lens (view, to)
import Data.Generics.Product
import Data.Pool (createPool)
import Database.Selda.PostgreSQL (PGConnectInfo(..), pgOpen, seldaClose)
import Database.Selda.PostgreSQL (PGConnectInfo (..), pgOpen,
seldaClose)
import Dhall (auto, input)
import Network.Wai.Handler.Warp (run)
import Servant.Auth.Server (generateKey)
import Server (server)
import Types
import System.Environment (getEnvironment)
defaultMain :: App -> IO ()
defaultMain = run 8080 . server
defaultMain app = run (view (field @"config" . field @"port" . to fromIntegral) app) $ server app
withApp :: Config -> (App -> IO ()) -> IO ()
withApp config f = do
@ -35,5 +37,6 @@ withApp config f = do
main :: IO ()
main = do
c <- input auto "./config/config.dhall"
path <- fmap pack . lookup "CONF" <$> getEnvironment
c <- input auto (fromMaybe "./config/config.dhall" path)
withApp c defaultMain

View File

@ -1,19 +1,15 @@
{-# 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 qualified API
import ClassyPrelude hiding (Handler)
import Control.Lens
import Control.Monad.Except
@ -24,18 +20,16 @@ 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 Server.Auth (SafeUser, authCheck)
import Types
type API = API.API
:<|> "help" :> Get '[PlainText, HTML] String
:<|> "static" :> Raw
:<|> "api" :> "help" :> Get '[PlainText, HTML] String
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")
server app = serveWithContext api cfg (hoistServerWithContext (Proxy @ API.API) (Proxy @ Ctx) server' API.handler :<|> serveDocs)
where
apiDocs :: Docs.API
apiDocs = Docs.docs (Proxy @API.API)
@ -46,6 +40,6 @@ server app = serveWithContext api cfg (hoistServerWithContext (Proxy @ API.API)
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")
server' = Handler . ExceptT . try . (`runReaderT` app) . runFileLoggingT "logs/server.log"
api :: Proxy API
api = Proxy

View File

@ -1,4 +1,4 @@
{-# Language TypeApplications #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import API.Books
@ -10,6 +10,7 @@ import Database.Schema
import Prelude
import Test.Hspec
import Test.Validity
import Test.Validity.Aeson
instance GenUnchecked PlainPassword
instance GenValid PlainPassword
@ -51,15 +52,15 @@ 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
jsonSpecOnValid @PlainPassword
jsonSpecOnValid @Email
jsonSpecOnValid @Username
jsonSpecOnValid @BookID
jsonSpecOnValid @ChannelID
jsonSpecOnValid @Role
jsonSpecOnValid @Visibility
jsonSpecOnValid @JsonBook
jsonSpecOnValid @PostBook
main :: IO ()
main = hspec spec

View File

@ -13,13 +13,10 @@ extra-source-files: ChangeLog.md
cabal-version: >=1.10
library
exposed-modules: Configuration
, Data.Versioned
exposed-modules: Data.Versioned
-- other-extensions:
build-depends: base >=4.10
, classy-prelude
, dhall
, foreign-store
, generic-lens
, lens
, mtl
@ -32,20 +29,19 @@ library
, 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
-- test-suite spec
-- type: exitcode-stdio-1.0
-- main-is: Spec.hs
-- hs-source-dirs: src
-- build-depends: base >=4.10
-- , classy-prelude
-- , foreign-store
-- , generic-lens
-- , lens
-- , mtl
-- , text
-- , transformers
-- , validity
-- , genvalidity-hspec
-- , genvalidity-property
-- , hspec

View File

@ -0,0 +1,8 @@
{ database : { username : Text
, password : Text
, host : Text
, database : Text
, migrations : Text }
, store : < Filestore : { path : Text } | IPFS : { common : Text } >
, port : Integer
}

View File

@ -4,5 +4,7 @@
, password = "password"
, host = "hostname"
, database = "ebook"
, migrations = "./migrations"
}
store = { path = "/tmp/store" }
}

View File

@ -1,12 +1,59 @@
{ nixpkgs, haskellPackages }:
let
miso = nixpkgs.fetchFromGitHub {
owner = "dmjio";
repo = "miso";
rev = "630e823dd40a434b73124e12b229a79d9fefb01d";
sha256 = "046gdp3ah2lsipfcy89rh20mn08xbhcgrj549v8zzy69j33xjm2l";
};
miso-jsaddle = super: if haskellPackages.ghc.isGhcjs or false then (super.callPackage (miso + "/miso-ghcjs.nix") {}) else (super.callPackage (miso + "/miso-ghc-jsaddle.nix") {});
dontCheck = nixpkgs.haskell.lib.dontCheck;
in
(import ./project.nix nixpkgs) {
haskellPackages = haskellPackages;
packages = {
common = ./common;
backend = ./backend;
frontend = ./frontend;
};
overrides = self: super: {
generic-lens = nixpkgs.haskell.lib.dontCheck super.generic-lens;
miso = miso-jsaddle super;
# doctest = null; # Not compilable with ghcjs
# comonad = dontCheck super.comonad;
# Glob = dontCheck super.Glob;
# SHA = dontCheck super.SHA;
# iproute = dontCheck super.iproute;
# semigroupoids = dontCheck super.semigroupoids;
# wai-app-static = dontCheck super.wai-app-static;
# attoparsec = dontCheck super.attoparsec;
# http-date = dontCheck super.http-date;
# lens = dontCheck super.lens;
# unix-time = dontCheck super.unix-time;
# http-types = dontCheck super.http-types;
# servant = dontCheck super.servant;
# servant-server = dontCheck super.servant-server;
# servant-auth-docs = dontCheck super.servant-auth-docs;
# lens-aeson = dontCheck super.lens-aeson;
# word8 = dontCheck super.word8;
# http2 = dontCheck super.http2;
# wai-extra = dontCheck super.wai-extra;
# pgp-wordlist = dontCheck super.pgp-wordlist;
# prettyprinter = dontCheck super.prettyprinter;
# unliftio = dontCheck super.unliftio;
# prettyprinter-ansi-terminal = dontCheck super.prettyprinter-ansi-terminal;
# distributive = dontCheck super.distributive;
# genvalidity-property = dontCheck super.genvalidity-property;
# genvalidity-hspec = dontCheck super.genvalidity-hspec;
# genvalidity = dontCheck super.genvalidity;
# megaparsec = dontCheck super.megaparsec;
# ncurses = null;
# haskeline = super.callHackage "haskeline" "0.7.4.2" {};
# terminfo = super.callHackage "terminfo" "0.4.1.1" {};
};
tools = with haskellPackages; [
ghcid

5
frontend/ChangeLog.md Normal file
View File

@ -0,0 +1,5 @@
# Revision history for frontend
## 0.1.0.0 -- YYYY-mm-dd
* First version. Released on an unsuspecting world.

30
frontend/LICENSE Normal file
View 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
frontend/Setup.hs Normal file
View File

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

32
frontend/frontend.cabal Normal file
View File

@ -0,0 +1,32 @@
-- Initial frontend.cabal generated by cabal init. For further
-- documentation, see http://haskell.org/cabal/users-guide/
name: frontend
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
executable frontend
main-is: Main.hs
-- other-modules:
-- other-extensions:
build-depends: base >=4.11 && <4.12
, common
, generic-lens
, jsaddle-warp
, lens
, miso
, mtl
, servant
hs-source-dirs: src
default-language: Haskell2010
ghc-options: -Wall

93
frontend/src/Main.hs Normal file
View File

@ -0,0 +1,93 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
module Main where
import Control.Lens (over, set)
import Control.Monad.Trans (liftIO)
import Data.Generics.Product
import GHC.Generics (Generic)
import Language.Javascript.JSaddle.Warp
import Miso hiding (set)
import Miso.String
import Servant.API
import Servant.Links
import Data.Proxy (Proxy(..))
type API = Home :<|> Login :<|> Register
type Home = View Action
type Login = "login" :> View Action
type Register = "register" :> View Action
data Action = Add
| Subtract
| SayHello
| HandleURI URI
| ChangeURI URI
| NoOp
data Model = Model { counter :: Int
, uri :: URI }
deriving (Eq, Generic)
updateModel :: Model -> Action -> Effect Action Model
updateModel m = \case
Add -> noEff (over (field @"counter") (+1) m)
Subtract -> noEff (over (field @"counter") (\x -> x - 1) m)
SayHello -> m <# (liftIO (putStrLn "Hello world") >> pure NoOp)
HandleURI uri -> noEff (set (field @"uri") uri m)
ChangeURI uri -> m <# do
liftIO $ putStrLn $ "Pushing uri " <> show uri
pushURI uri
return $ HandleURI uri
NoOp -> noEff m
viewModel :: Model -> View Action
viewModel model = view
where
view = either (const the404) id $ runRoute @API Proxy handlers uri model
handlers = home :<|> login :<|> register
home _ = div_ [] [ button_ [ onClick Add ] [ text "+" ]
, text (ms (counter model))
, button_ [ onClick Subtract ] [ text "-" ]
, button_ [ onClick goLogin ] [ text "go login" ]
, button_ [ onClick goRegister ] [ text "go register" ]
]
login _ = div_ [] []
register _ = div_ [] [
h3_ [] [text "register"]
, label_ [] [text "Username"], input_ [id_ "username", name_ "username"]
, label_ [] [text "Email"], input_ [id_ "email", name_ "email"]
, label_ [] [text "Password"], input_ [id_ "password", name_ "password"]
, label_ [] [text "Password again"], input_ [id_ "passwordAgain", name_ "passwordAgain"]
, button_ [] [text "Register"]
]
the404 = div_ [] []
goLogin, goHome, goRegister :: Action
goLogin = goto @Login @API Proxy Proxy
goHome = goto @Home @API Proxy Proxy
goRegister = goto @Register @API Proxy Proxy
goto :: (IsElem endpoint api, HasLink endpoint, MkLink endpoint Link ~ Link) => Proxy api -> Proxy endpoint -> Action
goto a b = ChangeURI (linkURI (safeLink a b))
main :: IO ()
main = run 8081 $ do
model <- mkModel
startApp App{..}
where
mkModel = Model <$> pure 0 <*> getCurrentURI
initialAction = SayHello
update = flip updateModel
view = viewModel
subs = [ uriSub HandleURI ]
events = defaultEvents
mountPoint = Nothing

16
ghcjs.patch Normal file
View File

@ -0,0 +1,16 @@
diff --git a/lib/boot/shims/src/string.js b/lib/boot/shims/src/string.js
index d82f75a..f16e55a 100644
--- a/lib/boot/shims/src/string.js
+++ b/lib/boot/shims/src/string.js
@@ -780,11 +780,7 @@ function h$throwJSException(e) {
// adding the Exception dictionary
var strVal = e.toString() + '\n' + Array.prototype.join.call(e.stack, '\n');
var someE = MK_SOMEEXCEPTION(HS_JSEXCEPTION_EXCEPTION,
-#ifdef GHCJS_PROF
- MK_JSEXCEPTION(MK_JSVAL(e), h$toHsString(strVal), h$CCS_SYSTEM))
-#else
MK_JSEXCEPTION(MK_JSVAL(e), h$toHsString(strVal))
-#endif
);
return h$throw(someE, true);
}

14
jsaddle-warp-ghcjs.nix Normal file
View File

@ -0,0 +1,14 @@
{ mkDerivation, base, fetchgit, stdenv }:
mkDerivation {
pname = "jsaddle-warp";
version = "0.9.5.0";
src = fetchgit {
url = "https://github.com/ghcjs/jsaddle.git";
rev = "34fe7d61b3f387b81aa748294ac8d993243f53b4";
sha256 = "0qdh5qdk23vcp1yp910zgw2hs4zpbx9ig25xgaax0iwj2m1ifh5x";
};
postUnpack = "sourceRoot+=/jsaddle-warp; echo source root reset to $sourceRoot";
libraryHaskellDepends = [ base ];
description = "Interface for JavaScript that works with GHCJS and GHC";
license = stdenv.lib.licenses.mit;
}

View File

@ -1,7 +1,7 @@
{
"url": "https://github.com/nixos/nixpkgs.git",
"rev": "e0d250e5cf6d179e1ccc775472d89718f61fcfd1",
"rev": "4507926b80c6b8f73053775ffee17f6781c7e7c8",
"date": "2018-01-08T11:52:28+01:00",
"sha256": "1iqpjz4czcpghbv924a5h4jvfmj6c8q6sl3b1z7blz3mi740aivs",
"sha256": "068v9xh7d8klk62p2qwr76fyfqfh1bp08xc12x138g5q6pg6yfzb",
"fetchSubmodules": true
}

View File

@ -6,7 +6,8 @@ let
in
{ packages
{ haskellPackages
, packages
, overrides ? _ : _ : {}
, tools ? []
}:
@ -17,12 +18,17 @@ let
(self: super: mapAttrs (name: path: self.callCabal2nix name path {}) packages)
overrides
];
haskellPackages = nixpkgs.haskellPackages.override { overrides = overrides'; };
packages' = mapAttrs (name: _: haskellPackages."${name}") packages;
haskellPackages' = haskellPackages.extend overrides';
packages' = mapAttrs (name: _: haskellPackages'."${name}") packages;
mkShell = name: pkg:
let
n = "${name}-shell";
deps = haskellPackages.ghcWithHoogle (pkgs: pkg.buildInputs ++ pkg.propagatedBuildInputs);
deps = with haskellPackages'; [
ghcid
cabal-install
hasktags
(haskellPackages'.ghcWithHoogle (pkgs: pkg.buildInputs ++ pkg.propagatedBuildInputs))
];
in
{
name = "${n}";

View File

@ -2,14 +2,71 @@
let
jsaddle = nixpkgs.fetchFromGitHub {
owner = "ghcjs";
repo = "jsaddle";
rev = "34fe7d61b3f387b81aa748294ac8d993243f53b4";
sha256 = "0qdh5qdk23vcp1yp910zgw2hs4zpbx9ig25xgaax0iwj2m1ifh5x";
};
config = {
packageOverrides = pkgs: with pkgs.haskell.lib; with pkgs.lib; {
haskell = pkgs.haskell // {
packages = pkgs.haskell.packages // {
ghccustom = pkgs.haskell.packages.ghc843.override {
overrides = self: super: {
jsaddle-warp = dontCheck (super.callPackage (jsaddle + "/jsaddle-warp") {});
# jsaddle-warp = super.callPackage ./jsaddle-warp-ghcjs.nix {};
jsaddle = dontCheck (super.callPackage (jsaddle + "/jsaddle") {});
};
};
ghcjscustom = pkgs.haskell.packages.ghcjs84.override {
overrides = self: super: {
doctest = null;
comonad = dontCheck (super.comonad);
classy-prelude = dontCheck (super.classy-prelude);
unliftio = dontCheck (super.unliftio);
semigroupoids = dontCheck (super.semigroupoids);
lens = dontCheck (super.lens);
directory-tree = dontCheck (super.directory-tree);
http-types = dontCheck (super.http-types);
tasty-quickcheck = dontCheck (super.tasty-quickcheck);
scientific = dontCheck (super.scientific);
servant = dontCheck (super.servant);
jsaddle-warp = super.callPackage ./jsaddle-warp-ghcjs.nix {};
ghc = overrideDerivation (super.ghc.override {
ghcjsSrc = pkgs.fetchgit {
url = "https://github.com/ghcjs/ghcjs.git";
rev = "dc190b1bb2453cfa484124e9f335ee3cad1492f7";
sha256 = "0dh52gj0f3700zfyrhisy44b6y9p1bsawwrmd5pllpdyw21zd9lw";
fetchSubmodules = true;
};
}) (drv: { patches = (drv.patches or []) ++ [ ./ghcjs.patch ]; });
};
};
};
};
};
};
pinnedVersion = nixpkgs.lib.importJSON ./nixpkgs-version.json;
pinnedPkgs = import (nixpkgs.fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs";
inherit (pinnedVersion) rev sha256;
}) {};
}) { inherit config; };
ghc = import ./default.nix { nixpkgs = pinnedPkgs; haskellPackages = pinnedPkgs.haskell.packages.ghccustom; };
ghcjs = import ./default.nix { nixpkgs = pinnedPkgs; haskellPackages = pinnedPkgs.haskell.packages.ghcjscustom; };
inherit (pinnedPkgs) pkgs;
in
import ./default.nix { nixpkgs = pinnedPkgs; haskellPackages = pinnedPkgs.haskellPackages; }
{
inherit ghc ghcjs;
deps = pkgs.buildEnv {
name = "deps";
paths = [
(pkgs.haskell.packages.ghccustom.ghcWithPackages (_: ghc.backend.buildInputs ++ ghc.backend.propagatedBuildInputs))
(pkgs.haskell.packages.ghcjscustom.ghcWithPackages (_: ghcjs.frontend.buildInputs ++ ghcjs.frontend.propagatedBuildInputs))
];
buildInputs = [ ];
};
}

8
to-flyway.dhall Normal file
View File

@ -0,0 +1,8 @@
\(conf : ./config/Configuration.dhall)
->
''
flyway.locations=filesystem:${conf.database.migrations}/
flyway.url=jdbc:postgresql://${conf.database.host}/${conf.database.database}
flyway.user=${conf.database.username}
flyway.password=${conf.database.password}
''