Compare commits

..

11 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
19 changed files with 427 additions and 128 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,5 +8,5 @@ before_script:
- mkdir -p ~/.config/nixpkgs - mkdir -p ~/.config/nixpkgs
script: script:
- nix-build ./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" -A ghc.backend -A 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" ghc.backend ghc.frontend
- nix-build ./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" -A ghcjs.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,6 +17,7 @@ cabal-version: >=1.10
executable backend executable backend
main-is: Main.hs main-is: Main.hs
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
other-modules: Devel.Main other-modules: Devel.Main
, API , API
, API.Books , API.Books
@ -94,25 +95,30 @@ test-suite spec
main-is: Spec.hs main-is: Spec.hs
hs-source-dirs: src hs-source-dirs: src
build-depends: base >=4.10 build-depends: base >=4.10
, exceptions
, monad-control
, common
, aeson , aeson
, asn1-data , asn1-data
, asn1-types , asn1-types
, bytestring , bytestring
, classy-prelude , classy-prelude
, common
, cryptonite , cryptonite
, dhall , dhall
, directory , directory
, exceptions
, foreign-store , foreign-store
, generic-lens , generic-lens
, genvalidity-hspec
, genvalidity-hspec-aeson
, genvalidity-property
, genvalidity-text
, hspec
, http-api-data , http-api-data
, http-media , http-media
, jose , jose
, lens , lens
, lucid , lucid
, memory , memory
, monad-control
, monad-logger , monad-logger
, mtl , mtl
, pandoc , pandoc
@ -124,25 +130,21 @@ test-suite spec
, selda-postgresql , selda-postgresql
, servant , servant
, servant-auth , servant-auth
, servant-auth-docs
, servant-auth-server , servant-auth-server
, servant-docs , servant-docs
, servant-auth-docs
, servant-lucid , servant-lucid
, servant-multipart , servant-multipart
, servant-server , servant-server
, text , text
, transformers , transformers
, validity
, wai , wai
, warp , warp
, x509 , x509
, x509-store , x509-store
, xml-conduit , xml-conduit
, xml-hamlet , xml-hamlet
, validity
, genvalidity-hspec
, genvalidity-property
, genvalidity-text
, hspec
default-extensions: DeriveGeneric default-extensions: DeriveGeneric
, NoImplicitPrelude , NoImplicitPrelude
, OverloadedStrings , OverloadedStrings

View File

@ -1,31 +1,21 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
module API (API, handler) where module API (API, handler) where
import Servant import Servant
import Servant.HTML.Lucid (HTML)
import Types import Types
import View
import qualified API.Books as Books import qualified API.Books as Books
import qualified API.Catalogue as Catalogue import qualified API.Catalogue as Catalogue
import qualified API.Channels as Channels import qualified API.Channels as Channels
import qualified API.Users as Users import qualified API.Users as Users
data Index = Index type API = "api" :> Users.API
type API = Users.API
:<|> "api" :> "current" :> Channels.API :<|> "api" :> "current" :> Channels.API
:<|> "api" :> "current" :> Books.API :<|> "api" :> "current" :> Books.API
:<|> "api" :> "1" :> Catalogue.VersionedAPI 1 :<|> "api" :> "1" :> Catalogue.VersionedAPI 1
@ -38,11 +28,3 @@ handler = Users.handler
:<|> Catalogue.handler :<|> Catalogue.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 ClassyPrelude
import Control.Lens import Control.Lens
import Control.Monad.Catch (MonadThrow, throwM) import Control.Monad.Catch (throwM)
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import Crypto.Hash (digestFromByteString) import Crypto.Hash (digestFromByteString)
import Data.Aeson import Data.Aeson
@ -63,8 +63,6 @@ instance FromJSON PostBook
type API = Auth '[SA.BasicAuth, SA.JWT] SafeUser :> BaseAPI type API = Auth '[SA.BasicAuth, SA.JWT] SafeUser :> BaseAPI
instance Docs.ToCapture (Capture "book_id" BookID) where
toCapture _ = Docs.DocCapture "book_id" "The book id"
type BaseAPI = "books" :> Get '[JSON] [JsonBook] type BaseAPI = "books" :> Get '[JSON] [JsonBook]
:<|> "books" :> ReqBody '[JSON] PostBook :> Post '[JSON] JsonBook :<|> "books" :> ReqBody '[JSON] PostBook :> Post '[JSON] JsonBook

View File

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

View File

@ -16,7 +16,7 @@ module API.Channels (API, handler, JsonChannel(..)) where
import ClassyPrelude import ClassyPrelude
import Control.Lens import Control.Lens
import Control.Monad.Catch (MonadThrow, throwM) import Control.Monad.Catch (throwM)
import Control.Monad.Logger import Control.Monad.Logger
import Data.Aeson import Data.Aeson
import Data.Generics.Product import Data.Generics.Product
@ -49,9 +49,6 @@ instance FromJSON UpdateChannel
type API = Auth '[SA.BasicAuth, SA.Cookie, SA.JWT] SafeUser :> BaseAPI type API = Auth '[SA.BasicAuth, SA.Cookie, SA.JWT] SafeUser :> BaseAPI
instance Docs.ToCapture (Capture "channel_id" ChannelID) where
toCapture _ = Docs.DocCapture "channel_id" "The channel id"
type BaseAPI = "channels" :> ReqBody '[JSON] JsonChannel :> Post '[JSON] UpdateChannel type BaseAPI = "channels" :> ReqBody '[JSON] JsonChannel :> Post '[JSON] UpdateChannel
:<|> "channels" :> Capture "channel_id" ChannelID :> ReqBody '[JSON] UpdateChannel :> Put '[JSON] UpdateChannel :<|> "channels" :> Capture "channel_id" ChannelID :> ReqBody '[JSON] UpdateChannel :> Put '[JSON] UpdateChannel
:<|> "channels" :> Get '[JSON] [JsonChannel] :<|> "channels" :> Get '[JSON] [JsonChannel]

View File

@ -1,30 +1,30 @@
{-# Language DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# Language TypeFamilies #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# Language OverloadedStrings #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# Language NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}
{-# Language TypeOperators #-} {-# LANGUAGE TypeApplications #-}
{-# Language DuplicateRecordFields #-} {-# LANGUAGE TypeFamilies #-}
{-# Language TypeApplications #-} {-# LANGUAGE TypeOperators #-}
module API.Users where module API.Users where
import ClassyPrelude import ClassyPrelude
import Control.Monad.Catch (throwM, MonadThrow) import Control.Monad.Catch (throwM)
import Data.Aeson import Data.Aeson
import Database (runDB) import Database (runDB)
import Database.Schema import Database.Schema
import Database.User import Database.User
import Servant import Servant
import Servant.Auth as SA import Servant.Auth as SA
import Servant.Auth.Server as SAS import Servant.Auth.Server as SAS
import qualified Servant.Docs as Docs import qualified Servant.Docs as Docs
import Server.Auth import Server.Auth
import Types import Types
import Web.FormUrlEncoded import Web.FormUrlEncoded
data RegisterForm = RegisterForm { username :: Username data RegisterForm = RegisterForm { username :: Username
, email :: Email , email :: Email
, password :: PlainPassword , password :: PlainPassword
, passwordAgain :: PlainPassword } , passwordAgain :: PlainPassword }
deriving (Generic, Show) deriving (Generic, Show)
@ -56,7 +56,7 @@ handler = loginHandler :<|> registerHandler
loginHandler :: AuthResult SafeUser -> AppM LoginStatus loginHandler :: AuthResult SafeUser -> AppM LoginStatus
loginHandler (Authenticated u) = return (LoginStatus (Just u)) loginHandler (Authenticated u) = return (LoginStatus (Just u))
loginHandler _ = return (LoginStatus Nothing) loginHandler _ = return (LoginStatus Nothing)
registerHandler :: RegisterForm -> AppM RegisterStatus registerHandler :: RegisterForm -> AppM RegisterStatus
registerHandler RegisterForm{..} = registerHandler RegisterForm{..} =

View File

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

View File

@ -3,6 +3,9 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
module Database.Schema where module Database.Schema where
import ClassyPrelude import ClassyPrelude
@ -11,6 +14,7 @@ import Database.Selda
import Database.Selda.Backend import Database.Selda.Backend
import Database.Selda.Generic import Database.Selda.Generic
import qualified Servant.Docs as Docs import qualified Servant.Docs as Docs
import Servant (Capture)
import Web.HttpApiData import Web.HttpApiData
-- | User type -- | 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) 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) 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) newtype TagID = TagID {unTagID :: Int} deriving (Show)
instance SqlType UserID where instance SqlType UserID where

View File

@ -1,25 +1,27 @@
{-# Language OverloadedStrings #-} {-# LANGUAGE DataKinds #-}
{-# Language RecordWildCards #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# Language DuplicateRecordFields #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# Language TypeApplications #-} {-# LANGUAGE OverloadedStrings #-}
{-# Language DataKinds #-} {-# LANGUAGE RecordWildCards #-}
{-# Language NoImplicitPrelude #-} {-# LANGUAGE TypeApplications #-}
module Main where module Main where
import Server (server) import ClassyPrelude
import Network.Wai.Handler.Warp (run) import Configuration
import Types import Control.Lens (view, to)
import Configuration import Data.Generics.Product
import Dhall (input, auto) import Data.Pool (createPool)
import ClassyPrelude import Database.Selda.PostgreSQL (PGConnectInfo (..), pgOpen,
import Control.Lens (view) seldaClose)
import Data.Generics.Product import Dhall (auto, input)
import Data.Pool (createPool) import Network.Wai.Handler.Warp (run)
import Database.Selda.PostgreSQL (PGConnectInfo(..), pgOpen, seldaClose) import Servant.Auth.Server (generateKey)
import Servant.Auth.Server (generateKey) import Server (server)
import Types
import System.Environment (getEnvironment)
defaultMain :: App -> IO () 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 -> (App -> IO ()) -> IO ()
withApp config f = do withApp config f = do
@ -35,5 +37,6 @@ withApp config f = do
main :: IO () main :: IO ()
main = do 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 withApp c defaultMain

View File

@ -1,19 +1,15 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
module Server where module Server where
import qualified API as API import qualified API
import ClassyPrelude hiding (Handler) import ClassyPrelude hiding (Handler)
import Control.Lens import Control.Lens
import Control.Monad.Except import Control.Monad.Except
@ -24,18 +20,16 @@ import Servant.Auth.Docs ()
import Servant.Auth.Server as SAS import Servant.Auth.Server as SAS
import qualified Servant.Docs as Docs import qualified Servant.Docs as Docs
import Servant.HTML.Lucid (HTML) import Servant.HTML.Lucid (HTML)
import Server.Auth (SafeUser) import Server.Auth (SafeUser, authCheck)
import Server.Auth (authCheck)
import Types import Types
type API = API.API type API = API.API
:<|> "help" :> Get '[PlainText, HTML] String :<|> "api" :> "help" :> Get '[PlainText, HTML] String
:<|> "static" :> Raw
type Ctx = '[BasicAuthData -> IO (AuthResult SafeUser), CookieSettings, JWTSettings] type Ctx = '[BasicAuthData -> IO (AuthResult SafeUser), CookieSettings, JWTSettings]
server :: App -> Application server :: App -> Application
server app = serveWithContext api cfg (hoistServerWithContext (Proxy @ API.API) (Proxy @ Ctx) server' API.handler :<|> serveDocs :<|> serveDirectoryFileServer "static") server app = serveWithContext api cfg (hoistServerWithContext (Proxy @ API.API) (Proxy @ Ctx) server' API.handler :<|> serveDocs)
where where
apiDocs :: Docs.API apiDocs :: Docs.API
apiDocs = Docs.docs (Proxy @API.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} cookieSettings = SAS.defaultCookieSettings{cookieIsSecure=SAS.NotSecure}
cfg = jwtCfg :. cookieSettings :. authCfg :. EmptyContext cfg = jwtCfg :. cookieSettings :. authCfg :. EmptyContext
server' :: AppM a -> Servant.Handler a 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 API
api = Proxy api = Proxy

View File

@ -1,15 +1,16 @@
{-# Language TypeApplications #-} {-# LANGUAGE TypeApplications #-}
module Main where module Main where
import API.Books import API.Books
import qualified Data.Aeson as A import qualified Data.Aeson as A
import Data.Char (isPrint) import Data.Char (isPrint)
import Data.GenValidity.Text () import Data.GenValidity.Text ()
import qualified Data.Text as T import qualified Data.Text as T
import Database.Schema import Database.Schema
import Prelude import Prelude
import Test.Hspec import Test.Hspec
import Test.Validity import Test.Validity
import Test.Validity.Aeson
instance GenUnchecked PlainPassword instance GenUnchecked PlainPassword
instance GenValid PlainPassword instance GenValid PlainPassword
@ -51,15 +52,15 @@ instance Validity PostBook
spec :: Spec spec :: Spec
spec = do spec = do
describe "JSON encoding" $ do describe "JSON encoding" $ do
it "Works for PlainPassword" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @PlainPassword) A.decode jsonSpecOnValid @PlainPassword
it "Works for Email" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Email) A.decode jsonSpecOnValid @Email
it "Username" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Username) A.decode jsonSpecOnValid @Username
it "Works for BookID" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @BookID) A.decode jsonSpecOnValid @BookID
it "Works for ChannelID" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @ChannelID) A.decode jsonSpecOnValid @ChannelID
it "Works for Role" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Role) A.decode jsonSpecOnValid @Role
it "Works for Visibility" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @Visibility) A.decode jsonSpecOnValid @Visibility
it "Works for JsonBook" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @JsonBook) A.decode jsonSpecOnValid @JsonBook
it "Works for PostBook" $ inverseFunctionsIfSecondSucceedsOnValid (A.encode @PostBook) A.decode jsonSpecOnValid @PostBook
main :: IO () main :: IO ()
main = hspec spec main = hspec spec

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" , password = "password"
, host = "hostname" , host = "hostname"
, database = "ebook" , database = "ebook"
, migrations = "./migrations"
} }
store = { path = "/tmp/store" }
} }

View File

@ -20,9 +20,13 @@ executable frontend
-- other-modules: -- other-modules:
-- other-extensions: -- other-extensions:
build-depends: base >=4.11 && <4.12 build-depends: base >=4.11 && <4.12
, miso
, jsaddle-warp
, mtl
, common , common
, generic-lens
, jsaddle-warp
, lens
, miso
, mtl
, servant
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010
ghc-options: -Wall

View File

@ -1,41 +1,93 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
module Main where module Main where
import Control.Lens (over, set)
import Control.Monad.Trans (liftIO) import Control.Monad.Trans (liftIO)
import Data.Generics.Product
import GHC.Generics (Generic)
import Language.Javascript.JSaddle.Warp import Language.Javascript.JSaddle.Warp
import Miso import Miso hiding (set)
import Miso.String 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 data Action = Add
| Subtract | Subtract
| SayHello | SayHello
| HandleURI URI
| ChangeURI URI
| NoOp | NoOp
newtype Model = Model Int deriving (Eq, Num, ToMisoString) data Model = Model { counter :: Int
, uri :: URI }
deriving (Eq, Generic)
updateModel :: Action -> Model -> Effect Action Model updateModel :: Model -> Action -> Effect Action Model
updateModel Add m = noEff (m + 1) updateModel m = \case
updateModel Subtract m = noEff (m - 1) Add -> noEff (over (field @"counter") (+1) m)
updateModel SayHello m = m <# (liftIO (putStrLn "Hello world") >> pure NoOp) Subtract -> noEff (over (field @"counter") (\x -> x - 1) m)
updateModel NoOp m = noEff 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 Action
viewModel x = viewModel model = view
div_ [] [ button_ [ onClick Add ] [ text "+" ] where
, text (ms x) view = either (const the404) id $ runRoute @API Proxy handlers uri model
, button_ [ onClick Subtract ] [ text "-" ] 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 :: IO ()
main = run 8081 $ startApp App{..} main = run 8081 $ do
model <- mkModel
startApp App{..}
where where
model = Model 0 mkModel = Model <$> pure 0 <*> getCurrentURI
initialAction = SayHello initialAction = SayHello
update = updateModel update = flip updateModel
view = viewModel view = viewModel
subs = [] subs = [ uriSub HandleURI ]
events = defaultEvents events = defaultEvents
mountPoint = Nothing mountPoint = Nothing

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}
''