Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| {-# LANGUAGE DeriveDataTypeable #-} | |
| {-# LANGUAGE GeneralizedNewtypeDeriving #-} | |
| {-# LANGUAGE OverloadedStrings #-} | |
| {-# LANGUAGE TypeFamilies #-} | |
| {-# LANGUAGE FlexibleContexts #-} | |
| {-# LANGUAGE ConstraintKinds #-} | |
| module Web.Users.Types | |
| ( -- * The core type class | |
| UserStorageBackend (..) | |
| -- * User representation | |
| , User(..), Password(..), makePassword, hidePassword | |
| , PasswordPlain(..), verifyPassword | |
| , UserField(..) | |
| -- * Token types | |
| , PasswordResetToken(..), ActivationToken(..), SessionId(..) | |
| -- * Error types | |
| , CreateUserError(..), UpdateUserError(..) | |
| , TokenError(..) | |
| -- * Helper typed | |
| , SortBy(..) | |
| ) | |
| where | |
| import Crypto.BCrypt | |
| import Data.Aeson | |
| import Data.Int | |
| import Data.Maybe | |
| import Data.String | |
| import Data.Time.Clock | |
| import Data.Typeable | |
| import Web.PathPieces | |
| import qualified Data.Text as T | |
| import qualified Data.Text.Encoding as T | |
| import qualified System.IO.Unsafe as U | |
| -- | Errors that happen on storage level during user creation | |
| data CreateUserError | |
| = InvalidPassword | |
| | UsernameAlreadyTaken | |
| | EmailAlreadyTaken | |
| | UsernameAndEmailAlreadyTaken | |
| deriving (Show, Eq) | |
| -- | Errors that happen on storage level during user updating | |
| data UpdateUserError | |
| = UsernameAlreadyExists | |
| | EmailAlreadyExists | |
| | UserDoesntExist | |
| deriving (Show, Eq) | |
| -- | Errors that happen on storage level during token actions | |
| data TokenError | |
| = TokenInvalid | |
| deriving (Show, Eq) | |
| -- | Sorting direction | |
| data SortBy t | |
| = SortAsc t | |
| | SortDesc t | |
| -- | Backend constraints | |
| type IsUserBackend b = | |
| ( Show (UserId b) | |
| , Eq (UserId b) | |
| , ToJSON (UserId b) | |
| , FromJSON (UserId b) | |
| , Typeable (UserId b) | |
| , PathPiece (UserId b) | |
| ) | |
| -- | An abstract backend for managing users. A backend library should implement the interface and | |
| -- an end user should build applications on top of this interface. | |
| class IsUserBackend b => UserStorageBackend b where | |
| -- | The storage backends userid | |
| type UserId b :: * | |
| -- | Initialise the backend. Call once on application launch to for | |
| -- example create missing database tables | |
| initUserBackend :: b -> IO () | |
| -- | Destory the backend. WARNING: This is only for testing! It deletes all tables and data. | |
| destroyUserBackend :: b -> IO () | |
| -- | This cleans up invalid sessions and other tokens. Call periodically as needed. | |
| housekeepBackend :: b -> IO () | |
| -- | Retrieve a user id from the database | |
| getUserIdByName :: b -> T.Text -> IO (Maybe (UserId b)) | |
| -- | Retrieve a user from the database | |
| getUserById :: b -> UserId b -> IO (Maybe User) | |
| -- | List all users unlimited, or limited, sorted by a 'UserField' | |
| listUsers :: b -> Maybe (Int64, Int64) -> SortBy UserField -> IO [(UserId b, User)] | |
| -- | Count all users | |
| countUsers :: b -> IO Int64 | |
| -- | Create a user | |
| createUser :: b -> User -> IO (Either CreateUserError (UserId b)) | |
| -- | Modify a user | |
| updateUser :: b -> UserId b -> (User -> User) -> IO (Either UpdateUserError ()) | |
| -- | Delete a user | |
| deleteUser :: b -> UserId b -> IO () | |
| -- | Authentificate a user using username/email and password. The 'NominalDiffTime' describes the session duration | |
| authUser :: b -> T.Text -> PasswordPlain -> NominalDiffTime -> IO (Maybe SessionId) | |
| -- | Authentificate a user and execute a single action. | |
| withAuthUser :: b -> T.Text -> (User -> Bool) -> (UserId b -> IO r) -> IO (Maybe r) | |
| -- | Verify a 'SessionId'. The session duration can be extended by 'NominalDiffTime' | |
| verifySession :: b -> SessionId -> NominalDiffTime -> IO (Maybe (UserId b)) | |
| -- | Force create a session for a user. This is useful for support/admin login. | |
| -- If the user does not exist, this will fail. | |
| createSession :: b -> UserId b -> NominalDiffTime -> IO (Maybe SessionId) | |
| -- | Destroy a session | |
| destroySession :: b -> SessionId -> IO () | |
| -- | Request a 'PasswordResetToken' for a given user, valid for 'NominalDiffTime' | |
| requestPasswordReset :: b -> UserId b -> NominalDiffTime -> IO PasswordResetToken | |
| -- | Check if a 'PasswordResetToken' is still valid and retrieve the owner of it | |
| verifyPasswordResetToken :: b -> PasswordResetToken -> IO (Maybe User) | |
| -- | Apply a new password to the owner of 'PasswordResetToken' iff the token is still valid | |
| applyNewPassword :: b -> PasswordResetToken -> Password -> IO (Either TokenError ()) | |
| -- | Request an 'ActivationToken' for a given user, valid for 'NominalDiffTime' | |
| requestActivationToken :: b -> UserId b -> NominalDiffTime -> IO ActivationToken | |
| -- | Activate the owner of 'ActivationToken' iff the token is still valid | |
| activateUser :: b -> ActivationToken -> IO (Either TokenError ()) | |
| -- | A password reset token to send out to users via email or sms | |
| newtype PasswordResetToken | |
| = PasswordResetToken { unPasswordResetToken :: T.Text } | |
| deriving (Show, Eq, ToJSON, FromJSON, Typeable, PathPiece) | |
| -- | An activation token to send out to users via email or sms | |
| newtype ActivationToken | |
| = ActivationToken { unActivationToken :: T.Text } | |
| deriving (Show, Eq, ToJSON, FromJSON, Typeable, PathPiece) | |
| -- | A session id for identifying user sessions | |
| newtype SessionId | |
| = SessionId { unSessionId :: T.Text } | |
| deriving (Show, Eq, ToJSON, FromJSON, Typeable, PathPiece) | |
| -- | Construct a password from plaintext by hashing it | |
| makePassword :: PasswordPlain -> Password | |
| makePassword (PasswordPlain plainText) = | |
| let hash = | |
| T.decodeUtf8 $ fromJustPass $ U.unsafePerformIO $ | |
| hashPasswordUsingPolicy policy (T.encodeUtf8 plainText) | |
| in PasswordHash hash | |
| where | |
| policy = | |
| HashingPolicy | |
| { preferredHashCost = 8 | |
| , preferredHashAlgorithm = "$2b$" | |
| } | |
| fromJustPass = | |
| fromMaybe (error "makePassword failed. This is probably a bcrypt library error") | |
| -- | Check a plaintext password against a password | |
| verifyPassword :: PasswordPlain -> Password -> Bool | |
| verifyPassword (PasswordPlain plainText) pwd = | |
| case pwd of | |
| PasswordHidden -> False | |
| PasswordHash hash -> | |
| validatePassword (T.encodeUtf8 hash) (T.encodeUtf8 plainText) | |
| -- | Plaintext passsword. Used for authentification. | |
| newtype PasswordPlain | |
| = PasswordPlain { unPasswordPlain :: T.Text } | |
| deriving (Show, Eq, Typeable, IsString) | |
| -- | Password representation. When updating or creating a user, use 'makePassword' to create one. | |
| -- The implementation details of this type are ONLY for use in backend implementations. | |
| data Password | |
| = PasswordHash !T.Text | |
| | PasswordHidden | |
| deriving (Show, Eq, Typeable) | |
| -- | Strip the password from the user type. | |
| hidePassword :: User -> User | |
| hidePassword user = | |
| user { u_password = PasswordHidden } | |
| -- | Fields of user datatype | |
| data UserField | |
| = UserFieldId | |
| | UserFieldName | |
| | UserFieldEmail | |
| | UserFieldPassword | |
| | UserFieldActive | |
| deriving (Show, Eq) | |
| -- | Core user datatype | |
| data User | |
| = User | |
| { u_name :: !T.Text | |
| , u_email :: !T.Text | |
| , u_password :: !Password | |
| , u_active :: !Bool | |
| } deriving (Show, Eq, Typeable) | |
| instance ToJSON User where | |
| toJSON (User name email _ active) = | |
| object | |
| [ "name" .= name | |
| , "email" .= email | |
| , "active" .= active | |
| ] | |
| instance FromJSON User where | |
| parseJSON = | |
| withObject "User" $ \obj -> | |
| User <$> obj .: "name" | |
| <*> obj .: "email" | |
| <*> (parsePassword <$> (obj .:? "password")) | |
| <*> obj .: "active" | |
| where | |
| parsePassword maybePass = | |
| case maybePass of | |
| Nothing -> PasswordHidden | |
| Just pwd -> makePassword (PasswordPlain pwd) |