176 lines
5.5 KiB
Haskell
176 lines
5.5 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
|
|
module API where
|
|
|
|
import Servant
|
|
import Database.Persist.Sqlite
|
|
import Control.Monad.Logger
|
|
|
|
import Crypto.PasswordStore
|
|
|
|
import qualified Data.Text as T
|
|
import Data.Text (Text(..))
|
|
import Data.Text.Encoding
|
|
|
|
import Control.Monad.IO.Class (liftIO)
|
|
|
|
import Network.Wai.Handler.Warp as Warp
|
|
|
|
import Model
|
|
import Config
|
|
|
|
type PasteCapture a =
|
|
Capture "username" Text :> Capture "pasteName" Text :> a
|
|
|
|
type GetPasteAPI =
|
|
PasteCapture (Get '[JSON] SanitizedPaste)
|
|
:<|> "raw" :> PasteCapture (Get '[PlainText] Text)
|
|
|
|
type RequiresAuth a =
|
|
BasicAuth "pastecan" (Entity User) :> a
|
|
|
|
type UpdatePasteAPI =
|
|
RequiresAuth (
|
|
PasteCapture (Delete '[PlainText] NoContent)
|
|
:<|> PasteCapture (ReqBody '[JSON] SanitizedPaste :> Put '[PlainText] NoContent))
|
|
|
|
type AccountsAPI =
|
|
"account" :> (
|
|
ReqBody '[JSON] User :> Post '[JSON] Bool
|
|
:<|> RequiresAuth (Delete '[JSON] NoContent))
|
|
|
|
type API =
|
|
UpdatePasteAPI :<|> GetPasteAPI :<|> AccountsAPI
|
|
|
|
-- If m is not Just x, will throw a Servant error. Otherwise, will return x.
|
|
justOrError :: ServantErr -> App (Maybe a) -> App a
|
|
justOrError e m =
|
|
m >>=
|
|
(\w -> case w of
|
|
Just x -> return x
|
|
Nothing -> throwError e)
|
|
|
|
-- justOrError specialised for Error 404
|
|
justOr404 :: App (Maybe a) -> App a
|
|
justOr404 = justOrError err404
|
|
|
|
-- Throws a Servant error if b is True.
|
|
errIf :: ServantErr -> Bool -> App ()
|
|
errIf e b = if b then throwError e else return ()
|
|
|
|
-- errIf specialised for Error 403
|
|
err403If :: Bool -> App ()
|
|
err403If = errIf err403
|
|
|
|
-- Checks BasicAuth credentials
|
|
basicAuthCheck :: HandlerConfig -> BasicAuthCheck (Entity User)
|
|
basicAuthCheck HandlerConfig{db = db} =
|
|
BasicAuthCheck check
|
|
where
|
|
check :: BasicAuthData -> IO (BasicAuthResult (Entity User))
|
|
check (BasicAuthData username suppliedPassword) = do
|
|
-- Annoyingly, we can't use the App monad here.
|
|
userEntity <- flip runSqlPool db $ getBy $ Username (decodeUtf8 username)
|
|
case userEntity of
|
|
Just (Entity uid userData) -> do
|
|
let correctPassword = encodeUtf8 $ userPassword userData
|
|
|
|
if verifyPassword suppliedPassword correctPassword then
|
|
return $ Authorized $ Entity uid userData
|
|
else
|
|
return BadPassword
|
|
Nothing -> return NoSuchUser
|
|
|
|
-- A Context with a BasicAuthCheck in it
|
|
basicAuthContext :: HandlerConfig -> Context (BasicAuthCheck (Entity User) ': '[])
|
|
basicAuthContext cfg = basicAuthCheck cfg :. EmptyContext
|
|
|
|
getUserByName :: Text -> App (Maybe (Entity User))
|
|
getUserByName n = runDB $ getBy $ Username n
|
|
|
|
getPasteData :: Text -> Text -> App SanitizedPaste
|
|
getPasteData username name = do
|
|
(Entity uid _) <- justOr404 $ getUserByName username
|
|
(Entity _ paste) <- justOr404 $ runDB $ getBy (PasteNamePoster name uid)
|
|
return $ sanitizePaste paste
|
|
|
|
getPasteRaw :: Text -> Text -> App Text
|
|
getPasteRaw u n = content <$> getPasteData u n
|
|
|
|
getPasteServer :: ServerT GetPasteAPI App
|
|
getPasteServer = getPasteData :<|> getPasteRaw
|
|
|
|
requireUserHasUsername :: User -> Text -> App ()
|
|
requireUserHasUsername user username = err403If $ (userUsername user) /= username
|
|
|
|
deletePaste :: (Entity User) -> Text -> Text -> App NoContent
|
|
deletePaste (Entity uid user) username pasteName = do
|
|
requireUserHasUsername user username
|
|
|
|
runDB $ deleteBy $ PasteNamePoster pasteName uid
|
|
|
|
return NoContent
|
|
|
|
updatePaste :: (Entity User) -> Text -> Text -> SanitizedPaste -> App NoContent
|
|
updatePaste (Entity uid user) username pasteName newPaste = do
|
|
requireUserHasUsername user username
|
|
|
|
let paste = attachPoster uid newPaste
|
|
|
|
e <- runDB $ getBy $ PasteNamePoster pasteName uid
|
|
|
|
case e of
|
|
Just (Entity pasteId _) -> -- Paste already exists in database
|
|
runDB $ replace pasteId paste
|
|
Nothing -> do -- Paste is new
|
|
runDB $ insert paste
|
|
return ()
|
|
|
|
return NoContent
|
|
|
|
updatePasteServer :: ServerT UpdatePasteAPI App
|
|
updatePasteServer user = deletePaste user :<|> updatePaste user
|
|
|
|
createAccount :: User -> App Bool
|
|
createAccount u = do
|
|
-- TODO: find more typesafe way to do this
|
|
hashedPass <- liftIO $ decodeUtf8 <$> makePassword (encodeUtf8 $ userPassword u) 17
|
|
let account = u {userPassword = hashedPass}
|
|
|
|
maybeUser <- getUserByName (userUsername account)
|
|
|
|
case maybeUser of
|
|
Just (Entity _ _) ->
|
|
return False -- User by that name already exists - disallow creation
|
|
Nothing -> do
|
|
runDB $ insert account
|
|
return True
|
|
|
|
deleteAccount :: Entity User -> App NoContent
|
|
deleteAccount (Entity uid _) = do
|
|
runDB $ do
|
|
deleteWhere [PastePoster ==. uid]
|
|
delete uid
|
|
|
|
return NoContent
|
|
|
|
accountServer :: ServerT AccountsAPI App
|
|
accountServer = createAccount :<|> deleteAccount
|
|
|
|
server :: HandlerConfig -> Server API
|
|
server cfg = enter (appToServer cfg) (updatePasteServer :<|> getPasteServer :<|> accountServer)
|
|
|
|
proxy :: Proxy API
|
|
proxy = Proxy
|
|
|
|
mkApp :: FilePath -> IO Application
|
|
mkApp path = do
|
|
pool <- runStderrLoggingT $ createSqlitePool (T.pack path) 5
|
|
runSqlPool doMigrations pool
|
|
let cfg = HandlerConfig pool
|
|
return $ serveWithContext proxy (basicAuthContext cfg) (server cfg)
|
|
|
|
runApp :: Int -> FilePath -> IO ()
|
|
runApp port dbFile =
|
|
Warp.run port =<< mkApp dbFile |