From cbf619c3708d7184d18437072b364fd4f2f6e3c3 Mon Sep 17 00:00:00 2001 From: Saku Laesvuori Date: Fri, 20 Jan 2023 09:20:06 +0200 Subject: [PATCH] Initial commit --- Client.hs | 30 + Client/ApiRequests.hs | 56 ++ Client/Auth.hs | 65 ++ Client/FormFields.hs | 46 ++ Client/Handlers.hs | 34 ++ Client/Handlers/Applications.hs | 117 ++++ Client/Handlers/Apply.hs | 89 +++ Client/Handlers/Profile.hs | 134 +++++ Client/Handlers/VerifyEmail.hs | 50 ++ Client/Types.hs | 330 ++++++++++ PrintSchema.hs | 6 + README.md | 26 + Server.hs | 157 +++++ Server/API.hs | 333 +++++++++++ Server/DB.hs | 95 +++ Server/DB/Queries.hs | 115 ++++ Server/Email.hs | 17 + Server/Types.hs | 245 ++++++++ Server/Utils.hs | 20 + Setup.hs | 4 + datarekisteri.cabal | 14 + db/migrations/20221208114323_init-db.sql | 53 ++ db/schema.sql | 283 +++++++++ guix.scm | 728 +++++++++++++++++++++++ schema.gql | 76 +++ 25 files changed, 3123 insertions(+) create mode 100644 Client.hs create mode 100644 Client/ApiRequests.hs create mode 100644 Client/Auth.hs create mode 100644 Client/FormFields.hs create mode 100644 Client/Handlers.hs create mode 100644 Client/Handlers/Applications.hs create mode 100644 Client/Handlers/Apply.hs create mode 100644 Client/Handlers/Profile.hs create mode 100644 Client/Handlers/VerifyEmail.hs create mode 100644 Client/Types.hs create mode 100644 PrintSchema.hs create mode 100644 README.md create mode 100644 Server.hs create mode 100644 Server/API.hs create mode 100644 Server/DB.hs create mode 100644 Server/DB/Queries.hs create mode 100644 Server/Email.hs create mode 100644 Server/Types.hs create mode 100644 Server/Utils.hs create mode 100644 Setup.hs create mode 100644 datarekisteri.cabal create mode 100644 db/migrations/20221208114323_init-db.sql create mode 100644 db/schema.sql create mode 100644 guix.scm create mode 100644 schema.gql diff --git a/Client.hs b/Client.hs new file mode 100644 index 0000000..b5b3423 --- /dev/null +++ b/Client.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE ViewPatterns #-} + +{-# LANGUAGE NoImplicitPrelude #-} + +import Relude hiding (get) + +import Yesod +import Yesod.Auth +import Client.Types +import Client.Handlers +import Client.Auth () +import Yesod.Static (static, Static) +import System.Directory (createDirectoryIfMissing) + +mkYesodDispatch "DataIdClient" resourcesDataIdClient + +main :: IO () +main = getStaticDir "/tmp/data-id" >>= warp 3000 . DataIdClient + +getStaticDir :: FilePath -> IO Static +getStaticDir dir = createDirectoryIfMissing True dir >> static dir diff --git a/Client/ApiRequests.hs b/Client/ApiRequests.hs new file mode 100644 index 0000000..b17df29 --- /dev/null +++ b/Client/ApiRequests.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-# LANGUAGE NoImplicitPrelude #-} + +module Client.ApiRequests where + +import Relude + +import Data.Morpheus.Client +import Yesod hiding (Header) +import Yesod.Auth + +type ClientTypeConstraint (a :: Type) = (RequestType a, ToJSON (RequestArgs a), FromJSON a) +-- From Data.Morpheus.Client.Fetch.RequestType +type Header = (Text, Text) +-- From Data.Morpheus.Client.Fetch.GQLClient +-- No clue why these are not exported + +type RequestConstraint a site = (ClientTypeConstraint a, ApiRequest site) + +type Authorization = Text + +class YesodAuth site => ApiRequest site where + getApiUrl :: HandlerFor site Text + authIdToAuthorization :: site -> AuthId site -> Authorization + +apiRequest' :: RequestConstraint a site => [Header] -> Bool -> Args a -> HandlerFor site a +apiRequest' extraHeaders authRequired args = do + yesod <- liftHandler getYesod + auth <- fmap (authIdToAuthorization yesod) <$> if authRequired then Just <$> requireAuthId else maybeAuthId + apiRequestAuth extraHeaders (("Bearer " <>) <$> auth) args >>= handleErrors + +apiRequestAuth :: RequestConstraint a site => + [Header] -> Maybe Authorization -> Args a -> HandlerFor site (GQLClientResult a) +apiRequestAuth extraHeaders auth args = do + apiUrl <- getApiUrl + stream <- liftIO $ request (fromString (toString apiUrl) `withHeaders` headers) args + single stream + where headers = maybe [] (\x -> [("Authorization", x)]) auth <> extraHeaders + +apiRequest :: RequestConstraint a site => Bool -> Args a -> HandlerFor site a +apiRequest authRequired = apiRequest' [] authRequired + +handleErrors :: RequestConstraint a site => GQLClientResult a -> HandlerFor site a +handleErrors (Right a) = pure a +handleErrors (Left (FetchErrorParseFailure s)) = error $ "GQL parse failure: " <> toText s +handleErrors (Left (FetchErrorProducedErrors errs Nothing)) = error $ "GQL errors: " <> show errs +handleErrors (Left (FetchErrorProducedErrors _ (Just a))) = pure a -- TODO log the errors? +handleErrors (Left (FetchErrorNoResult)) = error $ "GQL no results" diff --git a/Client/Auth.hs b/Client/Auth.hs new file mode 100644 index 0000000..470e509 --- /dev/null +++ b/Client/Auth.hs @@ -0,0 +1,65 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +{-# LANGUAGE NoImplicitPrelude #-} + +module Client.Auth where + +import Relude + +import Server.Types +import Data.Morpheus.Client +import Yesod +import Yesod.Auth +import qualified Data.ByteString.Base64 as B64 (encode) +import qualified Yesod.Auth.Message as Msg + +pluginName = "externalBasic" + +loginR = PluginR pluginName ["login"] + +type Authorization = Text +type CredsIdent = Text + +postLoginR :: YesodAuth master => + (Authorization -> HandlerFor master (Maybe CredsIdent)) -> AuthHandler master TypedContent +postLoginR authReq = do + res <- runInputPostResult $ (\x y -> x <> ":" <> y) + <$> ireq textField "email" <*> ireq textField "password" + case res of + FormSuccess auth -> do + maybeAuth <- liftHandler $ authReq $ ("Basic " <> ) $ decodeUtf8 $ B64.encode $ encodeUtf8 auth + case maybeAuth of + Nothing -> loginErrorMessageI LoginR Msg.NoIdentifierProvided -- invalid creds + Just txt -> do + setCredsRedirect Creds {credsPlugin = pluginName, credsIdent = txt, credsExtra = []} + _ -> loginErrorMessageI LoginR Msg.NoIdentifierProvided + +authExternalBasic :: forall site. YesodAuth site => + (Authorization -> HandlerFor site (Maybe CredsIdent)) -> AuthPlugin site +authExternalBasic authReq = AuthPlugin pluginName dispatch loginForm + where dispatch :: YesodAuth site => Text -> [Text] -> AuthHandler site TypedContent + dispatch "POST" ["login"] = postLoginR authReq >>= sendResponse + dispatch _ _ = notFound + +loginForm :: YesodAuth site => (Route Auth -> Route site) -> WidgetFor site () +loginForm toParent = do + emailId <- newIdent + passwordId <- newIdent + [whamlet| +

Kirjaudu +
+