Hash passwords when creating accounts
TL;DR: - introduce the Cryptonite library - Remove the redundant language extensions, imports, deps from Persistent - Prefer NoContent return type for POST /accounts - Define custom {To,From}JSON instances for Role
This commit is contained in:
parent
bb36dd1f9e
commit
b170be9375
4 changed files with 76 additions and 52 deletions
|
@ -8,9 +8,7 @@ in pkgs.mkShell {
|
||||||
hpkgs.resource-pool
|
hpkgs.resource-pool
|
||||||
hpkgs.sqlite-simple
|
hpkgs.sqlite-simple
|
||||||
hpkgs.warp
|
hpkgs.warp
|
||||||
hpkgs.persistent
|
hpkgs.cryptonite
|
||||||
hpkgs.persistent-sqlite
|
|
||||||
hpkgs.persistent-template
|
|
||||||
]))
|
]))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ import qualified Types as T
|
||||||
type API =
|
type API =
|
||||||
-- accounts: Create
|
-- accounts: Create
|
||||||
"accounts"
|
"accounts"
|
||||||
:> ReqBody '[JSON] T.Account
|
:> ReqBody '[JSON] T.CreateAccountRequest
|
||||||
:> Post '[JSON] (Maybe T.Session)
|
:> Post '[JSON] NoContent
|
||||||
-- accounts: Read
|
-- accounts: Read
|
||||||
-- accounts: Update
|
-- accounts: Update
|
||||||
-- accounts: Delete
|
-- accounts: Delete
|
||||||
|
|
18
src/App.hs
18
src/App.hs
|
@ -33,14 +33,16 @@ server dbFile = createAccountH
|
||||||
listTripsH = liftIO $ listTrips
|
listTripsH = liftIO $ listTrips
|
||||||
|
|
||||||
-- TODO(wpcarro): Handle failed CONSTRAINTs instead of sending 500s
|
-- TODO(wpcarro): Handle failed CONSTRAINTs instead of sending 500s
|
||||||
createAccount :: T.Account -> IO (Maybe T.Session)
|
createAccount :: T.CreateAccountRequest -> IO NoContent
|
||||||
createAccount account = withConnection dbFile $ \conn -> do
|
createAccount request = withConnection dbFile $ \conn -> do
|
||||||
execute conn "INSERT INTO Accounts (username,password,email,role,profilePicture) VALUES (?,?,?,?,?)"
|
hashed <- T.hashPassword (T.createAccountRequestPassword request)
|
||||||
(account & T.accountFields)
|
execute conn "INSERT INTO Accounts (username,password,email,role) VALUES (?,?,?,?)"
|
||||||
T.Session{ T.username = T.accountUsername account
|
( T.createAccountRequestUsername request
|
||||||
, T.password = T.accountPassword account
|
, hashed
|
||||||
, T.role = T.accountRole account
|
, T.createAccountRequestEmail request
|
||||||
} & Just & pure
|
, T.createAccountRequestRole request
|
||||||
|
)
|
||||||
|
pure NoContent
|
||||||
|
|
||||||
deleteAccount :: Text -> IO NoContent
|
deleteAccount :: Text -> IO NoContent
|
||||||
deleteAccount username = withConnection dbFile $ \conn -> do
|
deleteAccount username = withConnection dbFile $ \conn -> do
|
||||||
|
|
102
src/Types.hs
102
src/Types.hs
|
@ -1,11 +1,6 @@
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE GADTs #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
module Types where
|
module Types where
|
||||||
|
@ -14,14 +9,17 @@ import Data.Aeson
|
||||||
import Data.Function ((&))
|
import Data.Function ((&))
|
||||||
import Data.Text
|
import Data.Text
|
||||||
import Data.Typeable
|
import Data.Typeable
|
||||||
import Database.Persist.TH
|
|
||||||
import Database.SQLite.Simple
|
import Database.SQLite.Simple
|
||||||
import Database.SQLite.Simple.Ok
|
import Database.SQLite.Simple.Ok
|
||||||
import Database.SQLite.Simple.FromField
|
import Database.SQLite.Simple.FromField
|
||||||
import Database.SQLite.Simple.ToField
|
import Database.SQLite.Simple.ToField
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
|
import Crypto.Random.Types (MonadRandom)
|
||||||
|
|
||||||
|
import qualified Crypto.KDF.BCrypt as BC
|
||||||
|
import qualified Data.ByteString.Char8 as B
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.Text.Encoding as TE
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
-- TODO(wpcarro): Properly handle NULL for columns like profilePicture.
|
-- TODO(wpcarro): Properly handle NULL for columns like profilePicture.
|
||||||
|
@ -43,6 +41,18 @@ instance ToField Username where
|
||||||
instance FromField Username where
|
instance FromField Username where
|
||||||
fromField = forNewtype Username
|
fromField = forNewtype Username
|
||||||
|
|
||||||
|
newtype HashedPassword = HashedPassword BS.ByteString
|
||||||
|
deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
instance ToField HashedPassword where
|
||||||
|
toField (HashedPassword x) = SQLText (TE.decodeUtf8 x)
|
||||||
|
|
||||||
|
instance FromField HashedPassword where
|
||||||
|
fromField field =
|
||||||
|
case fieldData field of
|
||||||
|
(SQLText x) -> x & TE.encodeUtf8 & HashedPassword & Ok
|
||||||
|
_ -> returnError ConversionFailed field ""
|
||||||
|
|
||||||
newtype ClearTextPassword = ClearTextPassword Text
|
newtype ClearTextPassword = ClearTextPassword Text
|
||||||
deriving (Eq, Show, Generic)
|
deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
@ -70,8 +80,17 @@ instance FromField Email where
|
||||||
data Role = RegularUser | Manager | Admin
|
data Role = RegularUser | Manager | Admin
|
||||||
deriving (Eq, Show, Generic)
|
deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
instance ToJSON Role
|
instance ToJSON Role where
|
||||||
instance FromJSON Role
|
toJSON RegularUser = "user"
|
||||||
|
toJSON Manager = "manager"
|
||||||
|
toJSON Admin = "admin"
|
||||||
|
|
||||||
|
instance FromJSON Role where
|
||||||
|
parseJSON = withText "Role" $ \x ->
|
||||||
|
case x of
|
||||||
|
"user" -> pure RegularUser
|
||||||
|
"manager" -> pure Manager
|
||||||
|
"admin" -> pure Admin
|
||||||
|
|
||||||
instance ToField Role where
|
instance ToField Role where
|
||||||
toField RegularUser = SQLText "user"
|
toField RegularUser = SQLText "user"
|
||||||
|
@ -101,37 +120,14 @@ instance FromField ProfilePicture where
|
||||||
|
|
||||||
data Account = Account
|
data Account = Account
|
||||||
{ accountUsername :: Username
|
{ accountUsername :: Username
|
||||||
, accountPassword :: ClearTextPassword
|
, accountPassword :: HashedPassword
|
||||||
, accountEmail :: Email
|
, accountEmail :: Email
|
||||||
, accountRole :: Role
|
, accountRole :: Role
|
||||||
, accountProfilePicture :: ProfilePicture
|
, accountProfilePicture :: Maybe ProfilePicture
|
||||||
} deriving (Eq, Show, Generic)
|
} deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
instance ToJSON Account where
|
|
||||||
toJSON (Account username password email role profilePicture) =
|
|
||||||
object [ "username" .= username
|
|
||||||
, "password" .= password
|
|
||||||
, "email" .= email
|
|
||||||
, "role" .= role
|
|
||||||
, "profilePicture" .= profilePicture
|
|
||||||
]
|
|
||||||
|
|
||||||
instance FromJSON Account where
|
|
||||||
parseJSON = withObject "Account" $ \x -> do
|
|
||||||
username <- x .: "username"
|
|
||||||
password <- x .: "password"
|
|
||||||
email <- x .: "email"
|
|
||||||
role <- x .: "role"
|
|
||||||
profilePicture <- x .: "profilePicture"
|
|
||||||
pure Account{ accountUsername = username
|
|
||||||
, accountPassword = password
|
|
||||||
, accountEmail = email
|
|
||||||
, accountRole = role
|
|
||||||
, accountProfilePicture = profilePicture
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Return a tuple with all of the fields for an Account record to use for SQL.
|
-- | Return a tuple with all of the fields for an Account record to use for SQL.
|
||||||
accountFields :: Account -> (Username, ClearTextPassword, Email, Role, ProfilePicture)
|
accountFields :: Account -> (Username, HashedPassword, Email, Role, Maybe ProfilePicture)
|
||||||
accountFields (Account { accountUsername
|
accountFields (Account { accountUsername
|
||||||
, accountPassword
|
, accountPassword
|
||||||
, accountEmail
|
, accountEmail
|
||||||
|
@ -154,14 +150,12 @@ instance FromRow Account where
|
||||||
|
|
||||||
data Session = Session
|
data Session = Session
|
||||||
{ username :: Username
|
{ username :: Username
|
||||||
, password :: ClearTextPassword
|
|
||||||
, role :: Role
|
, role :: Role
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
instance ToJSON Session where
|
instance ToJSON Session where
|
||||||
toJSON (Session username password role) =
|
toJSON (Session username role) =
|
||||||
object [ "username" .= username
|
object [ "username" .= username
|
||||||
, "password" .= password
|
|
||||||
, "role" .= role
|
, "role" .= role
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -284,7 +278,7 @@ instance FromJSON Trip where
|
||||||
-- passwords and emails.
|
-- passwords and emails.
|
||||||
data User = User
|
data User = User
|
||||||
{ userUsername :: Username
|
{ userUsername :: Username
|
||||||
, userProfilePicture :: ProfilePicture
|
, userProfilePicture :: Maybe ProfilePicture
|
||||||
, userRole :: Role
|
, userRole :: Role
|
||||||
} deriving (Eq, Show, Generic)
|
} deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
@ -316,3 +310,33 @@ instance FromJSON AccountCredentials where
|
||||||
pure AccountCredentials{ accountCredentialsUsername = username
|
pure AccountCredentials{ accountCredentialsUsername = username
|
||||||
, accountCredentialsPassword = password
|
, accountCredentialsPassword = password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- -- | Hash password `x`.
|
||||||
|
hashPassword :: (MonadRandom m) => ClearTextPassword -> m HashedPassword
|
||||||
|
hashPassword (ClearTextPassword x) = do
|
||||||
|
hashed <- BC.hashPassword 12 (x & unpack & B.pack)
|
||||||
|
pure $ HashedPassword hashed
|
||||||
|
|
||||||
|
data CreateAccountRequest = CreateAccountRequest
|
||||||
|
{ createAccountRequestUsername :: Username
|
||||||
|
, createAccountRequestPassword :: ClearTextPassword
|
||||||
|
, createAccountRequestEmail :: Email
|
||||||
|
, createAccountRequestRole :: Role
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
instance FromJSON CreateAccountRequest where
|
||||||
|
parseJSON = withObject "CreateAccountRequest" $ \x -> do
|
||||||
|
username <- x .: "username"
|
||||||
|
password <- x .: "password"
|
||||||
|
email <- x .: "email"
|
||||||
|
role <- x .: "role"
|
||||||
|
pure $ CreateAccountRequest username password email role
|
||||||
|
|
||||||
|
createAccountRequestFields :: CreateAccountRequest -> (Username, ClearTextPassword, Email, Role)
|
||||||
|
createAccountRequestFields request =
|
||||||
|
( createAccountRequestUsername request
|
||||||
|
, createAccountRequestPassword request
|
||||||
|
, createAccountRequestEmail request
|
||||||
|
, createAccountRequestRole request
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue