git.iko.soy/ikoHSE/sc-lectures
on master help sign in
l5
commit9e6af09c8af28e32a7e382743348636754641cef authoriko <ilyakooo0@gmail.com> dateMon 1 Jun 2020, 10:10 (6 years ago) parentc738d30
17 files changed+810
l5/.hlint.yaml+64 l5/Setup.hs+2 l5/app/Main.hs+6 l5/l5.cabal+114 l5/package.yaml+69 l5/src/Handlers.hs+8 l5/src/Handlers/Posts.hs+34 l5/src/Handlers/Users/Registration.hs+101 l5/src/Orphans.hs+16 l5/src/Server.hs+92 l5/src/Types.hs+10 l5/src/Types/Hash.hs+31 l5/src/Types/Post.hs+20 l5/src/Types/Server.hs+115 l5/src/Types/Users.hs+60 l5/stack.yaml+66 l5/test/Spec.hs+2
l5/.hlint.yamlnew file+64
@@ -0,0 +1,64 @@
1+# HLint configuration file
2+# https://github.com/ndmitchell/hlint
3+##########################
4+
5+# This file contains a template configuration file, which is typically
6+# placed as .hlint.yaml in the root of your project
7+
8+
9+# Warnings currently triggered by your code
10+- ignore: {name: "Use newtype instead of data"}
11+
12+
13+# Specify additional command line arguments
14+#
15+# - arguments: [--color, --cpp-simple, -XQuasiQuotes]
16+
17+
18+# Control which extensions/flags/modules/functions can be used
19+#
20+# - extensions:
21+# - default: false # all extension are banned by default
22+# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
23+# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
24+#
25+# - flags:
26+# - {name: -w, within: []} # -w is allowed nowhere
27+#
28+# - modules:
29+# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
30+# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
31+#
32+# - functions:
33+# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
34+
35+
36+# Add custom hints for this project
37+#
38+# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
39+# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
40+
41+
42+# Turn on hints that are off by default
43+#
44+# Ban "module X(module X) where", to require a real export list
45+# - warn: {name: Use explicit module export list}
46+#
47+# Replace a $ b $ c with a . b $ c
48+# - group: {name: dollar, enabled: true}
49+#
50+# Generalise map to fmap, ++ to <>
51+# - group: {name: generalise, enabled: true}
52+
53+
54+# Ignore some builtin hints
55+# - ignore: {name: Use let}
56+# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
57+
58+
59+# Define some custom infix operators
60+# - fixity: infixr 3 ~^#^~
61+
62+
63+# To generate a suitable file for HLint do:
64+# $ hlint --default > .hlint.yaml
l5/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
l5/app/Main.hsnew file+6
@@ -0,0 +1,6 @@
1+module Main (main) where
2+
3+import Server
4+
5+main :: IO ()
6+main = runServer
l5/l5.cabalnew file+114
@@ -0,0 +1,114 @@
1+cabal-version: 1.12
2+
3+-- This file has been generated from package.yaml by hpack version 0.33.0.
4+--
5+-- see: https://github.com/sol/hpack
6+--
7+-- hash: be8617e31ced95a5ce5417438f31b676910f80384e47ef5aae1c359c8df9d857
8+
9+name: l5
10+version: 0.1.0.0
11+description: Please see the README on GitHub at <https://github.com/githubuser/l5#readme>
12+homepage: https://github.com/githubuser/l5#readme
13+bug-reports: https://github.com/githubuser/l5/issues
14+author: Author name here
15+maintainer: example@example.com
16+copyright: 2020 Author name here
17+license: BSD3
18+license-file: LICENSE
19+build-type: Simple
20+extra-source-files:
21+ README.md
22+ ChangeLog.md
23+
24+source-repository head
25+ type: git
26+ location: https://github.com/githubuser/l5
27+
28+library
29+ exposed-modules:
30+ Handlers
31+ Handlers.Posts
32+ Handlers.Users.Registration
33+ Orphans
34+ Server
35+ Types
36+ Types.Hash
37+ Types.Post
38+ Types.Server
39+ Types.Users
40+ other-modules:
41+ Paths_l5
42+ hs-source-dirs:
43+ src
44+ default-extensions: OverloadedStrings DeriveGeneric GeneralizedNewtypeDeriving FlexibleContexts
45+ ghc-options: -fwarn-unused-binds -fwarn-unused-imports -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -Werror=missing-home-modules -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists -Wunused-imports
46+ build-depends:
47+ aeson
48+ , base >=4.7 && <5
49+ , base64-bytestring
50+ , bytestring
51+ , containers
52+ , cookie
53+ , cryptonite
54+ , http-types
55+ , mtl
56+ , stm
57+ , text
58+ , time
59+ , wai
60+ , warp
61+ default-language: Haskell2010
62+
63+executable l5-exe
64+ main-is: Main.hs
65+ other-modules:
66+ Paths_l5
67+ hs-source-dirs:
68+ app
69+ default-extensions: OverloadedStrings DeriveGeneric GeneralizedNewtypeDeriving FlexibleContexts
70+ ghc-options: -fwarn-unused-binds -fwarn-unused-imports -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -Werror=missing-home-modules -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists -Wunused-imports -threaded -rtsopts -with-rtsopts=-N
71+ build-depends:
72+ aeson
73+ , base >=4.7 && <5
74+ , base64-bytestring
75+ , bytestring
76+ , containers
77+ , cookie
78+ , cryptonite
79+ , http-types
80+ , l5
81+ , mtl
82+ , stm
83+ , text
84+ , time
85+ , wai
86+ , warp
87+ default-language: Haskell2010
88+
89+test-suite l5-test
90+ type: exitcode-stdio-1.0
91+ main-is: Spec.hs
92+ other-modules:
93+ Paths_l5
94+ hs-source-dirs:
95+ test
96+ default-extensions: OverloadedStrings DeriveGeneric GeneralizedNewtypeDeriving FlexibleContexts
97+ ghc-options: -fwarn-unused-binds -fwarn-unused-imports -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -Werror=missing-home-modules -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists -Wunused-imports -threaded -rtsopts -with-rtsopts=-N
98+ build-depends:
99+ aeson
100+ , base >=4.7 && <5
101+ , base64-bytestring
102+ , bytestring
103+ , containers
104+ , cookie
105+ , cryptonite
106+ , http-types
107+ , l5
108+ , mtl
109+ , stm
110+ , text
111+ , time
112+ , wai
113+ , warp
114+ default-language: Haskell2010
l5/package.yamlnew file+69
@@ -0,0 +1,69 @@
1+name: l5
2+version: 0.1.0.0
3+github: "githubuser/l5"
4+license: BSD3
5+author: "Author name here"
6+maintainer: "example@example.com"
7+copyright: "2020 Author name here"
8+
9+extra-source-files:
10+- README.md
11+- ChangeLog.md
12+
13+# Metadata used when publishing your package
14+# synopsis: Short description of your package
15+# category: Web
16+
17+# To avoid duplicated efforts in documentation and dealing with the
18+# complications of embedding Haddock markup inside cabal files, it is
19+# common to point users to the README.md file.
20+description: Please see the README on GitHub at <https://github.com/githubuser/l5#readme>
21+
22+dependencies:
23+- base >= 4.7 && < 5
24+- wai
25+- warp
26+- http-types
27+- stm
28+- containers
29+- aeson
30+- text
31+- mtl
32+- bytestring
33+- cookie
34+- cryptonite
35+- base64-bytestring
36+- time
37+
38+default-extensions:
39+- OverloadedStrings
40+- DeriveGeneric
41+- GeneralizedNewtypeDeriving
42+- FlexibleContexts
43+
44+library:
45+ source-dirs: src
46+
47+executables:
48+ l5-exe:
49+ main: Main.hs
50+ source-dirs: app
51+ ghc-options:
52+ - -threaded
53+ - -rtsopts
54+ - -with-rtsopts=-N
55+ dependencies:
56+ - l5
57+
58+tests:
59+ l5-test:
60+ main: Spec.hs
61+ source-dirs: test
62+ ghc-options:
63+ - -threaded
64+ - -rtsopts
65+ - -with-rtsopts=-N
66+ dependencies:
67+ - l5
68+
69+ghc-options: "-fwarn-unused-binds -fwarn-unused-imports -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -Werror=missing-home-modules -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists -Wunused-imports"
l5/src/Handlers.hsnew file+8
@@ -0,0 +1,8 @@
1+module Handlers
2+ ( module Handlers.Users.Registration,
3+ module Handlers.Posts,
4+ )
5+where
6+
7+import Handlers.Posts
8+import Handlers.Users.Registration
l5/src/Handlers/Posts.hsnew file+34
@@ -0,0 +1,34 @@
1+module Handlers.Posts
2+ ( viewPosts,
3+ newPost,
4+ )
5+where
6+
7+import Control.Concurrent.STM.TVar
8+import Control.Monad.Reader
9+import Data.Text (Text)
10+import Data.Time.Clock
11+import GHC.Conc
12+import Types
13+
14+viewPosts ::
15+ (MonadReader ServerEnv m, MonadIO m) =>
16+ Username ->
17+ NoBody ->
18+ m (ServerResponse (JsonBody [Post]))
19+viewPosts _ _ = do
20+ postsTVar <- asks posts
21+ postsList <- liftIO . readTVarIO $ postsTVar
22+ return defaultServerResponse {responseBody = Just (JsonBody postsList)}
23+
24+newPost ::
25+ (MonadReader ServerEnv m, MonadIO m) =>
26+ Username ->
27+ Text ->
28+ m (ServerResponse NoBody)
29+newPost u p = do
30+ now <- liftIO getCurrentTime
31+ postsTVar <- asks posts
32+ liftIO . atomically $
33+ modifyTVar postsTVar (Post {author = u, body = p, timestamp = now} :)
34+ return defaultServerResponse
l5/src/Handlers/Users/Registration.hsnew file+101
@@ -0,0 +1,101 @@
1+module Handlers.Users.Registration
2+ ( registerUser,
3+ logIn,
4+ )
5+where
6+
7+import Control.Concurrent.STM.TVar
8+import Control.Monad.Reader
9+import Data.Aeson
10+import qualified Data.Map as M
11+import Data.Text (Text)
12+import GHC.Conc
13+import GHC.Generics
14+import Types
15+import Web.Cookie
16+
17+registerUser ::
18+ (MonadReader ServerEnv m, MonadIO m, MonadRandom m) =>
19+ JsonBody UserRegistration ->
20+ m (ServerResponse NoBody)
21+registerUser (JsonBody u) = do
22+ let (k, v) = toUser u
23+ usersTVar <- asks users
24+ liftIO . atomically $ modifyTVar usersTVar $ M.insert k v
25+ logInUser k
26+
27+data LogInErrors = UserNotFound | WrongPassword
28+ deriving (Show)
29+
30+instance ServerError LogInErrors
31+
32+logIn ::
33+ ( MonadReader ServerEnv m,
34+ MonadIO m,
35+ MonadRandom m,
36+ MonadError ResponseError m
37+ ) =>
38+ JsonBody UserLogIn ->
39+ m (ServerResponse NoBody)
40+logIn (JsonBody u) = do
41+ usersTVar <- asks users
42+ usersM <- liftIO . readTVarIO $ usersTVar
43+ let foundUser = M.lookup (logInUsername u) usersM
44+ case foundUser of
45+ Nothing -> throwServerError UserNotFound
46+ Just us ->
47+ if passwordHash us == hash (logInPassword u)
48+ then logInUser (logInUsername u)
49+ else throwServerError WrongPassword
50+
51+logInUser ::
52+ ( MonadReader ServerEnv m,
53+ MonadIO m,
54+ MonadRandom m
55+ ) =>
56+ Username ->
57+ m (ServerResponse NoBody)
58+logInUser uName = do
59+ cookie <- generateCookie
60+ cookiesTVar <- asks cookies
61+ liftIO . atomically $ modifyTVar cookiesTVar $ M.insert (hash cookie) uName
62+ let setCookies =
63+ defaultSetCookie
64+ { setCookieName = authCookieNameBS,
65+ setCookieValue = encodeCookie cookie,
66+ setCookieMaxAge = Just 100000000,
67+ setCookiePath = Just "/"
68+ }
69+ return . addCookies setCookies $
70+ defaultServerResponse
71+ { responseBody = Just NoBody,
72+ responseStatus = status200
73+ }
74+
75+data UserRegistration
76+ = UserRegistration
77+ { registrationUsername :: Username,
78+ registrationName :: Text,
79+ registrationPassword :: Password
80+ }
81+ deriving (Generic)
82+
83+instance FromJSON UserRegistration
84+
85+data UserLogIn
86+ = UserLogIn
87+ { logInUsername :: Username,
88+ logInPassword :: Password
89+ }
90+ deriving (Generic)
91+
92+instance FromJSON UserLogIn
93+
94+toUser :: UserRegistration -> (Username, User)
95+toUser u =
96+ ( registrationUsername u,
97+ User
98+ { name = registrationName u,
99+ passwordHash = hash $ registrationPassword u
100+ }
101+ )
l5/src/Orphans.hsnew file+16
@@ -0,0 +1,16 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Orphans
4+ (
5+ )
6+where
7+
8+import Control.Monad.Except
9+import Control.Monad.Reader
10+import Crypto.Random
11+
12+instance MonadRandom m => MonadRandom (ReaderT r m) where
13+ getRandomBytes = lift . getRandomBytes
14+
15+instance MonadRandom m => MonadRandom (ExceptT e m) where
16+ getRandomBytes = lift . getRandomBytes
l5/src/Server.hsnew file+92
@@ -0,0 +1,92 @@
1+module Server (runServer) where
2+
3+import Control.Concurrent.STM.TVar
4+import Control.Monad.Except
5+import Control.Monad.Reader
6+import qualified Data.ByteString.Char8 as BSC
7+import qualified Data.ByteString.Lazy.Char8 as BSLC
8+import Data.List
9+import qualified Data.Map as M
10+import Data.Text (Text)
11+import Debug.Trace
12+import Handlers
13+import Network.HTTP.Types.Method
14+import Network.HTTP.Types.Status
15+import Network.Wai
16+import Network.Wai.Handler.Warp
17+import Orphans ()
18+import Types
19+import Web.Cookie
20+
21+runServer :: IO ()
22+runServer = do
23+ usersTVar <- newTVarIO M.empty
24+ cookiesTVar <- newTVarIO M.empty
25+ postsTVar <- newTVarIO []
26+ let serverEnv =
27+ ServerEnv
28+ { users = usersTVar,
29+ cookies = cookiesTVar,
30+ posts = postsTVar
31+ }
32+ run 8000 $ \req respF -> do
33+ putStr $ BSC.unpack (rawPathInfo req) <> "\t"
34+ resp <- flip runReaderT serverEnv $ router (requestMethod req) (pathInfo req) req
35+ respF resp
36+
37+runHandler ::
38+ (FromResponseBody a, ToResponseBody b, MonadIO m) =>
39+ (a -> ExceptT ResponseError m (ServerResponse b)) ->
40+ Request ->
41+ m Response
42+runHandler f req = do
43+ rBody <- liftIO $ lazyRequestBody req
44+ case fromBody rBody of
45+ Right a -> do
46+ liftIO $ putStrLn "OK"
47+ either fromServerResponse fromServerResponse <$> runExceptT (f a)
48+ Left e -> do
49+ liftIO $ putStrLn "bad body"
50+ return $ responseLBS status422 [] (BSLC.pack e)
51+
52+runAuthHandler ::
53+ (FromResponseBody a, ToResponseBody b, MonadIO m, MonadReader ServerEnv m) =>
54+ (Username -> a -> ExceptT ResponseError m (ServerResponse b)) ->
55+ Request ->
56+ m Response
57+runAuthHandler f req = do
58+ let authCookie =
59+ fmap snd
60+ . traceShowId
61+ . find ((==) authCookieNameBS . fst)
62+ . traceShowId
63+ . mconcat
64+ . traceShowId
65+ . fmap (parseCookies . snd)
66+ . traceShowId
67+ . filter ((==) "Cookie" . fst)
68+ . traceShowId
69+ $ requestHeaders req
70+ decodedCookie = authCookie >>= either (const Nothing) Just . decodeCookie
71+ cookiesTVar <- asks cookies
72+ cs <- liftIO $ readTVarIO cookiesTVar
73+ let
74+ case hash <$> decodedCookie >>= flip M.lookup cs of
75+ Nothing -> return $ responseLBS unauthorized401 [] "Unauthorized"
76+ Just u -> runHandler (f u) req
77+
78+fromServerResponse :: ToResponseBody b => ServerResponse b -> Response
79+fromServerResponse (ServerResponse b s h) = responseLBS s h (maybe "" toBody b)
80+
81+type Path = [Text]
82+
83+router :: Method -> Path -> Request -> ServerM Response
84+router "POST" ["users", "register"] = runHandler registerUser
85+router "POST" ["users", "log_in"] = runHandler logIn
86+router "GET" ["posts"] = runAuthHandler viewPosts
87+router "POST" ["posts"] = runAuthHandler newPost
88+router _ _ = \_ -> do
89+ liftIO $ putStrLn "not found"
90+ return $ responseLBS status404 [] "Not found"
91+
92+type ServerM a = ReaderT ServerEnv IO a
l5/src/Types.hsnew file+10
@@ -0,0 +1,10 @@
1+module Types
2+ ( module Types.Users,
3+ module Types.Server,
4+ module Types.Post,
5+ )
6+where
7+
8+import Types.Post
9+import Types.Server
10+import Types.Users
l5/src/Types/Hash.hsnew file+31
@@ -0,0 +1,31 @@
1+module Types.Hash
2+ ( hash,
3+ Hash,
4+ Hashable (..),
5+ )
6+where
7+
8+import qualified Crypto.Hash as H
9+import Crypto.Hash.Algorithms
10+import Data.ByteString (ByteString)
11+import qualified Data.ByteString.Lazy as BSL
12+import Data.Text (Text)
13+import qualified Data.Text.Encoding as T
14+
15+newtype Hash r = Hash (H.Digest SHA3_256)
16+ deriving (Eq, Ord)
17+
18+class Hashable x where
19+ toBS :: x -> ByteString
20+
21+hash :: (Hashable x) => x -> Hash x
22+hash = Hash . H.hash . toBS
23+
24+instance Hashable ByteString where
25+ toBS = id
26+
27+instance Hashable BSL.ByteString where
28+ toBS = BSL.toStrict
29+
30+instance Hashable Text where
31+ toBS = toBS . T.encodeUtf8
l5/src/Types/Post.hsnew file+20
@@ -0,0 +1,20 @@
1+module Types.Post
2+ ( Post (..),
3+ )
4+where
5+
6+import Data.Aeson
7+import Data.Text (Text)
8+import Data.Time.Clock
9+import GHC.Generics
10+import Types.Users
11+
12+data Post
13+ = Post
14+ { author :: Username,
15+ body :: Text,
16+ timestamp :: UTCTime
17+ }
18+ deriving (Generic)
19+
20+instance ToJSON Post
l5/src/Types/Server.hsnew file+115
@@ -0,0 +1,115 @@
1+module Types.Server
2+ ( module Network.HTTP.Types.Status,
3+ module Network.HTTP.Types.Header,
4+ ServerResponse (..),
5+ defaultServerResponse,
6+ addCookies,
7+ ServerEnv (..),
8+ ToResponseBody (..),
9+ FromResponseBody (..),
10+ NoBody (..),
11+ JsonBody (..),
12+ ServerError (..),
13+ throwServerError,
14+ ResponseError,
15+ MonadError,
16+ )
17+where
18+
19+import Control.Concurrent.STM.TVar
20+import Control.Monad.Except
21+import qualified Data.Aeson as A
22+import Data.Bifunctor
23+import qualified Data.ByteString.Builder as BS
24+import qualified Data.ByteString.Lazy as BSL
25+import Data.ByteString.Lazy (ByteString)
26+import qualified Data.ByteString.Lazy.Char8 as BSLC
27+import Data.Map (Map)
28+import Data.Text (Text)
29+import qualified Data.Text.Encoding as T
30+import Network.HTTP.Types.Header
31+import Network.HTTP.Types.Status
32+import Types.Post
33+import Types.Users
34+import Web.Cookie
35+
36+data ServerEnv
37+ = ServerEnv
38+ { users :: TVar (Map Username User),
39+ cookies :: TVar (Map (Hash RandomCookie) Username),
40+ posts :: TVar [Post]
41+ }
42+
43+data ServerResponse a
44+ = ServerResponse
45+ { responseBody :: Maybe a,
46+ responseStatus :: Status,
47+ responseHeaders :: ResponseHeaders
48+ }
49+
50+defaultServerResponse :: ServerResponse a
51+defaultServerResponse =
52+ ServerResponse
53+ { responseBody = Nothing,
54+ responseStatus = status200,
55+ responseHeaders = []
56+ }
57+
58+addCookies :: SetCookie -> ServerResponse a -> ServerResponse a
59+addCookies c r =
60+ r
61+ { responseHeaders = ("Set-Cookie", newCookies) : responseHeaders r
62+ }
63+ where
64+ newCookies = BSL.toStrict . BS.toLazyByteString $ renderSetCookie c
65+
66+class ToResponseBody r where
67+ toBody :: r -> ByteString
68+
69+class FromResponseBody r where
70+ fromBody :: ByteString -> Either String r
71+
72+data NoBody = NoBody
73+
74+instance ToResponseBody NoBody where
75+ toBody _ = ""
76+
77+instance FromResponseBody NoBody where
78+ fromBody _ = return NoBody
79+
80+data JsonBody a = JsonBody {unJsonBody :: a}
81+
82+instance A.ToJSON a => ToResponseBody (JsonBody a) where
83+ toBody (JsonBody a) = A.encode a
84+
85+instance A.FromJSON a => FromResponseBody (JsonBody a) where
86+ fromBody = fmap JsonBody . A.eitherDecode
87+
88+instance ToResponseBody ByteString where
89+ toBody = id
90+
91+instance FromResponseBody ByteString where
92+ fromBody = return
93+
94+instance ToResponseBody Text where
95+ toBody = BSL.fromStrict . T.encodeUtf8
96+
97+instance FromResponseBody Text where
98+ fromBody = first show . T.decodeUtf8' . BSL.toStrict
99+
100+class ServerError e where
101+ serverErrorStatus :: e -> Status
102+ serverErrorStatus _ = status422
103+
104+throwServerError ::
105+ (MonadError (ServerResponse ByteString) m, ServerError e, Show e) =>
106+ e ->
107+ m a
108+throwServerError e =
109+ throwError $
110+ defaultServerResponse
111+ { responseBody = Just . BSLC.pack . show $ e,
112+ responseStatus = serverErrorStatus e
113+ }
114+
115+type ResponseError = ServerResponse ByteString
l5/src/Types/Users.hsnew file+60
@@ -0,0 +1,60 @@
1+module Types.Users
2+ ( module Types.Hash,
3+ module Crypto.Random,
4+ Username (..),
5+ User (..),
6+ Password (..),
7+ RandomCookie (..),
8+ generateCookie,
9+ encodeCookie,
10+ decodeCookie,
11+ authCookieNameT,
12+ authCookieNameBS,
13+ )
14+where
15+
16+import Crypto.Random
17+import Data.Aeson
18+import Data.ByteString
19+import qualified Data.ByteString.Base64 as B64
20+import Data.Text (Text)
21+import qualified Data.Text.Encoding as T
22+import GHC.Generics (Generic)
23+import Types.Hash
24+
25+newtype Username
26+ = Username {unUsername :: Text}
27+ deriving (FromJSON, ToJSON, Ord, Eq)
28+
29+newtype Password
30+ = Password {unPassword :: Text}
31+ deriving (FromJSON, ToJSON, Ord, Eq, Hashable)
32+
33+data User
34+ = User
35+ { name :: Text,
36+ passwordHash :: Hash Password
37+ }
38+ deriving (Generic)
39+
40+newtype RandomCookie
41+ = RandomCookie {unRandomCookie :: ByteString}
42+ deriving (Hashable, Eq, Ord)
43+
44+cookieSize :: Int
45+cookieSize = 512
46+
47+generateCookie :: MonadRandom m => m RandomCookie
48+generateCookie = RandomCookie <$> getRandomBytes cookieSize
49+
50+encodeCookie :: RandomCookie -> ByteString
51+encodeCookie = B64.encode . unRandomCookie
52+
53+decodeCookie :: ByteString -> Either String RandomCookie
54+decodeCookie = fmap RandomCookie . B64.decode
55+
56+authCookieNameBS :: ByteString
57+authCookieNameBS = T.encodeUtf8 authCookieNameT
58+
59+authCookieNameT :: Text
60+authCookieNameT = "auth"
l5/stack.yamlnew file+66
@@ -0,0 +1,66 @@
1+# This file was automatically generated by 'stack init'
2+#
3+# Some commonly used options have been documented as comments in this file.
4+# For advanced use and comprehensive documentation of the format, please see:
5+# https://docs.haskellstack.org/en/stable/yaml_configuration/
6+
7+# Resolver to choose a 'specific' stackage snapshot or a compiler version.
8+# A snapshot resolver dictates the compiler version and the set of packages
9+# to be used for project dependencies. For example:
10+#
11+# resolver: lts-3.5
12+# resolver: nightly-2015-09-21
13+# resolver: ghc-7.10.2
14+#
15+# The location of a snapshot can be provided as a file or url. Stack assumes
16+# a snapshot provided as a file might change, whereas a url resource does not.
17+#
18+# resolver: ./custom-snapshot.yaml
19+# resolver: https://example.com/snapshots/2018-01-01.yaml
20+resolver: lts-14.27
21+
22+# User packages to be built.
23+# Various formats can be used as shown in the example below.
24+#
25+# packages:
26+# - some-directory
27+# - https://example.com/foo/bar/baz-0.0.2.tar.gz
28+# subdirs:
29+# - auto-update
30+# - wai
31+packages:
32+- .
33+# Dependency packages to be pulled from upstream that are not in the resolver.
34+# These entries can reference officially published versions as well as
35+# forks / in-progress versions pinned to a git hash. For example:
36+#
37+# extra-deps:
38+# - acme-missiles-0.3
39+# - git: https://github.com/commercialhaskell/stack.git
40+# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
41+#
42+# extra-deps: []
43+
44+# Override default flag values for local packages and extra-deps
45+# flags: {}
46+
47+# Extra package databases containing global packages
48+# extra-package-dbs: []
49+
50+# Control whether we use the GHC we find on the path
51+# system-ghc: true
52+#
53+# Require a specific version of stack, using version ranges
54+# require-stack-version: -any # Default
55+# require-stack-version: ">=2.3"
56+#
57+# Override the architecture used by stack, especially useful on Windows
58+# arch: i386
59+# arch: x86_64
60+#
61+# Extra directories used by stack for building
62+# extra-include-dirs: [/path/to/dir]
63+# extra-lib-dirs: [/path/to/dir]
64+#
65+# Allow a newer minor version of GHC than the snapshot specifies
66+# compiler-check: newer-minor
l5/test/Spec.hsnew file+2
@@ -0,0 +1,2 @@
1+main :: IO ()
2+main = putStrLn "Test suite not yet implemented"