packages feed

wikimusic-api-1.2.0.8: src/WikiMusic/Sqlite/UserCommand.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module WikiMusic.Sqlite.UserCommand () where

import Data.ByteString.Base64 qualified
import Data.Password.Bcrypt
import Data.Text (pack)
import Data.Text qualified as T
import Data.UUID qualified as UUID
import Data.UUID.V4
import Database.Beam
import Database.Beam.Sqlite
import Relude
import WikiMusic.Beam.Database
import WikiMusic.Beam.User
import WikiMusic.Beam.Util
import WikiMusic.Free.UserCommand
import WikiMusic.Model.Auth
import WikiMusic.Model.Env
import WikiMusic.Protolude

instance Exec UserCommand where
  execAlgebra (MakeResetPasswordLink env email next) = doIfUserFoundByEmail env email (makeToken env) >>= next
  execAlgebra (ChangePasswordByEmail env email password next) = do
    next =<< doIfUserFoundByEmail env email (changePassword env password)
  execAlgebra (InvalidateResetTokenByEmail env email next) = next =<< doIfUserFoundByEmail env email (invalidateToken env)
  execAlgebra (InviteUser env _ email name' role desc next) = next =<< addUser env email name' role desc
  execAlgebra (DeleteUser env _ email next) = next =<< doIfUserFoundByEmail env email (deleteU env)

makeToken :: (MonadIO m) => Env -> UUID -> m (Either UserCommandError Text)
makeToken env identifier = do
  maybeUser <- liftIO $ runBeamSqliteDebug putStrLn (env ^. #conn) $ do
    runSelectReturningFirst $ select $ do
      filter_
        ( \s ->
            (s ^. #identifier)
              ==. val_ (UUID.toText identifier)
        )
        $ all_ ((^. #users) wikiMusicDatabase)
  now <- liftIO getCurrentTime
  maybeToken <- maybeGenToken
  case maybeUser of
    Nothing -> pure . Left $ PersistenceError ""
    Just x -> do
      case maybeToken of
        Left l -> pure . Left . PersistenceError . Relude.show $ l
        Right t -> do
          _ <-
            liftIO
              . runBeamSqliteDebug putStrLn (env ^. #conn)
              . runUpdate
              $ save
                ((^. #users) wikiMusicDatabase)
                (x {passwordResetToken = Just t, lastEditedAt = Just now} :: User')
          pure . Right $ t

maybeGenToken :: (MonadIO m) => m (Either UnicodeException Text)
maybeGenToken = do
  l <- pack . Relude.show <$> liftIO nextRandom
  r <- pack . Relude.show <$> liftIO nextRandom
  pure $ decodeUtf8' . Data.ByteString.Base64.encode . encodeUtf8 $ tokenConcat l r
  where
    tokenConcat l r = l <> "$" <> r

invalidateToken :: (MonadIO m) => Env -> UUID -> m (Either UserCommandError ())
invalidateToken env identifier = do
  now <- liftIO getCurrentTime
  art <- liftIO $ runBeamSqliteDebug putStrLn (env ^. #conn) $ do
    runSelectReturningOne $ select $ do
      filter_
        (\s -> (s ^. #identifier) ==. val_ (UUID.toText identifier))
        $ all_ ((^. #users) wikiMusicDatabase)
  case art of
    Nothing -> pure . Right $ ()
    Just foundArt -> do
      let a = foundArt {passwordResetToken = Nothing, lastEditedAt = Just now} :: User'
      liftIO . runBeamSqliteDebug putStrLn (env ^. #conn) . runUpdate . save ((^. #users) wikiMusicDatabase) $ a
      pure . Right $ ()

changePassword :: (MonadIO m) => Env -> UserPassword -> UUID -> m (Either UserCommandError ())
changePassword env password identifier = do
  hashed <- hashPassword (mkPassword (password ^. #value))
  now <- liftIO getCurrentTime
  new <- liftIO nextRandom
  art <- liftIO $ runBeamSqliteDebug putStrLn (env ^. #conn) $ do
    runSelectReturningOne $ select $ do
      filter_
        (\s -> (s ^. #identifier) ==. val_ (UUID.toText identifier))
        $ all_ ((^. #users) wikiMusicDatabase)
  case art of
    Nothing -> pure . Right $ ()
    Just foundArt -> do
      let a =
            foundArt
              { passwordHash = Just $ unPasswordHash hashed,
                authToken = Just $ newToken now new,
                lastEditedAt = Just now
              } ::
              User'
      liftIO . runBeamSqliteDebug putStrLn (env ^. #conn) . runUpdate . save ((^. #users) wikiMusicDatabase) $ a
      pure . Right $ ()

newToken :: UTCTime -> UUID -> Text
newToken now new =
  decodeUtf8
    . Data.ByteString.Base64.encode
    . encodeUtf8
    $ (pack . WikiMusic.Protolude.show $ now)
    <> "--"
    <> (pack . WikiMusic.Protolude.show $ new)

addUser :: (MonadIO m) => Env -> UserEmail -> UserName -> UserRole -> Maybe Text -> m (Either UserCommandError Text)
addUser env email name' role desc = do
  now <- liftIO getCurrentTime
  passResetToken <- liftIO nextRandom
  new <- liftIO nextRandom
  newNew <- liftIO nextRandom
  let u =
        User'
          { identifier = UUID.toText new,
            displayName = name' ^. #value,
            emailAddress = email ^. #value,
            passwordHash = Nothing,
            passwordResetToken = Just $ UUID.toText passResetToken,
            createdAt = now,
            authToken = Nothing,
            latestLoginAt = Nothing,
            latestLoginDevice = Nothing,
            avatarUrl = Nothing,
            lastEditedAt = Nothing,
            description = desc
          } ::
          User'
  let r =
        UserRole'
          { identifier = UUID.toText newNew,
            userIdentifier = UserId . UUID.toText $ new,
            roleId = T.pack . Relude.show $ role,
            createdAt = now
          } ::
          UserRole'
  liftIO
    . runBeamSqliteDebug putStrLn (env ^. #conn)
    . runInsert
    . insert ((^. #users) wikiMusicDatabase)
    $ insertValues [u]
  liftIO
    . runBeamSqliteDebug putStrLn (env ^. #conn)
    . runInsert
    . insert ((^. #userRoles) wikiMusicDatabase)
    $ insertValues [r]
  pure . Right . UUID.toText $ passResetToken

doIfUserFoundByEmail :: (MonadIO m) => Env -> UserEmail -> (UUID -> m (Either UserCommandError a)) -> m (Either UserCommandError a)
doIfUserFoundByEmail env email eff = do
  maybeUser <- liftIO $ runBeamSqliteDebug putStrLn (env ^. #conn) $ do
    runSelectReturningFirst $ select $ do
      filter_
        ( \s ->
            (s ^. #emailAddress)
              ==. val_ (email ^. #value)
        )
        $ all_ ((^. #users) wikiMusicDatabase)

  case maybeUser of
    Nothing -> pure . Left . LogicError $ "User could not be found!"
    Just u -> eff (textToUUID $ u ^. #identifier)

deleteU :: (MonadIO m) => Env -> UUID -> m (Either UserCommandError ())
deleteU env identifier = do
  maybeUser <- liftIO $ runBeamSqliteDebug putStrLn (env ^. #conn) $ do
    runSelectReturningFirst $ select $ do
      filter_
        ( \s ->
            (s ^. #identifier)
              ==. val_ (UUID.toText identifier)
        )
        $ all_ ((^. #users) wikiMusicDatabase)
  case maybeUser of
    Nothing -> pure . Left $ PersistenceError ""
    Just x -> do
      _ <-
        liftIO
          . runBeamSqliteDebug putStrLn (env ^. #conn)
          . runUpdate
          $ save ((^. #users) wikiMusicDatabase) (x {passwordHash = Just "", authToken = Just ""} :: User')
      pure . Right $ ()