Channels API
This commit is contained in:
		@@ -19,9 +19,11 @@ executable ebook-manager
 | 
				
			|||||||
  main-is:             Main.hs
 | 
					  main-is:             Main.hs
 | 
				
			||||||
  other-modules:       Devel.Main
 | 
					  other-modules:       Devel.Main
 | 
				
			||||||
                     , API
 | 
					                     , API
 | 
				
			||||||
 | 
					                     , API.Channels
 | 
				
			||||||
                     , API.Users
 | 
					                     , API.Users
 | 
				
			||||||
                     , Configuration
 | 
					                     , Configuration
 | 
				
			||||||
                     , Database
 | 
					                     , Database
 | 
				
			||||||
 | 
					                     , Database.Channel
 | 
				
			||||||
                     , Database.Schema
 | 
					                     , Database.Schema
 | 
				
			||||||
                     , Database.User
 | 
					                     , Database.User
 | 
				
			||||||
                     , Server
 | 
					                     , Server
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,14 +19,18 @@ import Types
 | 
				
			|||||||
import View
 | 
					import View
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import qualified API.Users as Users
 | 
					import qualified API.Users as Users
 | 
				
			||||||
 | 
					import qualified API.Channels as Channels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
data Index = Index
 | 
					data Index = Index
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type API = Get '[HTML] (AppView Index)
 | 
					type API = Get '[HTML] (AppView Index)
 | 
				
			||||||
      :<|> Users.API
 | 
					      :<|> Users.API
 | 
				
			||||||
 | 
					      :<|> Channels.API
 | 
				
			||||||
 | 
					
 | 
				
			||||||
handler :: ServerT API AppM
 | 
					handler :: ServerT API AppM
 | 
				
			||||||
handler = indexHandler :<|> Users.handler
 | 
					handler = indexHandler
 | 
				
			||||||
 | 
					    :<|> Users.handler
 | 
				
			||||||
 | 
					    :<|> Channels.handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
instance ToHtml Index where
 | 
					instance ToHtml Index where
 | 
				
			||||||
  toHtml _ = do
 | 
					  toHtml _ = do
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										51
									
								
								src/API/Channels.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/API/Channels.hs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					{-# 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 #-}
 | 
				
			||||||
 | 
					module API.Channels (API, handler) 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data JsonChannel = JsonChannel { channel :: Text } deriving (Show, Generic)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					instance ToJSON JsonChannel
 | 
				
			||||||
 | 
					instance FromJSON JsonChannel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type API = Auth '[SA.BasicAuth, SA.Cookie, SA.JWT] SafeUser :> BaseAPI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BaseAPI = "channels" :> ReqBody '[JSON] JsonChannel :> Put '[JSON] JsonChannel
 | 
				
			||||||
 | 
					          :<|> "channels" :> Get '[JSON] [JsonChannel]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					handler :: ServerT API AppM
 | 
				
			||||||
 | 
					handler user = newChannelHandler user :<|> listChannelsHandler user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					listChannelsHandler :: AuthResult SafeUser -> AppM [JsonChannel]
 | 
				
			||||||
 | 
					listChannelsHandler = requireLoggedIn $ \user ->
 | 
				
			||||||
 | 
					  -- I could use the super thing from generic-lens, but then I would need to
 | 
				
			||||||
 | 
					  -- use the 'channel' accessor somehow or export it
 | 
				
			||||||
 | 
					  fmap (\Channel{..} -> JsonChannel{..}) <$> runDB (userChannels (view (field @"username") user))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					newChannelHandler :: AuthResult SafeUser -> JsonChannel -> AppM JsonChannel
 | 
				
			||||||
 | 
					newChannelHandler auth ch@JsonChannel{..} = flip requireLoggedIn auth $ \user -> do
 | 
				
			||||||
 | 
					  $logInfo $ "Creating channel for user " <> pack (show user)
 | 
				
			||||||
 | 
					  runDB (insertChannel (view (field @"username") user) channel)
 | 
				
			||||||
 | 
					  return ch
 | 
				
			||||||
@@ -41,7 +41,7 @@ instance FromForm RegisterForm
 | 
				
			|||||||
instance ToForm RegisterForm
 | 
					instance ToForm RegisterForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type API = Auth '[SA.BasicAuth] SafeUser :> "login" :> Get '[JSON] LoginStatus
 | 
					type API = Auth '[SA.BasicAuth, SA.JWT] SafeUser :> "login" :> Get '[JSON] LoginStatus
 | 
				
			||||||
      :<|> "register" :> ReqBody '[JSON, FormUrlEncoded] RegisterForm :> Post '[JSON] RegisterStatus
 | 
					      :<|> "register" :> ReqBody '[JSON, FormUrlEncoded] RegisterForm :> Post '[JSON] RegisterStatus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
handler :: ServerT API AppM
 | 
					handler :: ServerT API AppM
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ module Database
 | 
				
			|||||||
  , select
 | 
					  , select
 | 
				
			||||||
  , gen
 | 
					  , gen
 | 
				
			||||||
  , fromRel
 | 
					  , fromRel
 | 
				
			||||||
 | 
					  , fromRels
 | 
				
			||||||
  , toRel
 | 
					  , toRel
 | 
				
			||||||
  , SeldaT )
 | 
					  , SeldaT )
 | 
				
			||||||
  where
 | 
					  where
 | 
				
			||||||
@@ -18,7 +19,7 @@ import Control.Lens (view)
 | 
				
			|||||||
import Data.Pool (Pool, withResource)
 | 
					import Data.Pool (Pool, withResource)
 | 
				
			||||||
import Database.Selda.Backend (SeldaConnection, runSeldaT, SeldaT)
 | 
					import Database.Selda.Backend (SeldaConnection, runSeldaT, SeldaT)
 | 
				
			||||||
import Database.Selda (query, select)
 | 
					import Database.Selda (query, select)
 | 
				
			||||||
import Database.Selda.Generic (gen, fromRel, toRel)
 | 
					import Database.Selda.Generic (gen, fromRel, fromRels, toRel)
 | 
				
			||||||
import ClassyPrelude
 | 
					import ClassyPrelude
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DBLike r m = (MonadIO m, MonadReader r m, MonadBaseControl IO m, MonadMask m, HasField' "database" r (Pool SeldaConnection))
 | 
					type DBLike r m = (MonadIO m, MonadReader r m, MonadBaseControl IO m, MonadMask m, HasField' "database" r (Pool SeldaConnection))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										34
									
								
								src/Database/Channel.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/Database/Channel.hs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					{-# Language TypeApplications #-}
 | 
				
			||||||
 | 
					{-# Language DataKinds #-}
 | 
				
			||||||
 | 
					module Database.Channel
 | 
				
			||||||
 | 
					  ( userChannels
 | 
				
			||||||
 | 
					  , insertChannel
 | 
				
			||||||
 | 
					  , Channel(..) )
 | 
				
			||||||
 | 
					  where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ClassyPrelude
 | 
				
			||||||
 | 
					import Database.Schema
 | 
				
			||||||
 | 
					import Database
 | 
				
			||||||
 | 
					import Database.Selda
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					userChannels :: (MonadMask m, MonadIO m) => Username -> SeldaT m [Channel]
 | 
				
			||||||
 | 
					userChannels username = fromRels <$> query q
 | 
				
			||||||
 | 
					  where
 | 
				
			||||||
 | 
					    q = do
 | 
				
			||||||
 | 
					      userId :*: _ :*: username' :*: _ <- select (gen users)
 | 
				
			||||||
 | 
					      channel@(_ :*: _ :*: owner) <- select (gen channels)
 | 
				
			||||||
 | 
					      restrict (owner .== userId)
 | 
				
			||||||
 | 
					      restrict (username' .== literal username)
 | 
				
			||||||
 | 
					      return channel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					insertChannel :: (MonadMask m, MonadIO m) => Username -> Text -> SeldaT m ()
 | 
				
			||||||
 | 
					insertChannel username channel = do
 | 
				
			||||||
 | 
					  mUserId <- listToMaybe <$> getUser
 | 
				
			||||||
 | 
					  void $ forM mUserId $ \userId ->
 | 
				
			||||||
 | 
					    insertUnless (gen channels) (doesNotExist userId) [ def :*: channel :*: userId ]
 | 
				
			||||||
 | 
					  where
 | 
				
			||||||
 | 
					    doesNotExist userId (_ :*: channel' :*: userId') = channel' .== literal channel .&& userId' .== literal userId
 | 
				
			||||||
 | 
					    getUser = query $ do
 | 
				
			||||||
 | 
					      userId :*: _ :*: user :*: _ <- select (gen users)
 | 
				
			||||||
 | 
					      restrict (user .== literal username)
 | 
				
			||||||
 | 
					      return userId
 | 
				
			||||||
@@ -112,8 +112,8 @@ tags = genTable "tags" [ (identifier :: Tag -> RowID) :- autoPrimaryGen
 | 
				
			|||||||
    i :*: _ = selectors (gen users)
 | 
					    i :*: _ = selectors (gen users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
channels :: GenTable Channel
 | 
					channels :: GenTable Channel
 | 
				
			||||||
channels = genTable "tags" [ (identifier :: Channel -> RowID) :- autoPrimaryGen
 | 
					channels = genTable "channels" [ (identifier :: Channel -> RowID) :- autoPrimaryGen
 | 
				
			||||||
                           , (owner :: Channel -> RowID) :- fkGen (gen users) i ]
 | 
					                               , (owner :: Channel -> RowID) :- fkGen (gen users) i ]
 | 
				
			||||||
  where
 | 
					  where
 | 
				
			||||||
    i :*: _ = selectors (gen users)
 | 
					    i :*: _ = selectors (gen users)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ import Types
 | 
				
			|||||||
import ClassyPrelude hiding (Handler)
 | 
					import ClassyPrelude hiding (Handler)
 | 
				
			||||||
import Control.Monad.Logger
 | 
					import Control.Monad.Logger
 | 
				
			||||||
import Control.Monad.Except
 | 
					import Control.Monad.Except
 | 
				
			||||||
import Servant.Auth.Server
 | 
					import Servant.Auth.Server as SAS
 | 
				
			||||||
import Control.Lens
 | 
					import Control.Lens
 | 
				
			||||||
import Data.Generics.Product
 | 
					import Data.Generics.Product
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,7 +32,8 @@ server app = serveWithContext api cfg (enter server' API.handler :<|> serveDirec
 | 
				
			|||||||
    myKey = view (field @"jwk") app
 | 
					    myKey = view (field @"jwk") app
 | 
				
			||||||
    jwtCfg = defaultJWTSettings myKey
 | 
					    jwtCfg = defaultJWTSettings myKey
 | 
				
			||||||
    authCfg = authCheck app
 | 
					    authCfg = authCheck app
 | 
				
			||||||
    cfg = jwtCfg :. defaultCookieSettings :. authCfg :. EmptyContext
 | 
					    cookieSettings = SAS.defaultCookieSettings{cookieIsSecure=SAS.NotSecure}
 | 
				
			||||||
 | 
					    cfg = jwtCfg :. cookieSettings :. authCfg :. EmptyContext
 | 
				
			||||||
    server' :: AppM :~> Servant.Handler
 | 
					    server' :: AppM :~> Servant.Handler
 | 
				
			||||||
    server' = NT (Handler . ExceptT . try . (`runReaderT` app) . (runFileLoggingT "logs/server.log"))
 | 
					    server' = NT (Handler . ExceptT . try . (`runReaderT` app) . (runFileLoggingT "logs/server.log"))
 | 
				
			||||||
    api :: Proxy API
 | 
					    api :: Proxy API
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,13 @@
 | 
				
			|||||||
{-# Language TypeOperators #-}
 | 
					{-# Language TypeOperators #-}
 | 
				
			||||||
{-# Language DuplicateRecordFields #-}
 | 
					{-# Language DuplicateRecordFields #-}
 | 
				
			||||||
{-# Language TypeApplications #-}
 | 
					{-# Language TypeApplications #-}
 | 
				
			||||||
module Server.Auth where
 | 
					{-# Language TemplateHaskell #-}
 | 
				
			||||||
 | 
					module Server.Auth
 | 
				
			||||||
 | 
					  ( SafeUser(..)
 | 
				
			||||||
 | 
					  , authCheck
 | 
				
			||||||
 | 
					  , AuthResult(..)
 | 
				
			||||||
 | 
					  , requireLoggedIn)
 | 
				
			||||||
 | 
					  where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ClassyPrelude
 | 
					import ClassyPrelude
 | 
				
			||||||
import Servant.Auth.Server as SAS
 | 
					import Servant.Auth.Server as SAS
 | 
				
			||||||
@@ -16,6 +22,8 @@ import Database
 | 
				
			|||||||
import Types
 | 
					import Types
 | 
				
			||||||
import Control.Lens (view)
 | 
					import Control.Lens (view)
 | 
				
			||||||
import Data.Generics.Product
 | 
					import Data.Generics.Product
 | 
				
			||||||
 | 
					import Servant (err401)
 | 
				
			||||||
 | 
					import Control.Monad.Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- generic-lens can convert similar types to this
 | 
					-- generic-lens can convert similar types to this
 | 
				
			||||||
-- I'm trying out servant-auth-server which uses a jwt style login. IIRC anyone
 | 
					-- I'm trying out servant-auth-server which uses a jwt style login. IIRC anyone
 | 
				
			||||||
@@ -44,3 +52,7 @@ authCheck app (BasicAuthData username password) = flip runReaderT app $
 | 
				
			|||||||
    username' = Username $ decodeUtf8 username
 | 
					    username' = Username $ decodeUtf8 username
 | 
				
			||||||
    password' = PlainPassword $ decodeUtf8 password
 | 
					    password' = PlainPassword $ decodeUtf8 password
 | 
				
			||||||
    authenticated = SAS.Authenticated . view (super @SafeUser)
 | 
					    authenticated = SAS.Authenticated . view (super @SafeUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					requireLoggedIn :: (MonadLogger m, MonadThrow m, Monad m) => (SafeUser -> m a) -> AuthResult SafeUser -> m a
 | 
				
			||||||
 | 
					requireLoggedIn f (Authenticated user) = f user
 | 
				
			||||||
 | 
					requireLoggedIn _ u = $logError (pack (show u)) >> throwM err401
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ module View
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import ClassyPrelude
 | 
					import ClassyPrelude
 | 
				
			||||||
import Server.Auth
 | 
					import Server.Auth
 | 
				
			||||||
import Servant.Auth.Server
 | 
					 | 
				
			||||||
import Lucid (HtmlT, ToHtml(..))
 | 
					import Lucid (HtmlT, ToHtml(..))
 | 
				
			||||||
import Lucid.Html5 as H
 | 
					import Lucid.Html5 as H
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user