From 8c34a7b3fd81e3dfef9a11e64e9a158eeb7eaad5 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 15 Jan 2026 15:19:04 +0100 Subject: [PATCH 01/16] UserStore: Move queries for service users into the store effect --- libs/wire-subsystems/src/Wire/UserStore.hs | 3 + .../src/Wire/UserStore/Cassandra.hs | 63 ++++++++++++++ .../unit/Wire/MockInterpreters/UserStore.hs | 3 + services/brig/src/Brig/Data/User.hs | 60 -------------- .../brig/src/Brig/InternalEvent/Process.hs | 6 +- services/brig/src/Brig/Provider/API.hs | 82 ++++++++----------- 6 files changed, 108 insertions(+), 109 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 55485bfb939..f8d8fe30735 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -97,6 +97,9 @@ data UserStore m a where GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) DeleteEmail :: UserId -> UserStore m () SetUserSearchable :: UserId -> SetSearchable -> UserStore m () + DeleteServiceUser :: ProviderId -> ServiceId -> BotId -> UserStore m () + LookupServiceUsers :: ProviderId -> ServiceId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId, Maybe TeamId)) + LookupServiceUsersForTeam :: ProviderId -> ServiceId -> TeamId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId)) makeSem ''UserStore diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index ec2c6a85b9b..fb32e6c8053 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -62,6 +62,9 @@ interpretUserStoreCassandra casClient = GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid DeleteEmail uid -> deleteEmailImpl uid SetUserSearchable uid searchable -> setUserSearchableImpl uid searchable + DeleteServiceUser pid sid bid -> deleteServiceUserImpl pid sid bid + LookupServiceUsers pid sid mPagingState -> lookupServiceUsersImpl pid sid mPagingState + LookupServiceUsersForTeam pid sid tid mPagingState -> lookupServiceUsersForTeamImpl pid sid tid mPagingState createUserImpl :: NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> Client () createUserImpl new mbConv = retry x5 . batch $ do @@ -260,6 +263,66 @@ setUserSearchableImpl uid (SetSearchable searchable) = retry x5 $ write q (param q :: PrepQuery W (Bool, UserId) () q = "UPDATE user SET searchable = ? WHERE id = ?" +deleteServiceUserImpl :: ProviderId -> ServiceId -> BotId -> Client () +deleteServiceUserImpl pid sid bid = do + lookupServiceUser pid sid bid >>= \case + Nothing -> pure () + Just (_, mbTid) -> retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery cql (pid, sid, bid) + for_ mbTid $ \tid -> + addPrepQuery cqlTeam (pid, sid, tid, bid) + where + cql :: PrepQuery W (ProviderId, ServiceId, BotId) () + cql = + "DELETE FROM service_user \ + \WHERE provider = ? AND service = ? AND user = ?" + cqlTeam :: PrepQuery W (ProviderId, ServiceId, TeamId, BotId) () + cqlTeam = + "DELETE FROM service_team \ + \WHERE provider = ? AND service = ? AND team = ? AND user = ?" + +lookupServiceUser :: + ProviderId -> + ServiceId -> + BotId -> + Client (Maybe (ConvId, Maybe TeamId)) +lookupServiceUser pid sid bid = + retry x1 (query1 cql (params LocalQuorum (pid, sid, bid))) + where + cql :: PrepQuery R (ProviderId, ServiceId, BotId) (ConvId, Maybe TeamId) + cql = + "SELECT conv, team FROM service_user \ + \WHERE provider = ? AND service = ? AND user = ?" + +lookupServiceUsersImpl :: + ProviderId -> + ServiceId -> + Maybe PagingState -> + Client (PageWithState (BotId, ConvId, Maybe TeamId)) +lookupServiceUsersImpl pid sid mPagingState = + paginateWithState cql (paramsPagingState LocalQuorum (pid, sid) 100 mPagingState) + where + cql :: PrepQuery R (ProviderId, ServiceId) (BotId, ConvId, Maybe TeamId) + cql = + "SELECT user, conv, team FROM service_user \ + \WHERE provider = ? AND service = ?" + +lookupServiceUsersForTeamImpl :: + ProviderId -> + ServiceId -> + TeamId -> + Maybe PagingState -> + Client (PageWithState (BotId, ConvId)) +lookupServiceUsersForTeamImpl pid sid tid mPagingState = + paginateWithState cql (paramsPagingState LocalQuorum (pid, sid, tid) 100 mPagingState) + where + cql :: PrepQuery R (ProviderId, ServiceId, TeamId) (BotId, ConvId) + cql = + "SELECT user, conv FROM service_team \ + \WHERE provider = ? AND service = ? AND team = ?" + -------------------------------------------------------------------------------- -- Queries diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index 6e2086242c4..d0725865967 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -115,6 +115,9 @@ inMemoryUserStoreInterpreter = interpret $ \case if u.id == uid then u {Wire.StoredUser.searchable = Just searchable} :: StoredUser else u + DeleteServiceUser {} -> error "DeleteServiceUser: Not implemented" + LookupServiceUsers {} -> error "lookupServiceUsers: Not implemented" + LookupServiceUsersForTeam {} -> error "lookupServiceUsersForteam: Not implemented" storedUserToIndexUser :: StoredUser -> IndexUser storedUserToIndexUser storedUser = diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 0116e5e8e2b..39a091c53f2 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -29,8 +29,6 @@ module Brig.Data.User lookupName, lookupRichInfoMultiUsers, lookupUserTeam, - lookupServiceUsers, - lookupServiceUsersForTeam, lookupFeatureConferenceCalling, userExists, @@ -46,7 +44,6 @@ module Brig.Data.User -- * Deletions deleteEmailUnvalidated, - deleteServiceUser, ) where @@ -56,7 +53,6 @@ import Brig.Types.Intra import Cassandra hiding (Set) import Control.Error import Control.Lens hiding (from) -import Data.Conduit (ConduitM) import Data.Domain import Data.Handle (Handle) import Data.HavePendingInvitations @@ -205,26 +201,6 @@ updateFeatureConferenceCalling uid mStatus = deleteEmailUnvalidated :: (MonadClient m) => UserId -> m () deleteEmailUnvalidated u = retry x5 $ write userEmailUnvalidatedDelete (params LocalQuorum (Identity u)) -deleteServiceUser :: (MonadClient m) => ProviderId -> ServiceId -> BotId -> m () -deleteServiceUser pid sid bid = do - lookupServiceUser pid sid bid >>= \case - Nothing -> pure () - Just (_, mbTid) -> retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery cql (pid, sid, bid) - for_ mbTid $ \tid -> - addPrepQuery cqlTeam (pid, sid, tid, bid) - where - cql :: PrepQuery W (ProviderId, ServiceId, BotId) () - cql = - "DELETE FROM service_user \ - \WHERE provider = ? AND service = ? AND user = ?" - cqlTeam :: PrepQuery W (ProviderId, ServiceId, TeamId, BotId) () - cqlTeam = - "DELETE FROM service_team \ - \WHERE provider = ? AND service = ? AND team = ? AND user = ?" - updateStatus :: (MonadClient m) => UserId -> AccountStatus -> m () updateStatus u s = retry x5 $ write userStatusUpdate (params LocalQuorum (s, u)) @@ -272,42 +248,6 @@ lookupUsers hpi usrs = do domain <- viewFederationDomain toUsers domain loc hpi <$> retry x1 (query usersSelect (params LocalQuorum (Identity usrs))) -lookupServiceUser :: (MonadClient m) => ProviderId -> ServiceId -> BotId -> m (Maybe (ConvId, Maybe TeamId)) -lookupServiceUser pid sid bid = retry x1 (query1 cql (params LocalQuorum (pid, sid, bid))) - where - cql :: PrepQuery R (ProviderId, ServiceId, BotId) (ConvId, Maybe TeamId) - cql = - "SELECT conv, team FROM service_user \ - \WHERE provider = ? AND service = ? AND user = ?" - --- | NB: might return a lot of users, and therefore we do streaming here (page-by-page). -lookupServiceUsers :: - (MonadClient m) => - ProviderId -> - ServiceId -> - ConduitM () [(BotId, ConvId, Maybe TeamId)] m () -lookupServiceUsers pid sid = - paginateC cql (paramsP LocalQuorum (pid, sid) 100) x1 - where - cql :: PrepQuery R (ProviderId, ServiceId) (BotId, ConvId, Maybe TeamId) - cql = - "SELECT user, conv, team FROM service_user \ - \WHERE provider = ? AND service = ?" - -lookupServiceUsersForTeam :: - (MonadClient m) => - ProviderId -> - ServiceId -> - TeamId -> - ConduitM () [(BotId, ConvId)] m () -lookupServiceUsersForTeam pid sid tid = - paginateC cql (paramsP LocalQuorum (pid, sid, tid) 100) x1 - where - cql :: PrepQuery R (ProviderId, ServiceId, TeamId) (BotId, ConvId) - cql = - "SELECT user, conv FROM service_team \ - \WHERE provider = ? AND service = ? AND team = ?" - lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe FeatureStatus) lookupFeatureConferenceCalling uid = do let q = query1 select (params LocalQuorum (Identity uid)) diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index db71348d65e..14bc9dfafe9 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -39,6 +39,7 @@ import Wire.AuthenticationSubsystem import Wire.Events (Events) import Wire.NotificationSubsystem import Wire.PropertySubsystem +import Wire.Sem.Concurrency import Wire.Sem.Delay import Wire.UserGroupSubsystem import Wire.UserKeyStore @@ -61,7 +62,8 @@ onEvent :: Member UserSubsystem r, Member Events r, Member AuthenticationSubsystem r, - Member UserGroupSubsystem r + Member UserGroupSubsystem r, + Member (Concurrency Unsafe) r ) => InternalNotification -> Sem r () @@ -85,7 +87,7 @@ onEvent n = handleTimeout $ case n of msg (val "Processing service delete event") ~~ field "provider" (toByteString pid) ~~ field "service" (toByteString sid) - embed $ API.finishDeleteService pid sid + API.finishDeleteService pid sid where handleTimeout act = timeout (pure ()) (Seconds 60) act >>= \case diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 86c7ffb13e8..caae071cfe7 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -27,8 +27,6 @@ module Brig.Provider.API ) where -import Bilge.IO (MonadHttp) -import Bilge.RPC (HasRequestId) import Brig.API.Client qualified as Client import Brig.API.Error import Brig.API.Handler @@ -42,11 +40,10 @@ import Brig.Provider.DB (ServiceConn (..)) import Brig.Provider.DB qualified as DB import Brig.Provider.Email import Brig.Provider.RPC qualified as RPC -import Cassandra (MonadClient) +import Cassandra.Exec (paginateWithStateC) import Control.Error (throwE) import Control.Exception.Enclosed (handleAny) import Control.Lens ((^.)) -import Control.Monad.Catch (MonadMask) import Control.Monad.Except import Data.ByteString.Conversion import Data.ByteString.Lazy.Char8 qualified as LC8 @@ -89,8 +86,6 @@ import Polysemy.Error import Polysemy.Input import Servant (ServerT, (:<|>) (..)) import Ssl.Util qualified as SSL -import System.Logger.Class (MonadLogger) -import UnliftIO.Async (pooledMapConcurrentlyN_) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Bot import Wire.API.Conversation.Bot qualified as Public @@ -134,7 +129,7 @@ import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.HashPassword (HashPassword) import Wire.HashPassword qualified as HashPassword import Wire.RateLimit -import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) +import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe), unsafePooledMapConcurrentlyN_) import Wire.Sem.Now (Now) import Wire.StoredUser import Wire.TeamSubsystem (TeamSubsystem) @@ -155,7 +150,8 @@ botAPI :: Member (Input AuthenticationSubsystemConfig) r, Member Now r, Member CryptoSign r, - Member UserStore r + Member UserStore r, + Member (Embed HttpClientIO) r ) => ServerT BotAPI (Handler r) botAPI = @@ -179,7 +175,10 @@ servicesAPI :: Member AuthenticationSubsystem r, Member DeleteQueue r, Member (Error UserSubsystemError) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Concurrency Unsafe) r, + Member (Embed HttpClientIO) r, + Member UserStore r ) => ServerT ServicesAPI (Handler r) servicesAPI = @@ -576,27 +575,23 @@ deleteService pid sid del = do lift . liftSem $ enqueueServiceDeletion pid sid finishDeleteService :: - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m, - MonadClient m, - MonadUnliftIO m + ( Member UserStore r, + Member (Embed HttpClientIO) r, + Member (Concurrency Unsafe) r ) => ProviderId -> ServiceId -> - m () + Sem r () finishDeleteService pid sid = do - mbSvc <- DB.lookupService pid sid + mbSvc <- embed $ DB.lookupService pid sid for_ mbSvc $ \svc -> do let tags = unsafeRange (serviceTags svc) name = serviceName svc runConduit $ - User.lookupServiceUsers pid sid - .| C.mapM_ (pooledMapConcurrentlyN_ 16 kick) - RPC.removeServiceConn pid sid - DB.deleteService pid sid name tags + paginateWithStateC (UserStore.lookupServiceUsers pid sid) + .| C.mapM_ (unsafePooledMapConcurrentlyN_ 16 kick) + embed $ RPC.removeServiceConn pid sid + embed $ DB.deleteService pid sid name tags where kick (bid, cid, _) = deleteBot (botUserId bid) Nothing bid cid @@ -690,7 +685,10 @@ getServiceTagList _ = do updateServiceWhitelist :: ( Member GalleyAPIAccess r, Member TeamSubsystem r, - Member (Error UserSubsystemError) r + Member (Error UserSubsystemError) r, + Member (Concurrency Unsafe) r, + Member (Embed HttpClientIO) r, + Member UserStore r ) => UserId -> ConnId -> @@ -717,13 +715,10 @@ updateServiceWhitelist uid con tid upd = do (True, False) -> do -- When the service is de-whitelisted, remove its bots from team -- conversations - lift - $ fmap - wrapHttpClient - runConduit - $ User.lookupServiceUsersForTeam pid sid tid + lift . liftSem . runConduit $ + paginateWithStateC (UserStore.lookupServiceUsersForTeam pid sid tid) .| C.mapM_ - ( pooledMapConcurrentlyN_ + ( unsafePooledMapConcurrentlyN_ 16 (uncurry (deleteBot uid (Just con))) ) @@ -867,7 +862,7 @@ addBot zuid zcon cid add = do Public.rsAddBotEvent = ev } -removeBot :: (Member GalleyAPIAccess r) => UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) +removeBot :: (Member GalleyAPIAccess r, Member (Embed HttpClientIO) r, Member UserStore r) => UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) removeBot zusr zcon cid bid = do guardSecondFactorDisabled (Just zusr) -- Get the conversation and check preconditions @@ -888,7 +883,7 @@ removeBot zusr zcon cid bid = do case bot >>= omService of Nothing -> pure Nothing Just _ -> do - lift $ Public.RemoveBotResponse <$$> wrapHttpClient (deleteBot zusr (Just zcon) bid cid) + lift . liftSem $ Public.RemoveBotResponse <$$> deleteBot zusr (Just zcon) bid cid guardConvAdmin :: OwnConversation -> ExceptT HttpError (AppT r) () guardConvAdmin conv = do @@ -952,12 +947,12 @@ botGetUserClients _ uid = do where pubClient c = Public.PubClient c.clientId c.clientClass -botDeleteSelf :: (Member GalleyAPIAccess r) => BotId -> ConvId -> (Handler r) () +botDeleteSelf :: (Member GalleyAPIAccess r, Member (Embed HttpClientIO) r, Member UserStore r) => BotId -> ConvId -> (Handler r) () botDeleteSelf bid cid = do guardSecondFactorDisabled (Just (botUserId bid)) bot <- lift . wrapClient $ User.lookupUser NoPendingInvitations (botUserId bid) _ <- maybe (throwStd (errorToWai @'E.InvalidBot)) pure $ (userService =<< bot) - _ <- lift $ wrapHttpClient $ deleteBot (botUserId bid) Nothing bid cid + _ <- lift . liftSem $ deleteBot (botUserId bid) Nothing bid cid pure () -------------------------------------------------------------------------------- @@ -989,33 +984,26 @@ activate pid old new = do wrapClientE $ DB.insertKey pid (mkEmailKey <$> old) emailKey deleteBot :: - ( MonadHttp m, - MonadReader Env m, - MonadMask m, - MonadUnliftIO m, - HasRequestId m, - MonadLogger m, - MonadClient m - ) => + (Member (Embed HttpClientIO) r, Member UserStore r) => UserId -> Maybe ConnId -> BotId -> ConvId -> - m (Maybe Public.Event) + Sem r (Maybe Public.Event) deleteBot zusr zcon bid cid = do -- Remove the bot from the conversation - ev <- RPC.removeBotMember zusr zcon cid bid + ev <- embed $ RPC.removeBotMember zusr zcon cid bid -- Delete the bot user and client let buid = botUserId bid - mbUser <- User.lookupUser NoPendingInvitations buid - User.lookupClients buid >>= mapM_ (User.rmClient buid . (.clientId)) + mbUser <- embed $ User.lookupUser NoPendingInvitations buid + embed $ User.lookupClients buid >>= mapM_ (User.rmClient buid . (.clientId)) for_ (userService =<< mbUser) $ \sref -> do let pid = sref ^. serviceRefProvider sid = sref ^. serviceRefId - User.deleteServiceUser pid sid bid + UserStore.deleteServiceUser pid sid bid -- TODO: Consider if we can actually delete the bot user entirely, -- i.e. not just marking the account as deleted. - void $ runExceptT $ User.updateStatus buid Deleted + void . embed . runExceptT $ User.updateStatus buid Deleted pure ev validateServiceKey :: (MonadIO m) => Public.ServiceKeyPEM -> m (Maybe (Public.ServiceKey, Fingerprint Rsa)) From a0f07d9cda8f3d564fe79ef4e57a37bef99fb7f7 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 15 Jan 2026 17:10:20 +0100 Subject: [PATCH 02/16] UserStore: Move a few update functions from brig --- libs/wire-subsystems/src/Wire/UserStore.hs | 10 +++- .../src/Wire/UserStore/Cassandra.hs | 58 ++++++++++++++++++ .../unit/Wire/MockInterpreters/UserStore.hs | 30 ++++++++++ services/brig/src/Brig/API/Auth.hs | 12 ++-- services/brig/src/Brig/API/Internal.hs | 33 ++++++----- services/brig/src/Brig/API/Public.hs | 6 +- services/brig/src/Brig/API/User.hs | 55 +++++++++-------- services/brig/src/Brig/Data/Activation.hs | 9 ++- services/brig/src/Brig/Data/User.hs | 59 ------------------- services/brig/src/Brig/Provider/API.hs | 2 +- services/brig/src/Brig/Team/API.hs | 18 +++--- services/brig/src/Brig/User/Auth.hs | 15 +++-- 12 files changed, 183 insertions(+), 124 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index f8d8fe30735..693ed42a19b 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -28,6 +28,7 @@ import Imports import Polysemy import Polysemy.Error import Wire.API.Password +import Wire.API.Team.Feature (FeatureStatus) import Wire.API.User import Wire.API.User.RichInfo import Wire.API.User.Search (SetSearchable) @@ -73,8 +74,14 @@ data UserStore m a where GetIndexUsersPaginated :: Int32 -> Maybe PagingState -> UserStore m (PageWithState IndexUser) GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () + UpdateEmail :: UserId -> EmailAddress -> UserStore m () + DeleteEmail :: UserId -> UserStore m () UpdateEmailUnvalidated :: UserId -> EmailAddress -> UserStore m () + DeleteEmailUnvalidated :: UserId -> UserStore m () UpdateUserHandleEither :: UserId -> StoredUserHandleUpdate -> UserStore m (Either StoredUserUpdateError ()) + UpdateSSOId :: UserId -> Maybe UserSSOId -> UserStore m Bool + UpdateManagedBy :: UserId -> ManagedBy -> UserStore m () + UpdateAccountStatus :: UserId -> AccountStatus -> UserStore m () DeleteUser :: User -> UserStore m () -- | This operation looks up a handle but is guaranteed to not give you stale locks. -- It is potentially slower and less resilient than 'GlimpseHandle'. @@ -94,9 +101,10 @@ data UserStore m a where UpdateUserTeam :: UserId -> TeamId -> UserStore m () GetActivityTimestamps :: UserId -> UserStore m [Maybe UTCTime] GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) + UpdateRichInfo :: UserId -> RichInfoAssocList -> UserStore m () GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) - DeleteEmail :: UserId -> UserStore m () SetUserSearchable :: UserId -> SetSearchable -> UserStore m () + UpdateFeatureConferenceCalling :: UserId -> Maybe FeatureStatus -> UserStore m () DeleteServiceUser :: ProviderId -> ServiceId -> BotId -> UserStore m () LookupServiceUsers :: ProviderId -> ServiceId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId, Maybe TeamId)) LookupServiceUsersForTeam :: ProviderId -> ServiceId -> TeamId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId)) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index fb32e6c8053..846445ca3b5 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -30,6 +30,7 @@ import Polysemy.Embed import Polysemy.Error import Wire.API.Password (Password) import Wire.API.Provider.Service +import Wire.API.Team.Feature (FeatureStatus) import Wire.API.User hiding (DeleteUser) import Wire.API.User.RichInfo import Wire.API.User.Search (SetSearchable (SetSearchable)) @@ -47,8 +48,15 @@ interpretUserStoreCassandra casClient = GetIndexUser uid -> getIndexUserImpl uid GetIndexUsersPaginated pageSize mPagingState -> getIndexUserPaginatedImpl pageSize mPagingState UpdateUser uid update -> updateUserImpl uid update + UpdateEmail uid email -> updateEmailImpl uid email UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid email + DeleteEmailUnvalidated uid -> deleteEmailUnvalidatedImpl uid UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update + UpdateSSOId uid ssoId -> updateSSOIdImpl uid ssoId + UpdateManagedBy uid managedBy -> updateManagedByImpl uid managedBy + UpdateAccountStatus uid accountStatus -> updateAccountStatusImpl uid accountStatus + UpdateRichInfo uid richInfo -> updateRichInfoImpl uid richInfo + UpdateFeatureConferenceCalling uid feat -> updateFeatureConferenceCallingImpl uid feat DeleteUser user -> deleteUserImpl user LookupHandle hdl -> lookupHandleImpl LocalQuorum hdl GlimpseHandle hdl -> lookupHandleImpl One hdl @@ -146,6 +154,12 @@ updateUserImpl uid update = for_ update.accentId \c -> addPrepQuery userAccentIdUpdate (c, uid) for_ update.supportedProtocols \a -> addPrepQuery userSupportedProtocolsUpdate (a, uid) +updateEmailImpl :: UserId -> EmailAddress -> Client () +updateEmailImpl u e = retry x5 $ write userEmailUpdate (params LocalQuorum (e, u)) + where + userEmailUpdate :: PrepQuery W (EmailAddress, UserId) () + userEmailUpdate = "UPDATE user SET email = ? WHERE id = ?" + updateEmailUnvalidatedImpl :: UserId -> EmailAddress -> Client () updateEmailUnvalidatedImpl u e = retry x5 $ write userEmailUnvalidatedUpdate (params LocalQuorum (e, u)) @@ -153,12 +167,56 @@ updateEmailUnvalidatedImpl u e = userEmailUnvalidatedUpdate :: PrepQuery W (EmailAddress, UserId) () userEmailUnvalidatedUpdate = "UPDATE user SET email_unvalidated = ? WHERE id = ?" +deleteEmailUnvalidatedImpl :: UserId -> Client () +deleteEmailUnvalidatedImpl u = retry x5 $ write userEmailUnvalidatedDelete (params LocalQuorum (Identity u)) + where + userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () + userEmailUnvalidatedDelete = "UPDATE user SET email_unvalidated = null WHERE id = ?" + updateUserHandleEitherImpl :: UserId -> StoredUserHandleUpdate -> Client (Either StoredUserUpdateError ()) updateUserHandleEitherImpl uid update = runM $ runError do claimed <- embed $ claimHandleImpl uid update.old update.new unless claimed $ throw StoredUserUpdateHandleExists +updateSSOIdImpl :: UserId -> Maybe UserSSOId -> Client Bool +updateSSOIdImpl u ssoid = do + mteamid <- getUserTeamImpl u + case mteamid of + Just _ -> do + retry x5 $ write userSSOIdUpdate (params LocalQuorum (ssoid, u)) + pure True + Nothing -> pure False + where + userSSOIdUpdate :: PrepQuery W (Maybe UserSSOId, UserId) () + userSSOIdUpdate = "UPDATE user SET sso_id = ? WHERE id = ?" + +updateManagedByImpl :: UserId -> ManagedBy -> Client () +updateManagedByImpl u h = retry x5 $ write userManagedByUpdate (params LocalQuorum (h, u)) + where + userManagedByUpdate :: PrepQuery W (ManagedBy, UserId) () + userManagedByUpdate = "UPDATE user SET managed_by = ? WHERE id = ?" + +updateAccountStatusImpl :: UserId -> AccountStatus -> Client () +updateAccountStatusImpl u s = + retry x5 $ write userStatusUpdate (params LocalQuorum (s, u)) + where + userStatusUpdate :: PrepQuery W (AccountStatus, UserId) () + userStatusUpdate = "UPDATE user SET status = ? WHERE id = ?" + +updateRichInfoImpl :: (MonadClient m) => UserId -> RichInfoAssocList -> m () +updateRichInfoImpl u ri = retry x5 $ write userRichInfoUpdate (params LocalQuorum (ri, u)) + where + userRichInfoUpdate :: PrepQuery W (RichInfoAssocList, UserId) () + userRichInfoUpdate = "UPDATE rich_info SET json = ? WHERE user = ?" + +updateFeatureConferenceCallingImpl :: (MonadClient m) => UserId -> Maybe FeatureStatus -> m () +updateFeatureConferenceCallingImpl uid mStatus = + retry x5 $ write update (params LocalQuorum (mStatus, uid)) + where + update :: PrepQuery W (Maybe FeatureStatus, UserId) () + update = fromString "update user set feature_conference_calling = ? where id = ?" + -- | Claim a new handle for an existing 'User': validate it, and in case of success, assign it -- to user and mark it as taken. claimHandleImpl :: UserId -> Maybe Handle -> Handle -> Client Bool diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index d0725865967..7de0daeeb6f 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -56,6 +56,13 @@ inMemoryUserStoreInterpreter = interpret $ \case . maybe Imports.id setStoredUserSupportedProtocols update.supportedProtocols $ u else u + UpdateEmail uid email -> modify (map doUpdate) + where + doUpdate :: StoredUser -> StoredUser + doUpdate u = + if u.id == uid + then u {email = Just email} :: StoredUser + else u UpdateEmailUnvalidated uid email -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser @@ -63,6 +70,20 @@ inMemoryUserStoreInterpreter = interpret $ \case if u.id == uid then u {emailUnvalidated = Just email} :: StoredUser else u + DeleteEmailUnvalidated uid -> modify (map doUpdate) + where + doUpdate :: StoredUser -> StoredUser + doUpdate u = + if u.id == uid + then u {emailUnvalidated = Nothing} :: StoredUser + else u + UpdateSSOId uid ssoId -> do + updateUserInStore uid (\u -> u {ssoId = ssoId}) + gets (any (\u -> u.id == uid)) + UpdateManagedBy uid managedBy -> updateUserInStore uid (\u -> u {managedBy = Just managedBy}) + UpdateAccountStatus uid accountStatus -> updateUserInStore uid (\u -> u {status = Just accountStatus}) + UpdateRichInfo {} -> error "UpdateRichInfo: Not implemented" + UpdateFeatureConferenceCalling {} -> error "UpdateFeatureConferenceCalling: Not implemented" GetIndexUser uid -> do mUser <- gets @[StoredUser] $ find (\user -> user.id == uid) pure $ storedUserToIndexUser <$> mUser @@ -192,3 +213,12 @@ newStoredUserToStoredUser new = supportedProtocols = Just new.supportedProtocols, searchable = Just new.searchable } + +updateUserInStore :: (Member (State [StoredUser]) r) => UserId -> (StoredUser -> StoredUser) -> Sem r () +updateUserInStore uid f = modify (map doUpdate) + where + doUpdate :: StoredUser -> StoredUser + doUpdate u = + if u.id == uid + then f u + else u diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 7c26b749889..962a8d79b98 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -86,7 +86,8 @@ accessH :: Member CryptoSign r, Member Now r, Member AuthenticationSubsystem r, - Member Random r + Member Random r, + Member UserStore r ) => Maybe ClientId -> [Either Text SomeUserToken] -> @@ -113,7 +114,8 @@ access :: Member CryptoSign r, Member Now r, Member AuthenticationSubsystem r, - Member Random r + Member Random r, + Member UserStore r ) => Maybe ClientId -> NonEmpty (Token u) -> @@ -247,7 +249,8 @@ legalHoldLogin :: Member (Concurrency Unsafe) r, Member Now r, Member CryptoSign r, - Member Random r + Member Random r, + Member UserStore r ) => LegalHoldLogin -> Handler r SomeAccess @@ -265,7 +268,8 @@ ssoLogin :: Member (Concurrency Unsafe) r, Member Now r, Member CryptoSign r, - Member Random r + Member Random r, + Member UserStore r ) => SsoLogin -> Maybe Bool -> diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index fc0d6a62e90..864b6f769b9 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -295,7 +295,6 @@ teamsAPI :: ( Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member BlockListStore r, - Member (Embed HttpClientIO) r, Member UserKeyStore r, Member UserStore r, Member (Concurrency 'Unsafe) r, @@ -342,7 +341,8 @@ authAPI :: Member (Concurrency Unsafe) r, Member Now r, Member CryptoSign r, - Member Random r + Member Random r, + Member UserStore r ) => ServerT BrigIRoutes.AuthAPI (Handler r) authAPI = @@ -441,13 +441,13 @@ getAccountConferenceCallingConfig uid = do mDefStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNull) pure $ def {status = mStatus <|> mDefStatus ?: (def :: LockableFeature ConferenceCallingConfig).status} -putAccountConferenceCallingConfig :: UserId -> Feature ConferenceCallingConfig -> Handler r NoContent +putAccountConferenceCallingConfig :: (Member UserStore r) => UserId -> Feature ConferenceCallingConfig -> Handler r NoContent putAccountConferenceCallingConfig uid feat = do - lift $ wrapClient $ Data.updateFeatureConferenceCalling uid (Just feat.status) $> NoContent + lift . liftSem $ UserStore.updateFeatureConferenceCalling uid (Just feat.status) $> NoContent -deleteAccountConferenceCallingConfig :: UserId -> Handler r NoContent +deleteAccountConferenceCallingConfig :: (Member UserStore r) => UserId -> Handler r NoContent deleteAccountConferenceCallingConfig uid = - lift $ wrapClient $ Data.updateFeatureConferenceCalling uid Nothing $> NoContent + lift . liftSem $ UserStore.updateFeatureConferenceCalling uid Nothing $> NoContent getMLSClientH :: UserId -> ClientId -> CipherSuite -> Handler r ClientInfo getMLSClientH usr cid suite = do @@ -789,7 +789,8 @@ changeAccountStatusH :: ( Member UserSubsystem r, Member Events r, Member (Concurrency Unsafe) r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserStore r ) => UserId -> AccountStatusUpdate -> @@ -862,13 +863,14 @@ addBlacklist email = lift $ NoContent <$ API.blacklistInsert email updateSSOIdH :: ( Member UserSubsystem r, - Member Events r + Member Events r, + Member UserStore r ) => UserId -> UserSSOId -> (Handler r) UpdateSSOIdResponse updateSSOIdH uid ssoid = lift $ do - success <- wrapClient $ Data.updateSSOId uid (Just ssoid) + success <- liftSem $ UserStore.updateSSOId uid (Just ssoid) liftSem $ if success then do @@ -879,12 +881,13 @@ updateSSOIdH uid ssoid = lift $ do deleteSSOIdH :: ( Member UserSubsystem r, - Member Events r + Member Events r, + Member UserStore r ) => UserId -> (Handler r) UpdateSSOIdResponse deleteSSOIdH uid = lift $ do - success <- wrapClient $ Data.updateSSOId uid Nothing + success <- liftSem $ UserStore.updateSSOId uid Nothing if success then liftSem $ do UserSubsystem.internalUpdateSearchIndex uid @@ -892,11 +895,11 @@ deleteSSOIdH uid = lift $ do pure UpdateSSOIdSuccess else pure UpdateSSOIdNotFound -updateManagedByH :: UserId -> ManagedByUpdate -> (Handler r) NoContent +updateManagedByH :: (Member UserStore r) => UserId -> ManagedByUpdate -> (Handler r) NoContent updateManagedByH uid (ManagedByUpdate managedBy) = do - NoContent <$ lift (wrapClient $ Data.updateManagedBy uid managedBy) + NoContent <$ lift (liftSem $ UserStore.updateManagedBy uid managedBy) -updateRichInfoH :: UserId -> RichInfoUpdate -> (Handler r) NoContent +updateRichInfoH :: (Member UserStore r) => UserId -> RichInfoUpdate -> (Handler r) NoContent updateRichInfoH uid rup = NoContent <$ do let (unRichInfoAssocList -> richInfo) = normalizeRichInfoAssocList . riuRichInfo $ rup @@ -904,7 +907,7 @@ updateRichInfoH uid rup = when (richInfoSize (RichInfo (mkRichInfoAssocList richInfo)) > maxSize) $ throwStd tooLargeRichInfo -- FUTUREWORK: send an event -- Intra.onUserEvent uid (Just conn) (richInfoUpdate uid ri) - lift $ wrapClient $ Data.updateRichInfo uid (mkRichInfoAssocList richInfo) + lift $ liftSem $ UserStore.updateRichInfo uid (mkRichInfoAssocList richInfo) updateLocale :: (Member UserSubsystem r) => UserId -> LocaleUpdate -> (Handler r) LocaleUpdate updateLocale uid upd@(LocaleUpdate locale) = do diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 5a206da8335..aea927fa62f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1504,7 +1504,8 @@ activate :: Member TinyLog r, Member UserSubsystem r, Member Events r, - Member PasswordResetCodeStore r + Member PasswordResetCodeStore r, + Member UserStore r ) => Public.ActivationKey -> Public.ActivationCode -> @@ -1519,7 +1520,8 @@ activateKey :: Member TinyLog r, Member Events r, Member UserSubsystem r, - Member PasswordResetCodeStore r + Member PasswordResetCodeStore r, + Member UserStore r ) => Public.Activate -> (Handler r) ActivationRespWithStatus diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 8ef4f403374..9ee3b90e577 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -214,17 +214,17 @@ createUserSpar new = do account <- lift $ newStoredUser new' Nothing (Just tid) handle' domain <- viewFederationDomain let u = newStoredUserToUser (Qualified account domain) - lift $ do + lift . liftSem $ do let uid = account.id -- FUTUREWORK: make this transactional if possible - liftSem $ UserStore.createUser account Nothing + UserStore.createUser account Nothing case unRichInfo <$> newUserSparRichInfo new of - Just richInfo -> wrapClient $ Data.updateRichInfo uid richInfo + Just richInfo -> UserStore.updateRichInfo uid richInfo Nothing -> pure () -- Nothing to do - liftSem $ GalleyAPIAccess.createSelfConv uid - liftSem $ User.internalUpdateSearchIndex uid - liftSem $ Events.generateUserEvent uid Nothing (UserCreated u) + GalleyAPIAccess.createSelfConv uid + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (UserCreated u) -- Add to team userTeam <- withExceptT CreateUserSparRegistrationError $ addUserToTeamSSO u tid (SSOIdentity ident Nothing) (newUserSparRole new) @@ -524,10 +524,10 @@ createUser rateLimitKey new = do !>> activationErrorToRegisterError pure Nothing -initAccountFeatureConfig :: UserId -> (AppT r) () +initAccountFeatureConfig :: (Member UserStore r) => UserId -> (AppT r) () initAccountFeatureConfig uid = do mStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNew . _Just) - wrapClient $ traverse_ (Data.updateFeatureConferenceCalling uid . Just) mStatus + liftSem $ traverse_ (UserStore.updateFeatureConferenceCalling uid . Just) mStatus -- | 'createUser' is becoming hard to maintain, and instead of adding more case distinctions -- all over the place there, we add a new function that handles just the one new flow where @@ -595,11 +595,11 @@ revokeIdentity key = do changeAccountStatus :: forall r. - ( Member (Embed HttpClientIO) r, - Member (Concurrency 'Unsafe) r, + ( Member (Concurrency 'Unsafe) r, Member UserSubsystem r, Member Events r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserStore r ) => NonEmpty UserId -> AccountStatus -> @@ -613,7 +613,7 @@ changeAccountStatus usrs status = do UserId -> Sem r () update ev u = do - embed $ Data.updateStatus u status + UserStore.updateAccountStatus u status User.internalUpdateSearchIndex u Events.generateUserEvent u Nothing (ev u) @@ -621,7 +621,8 @@ changeSingleAccountStatus :: ( Member UserSubsystem r, Member Events r, Member (Concurrency Unsafe) r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserStore r ) => UserId -> AccountStatus -> @@ -629,10 +630,10 @@ changeSingleAccountStatus :: changeSingleAccountStatus uid status = do unlessM (wrapClientE $ Data.userExists uid) $ throwE AccountNotFound ev <- mkUserEvent (NonEmpty.singleton uid) status - lift $ do - wrapClient $ Data.updateStatus uid status - liftSem $ User.internalUpdateSearchIndex uid - liftSem $ Events.generateUserEvent uid Nothing (ev uid) + lift . liftSem $ do + UserStore.updateAccountStatus uid status + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (ev uid) mkUserEvent :: ( Traversable t, @@ -660,7 +661,8 @@ activate :: Member TinyLog r, Member Events r, Member PasswordResetCodeStore r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r ) => ActivationTarget -> ActivationCode -> @@ -674,7 +676,8 @@ activateNoVerifyEmailDomain :: Member TinyLog r, Member Events r, Member PasswordResetCodeStore r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r ) => ActivationTarget -> ActivationCode -> @@ -688,7 +691,8 @@ activateWithCurrency :: Member TinyLog r, Member Events r, Member PasswordResetCodeStore r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r ) => Bool -> ActivationTarget -> @@ -735,7 +739,8 @@ preverify tgt code = do onActivated :: ( Member TinyLog r, Member UserSubsystem r, - Member Events r + Member Events r, + Member UserStore r ) => ActivationEvent -> AppT r (UserId, Maybe UserIdentity, Bool) @@ -746,10 +751,10 @@ onActivated (AccountActivated account) = liftSem $ do User.internalUpdateSearchIndex uid Events.generateUserEvent uid Nothing $ UserActivated account pure (uid, userIdentity account, True) -onActivated (EmailActivated uid email) = do - liftSem $ User.internalUpdateSearchIndex uid - liftSem $ Events.generateUserEvent uid Nothing (emailUpdated uid email) - wrapHttpClient $ Data.deleteEmailUnvalidated uid +onActivated (EmailActivated uid email) = liftSem $ do + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (emailUpdated uid email) + UserStore.deleteEmailUnvalidated uid pure (uid, Just (EmailIdentity email), False) -- docs/reference/user/activation.md {#RefActivationRequest} diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 9d7cbe913fb..6e548e72b0a 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -26,7 +26,7 @@ module Brig.Data.Activation ) where -import Brig.App (AppT, adhocUserKeyStoreInterpreter, liftSem, qualifyLocal, wrapClient, wrapClientE) +import Brig.App (AppT, adhocUserKeyStoreInterpreter, liftSem, qualifyLocal, wrapClientE) import Brig.Data.User import Brig.Types.Intra import Cassandra @@ -44,6 +44,8 @@ import Wire.API.User.Password import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordResetCodeStore qualified as Password import Wire.UserKeyStore +import Wire.UserStore (UserStore) +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem import Wire.UserSubsystem qualified as User @@ -71,7 +73,8 @@ data ActivationEvent activateKey :: forall r. ( Member UserSubsystem r, - Member PasswordResetCodeStore r + Member PasswordResetCodeStore r, + Member UserStore r ) => ActivationKey -> ActivationCode -> @@ -125,7 +128,7 @@ activateKey k c u = do where updateEmailAndDeleteEmailUnvalidated :: UserId -> EmailAddress -> AppT r () updateEmailAndDeleteEmailUnvalidated u' email = - wrapClient (updateEmail u' email <* deleteEmailUnvalidated u') + liftSem (UserStore.updateEmail u' email <* UserStore.deleteEmailUnvalidated u') claim :: EmailKey -> UserId -> ExceptT ActivationError (AppT r) () claim key uid = do diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 39a091c53f2..d3d35eb5469 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -33,17 +33,8 @@ module Brig.Data.User userExists, -- * Updates - updateEmail, - updateSSOId, - updateManagedBy, activateUser, deactivateUser, - updateStatus, - updateRichInfo, - updateFeatureConferenceCalling, - - -- * Deletions - deleteEmailUnvalidated, ) where @@ -173,38 +164,6 @@ newStoredUserViaScim uid externalId tid locale name email = do searchable = True } -updateEmail :: (MonadClient m) => UserId -> EmailAddress -> m () -updateEmail u e = retry x5 $ write userEmailUpdate (params LocalQuorum (e, u)) - -updateSSOId :: (MonadClient m) => UserId -> Maybe UserSSOId -> m Bool -updateSSOId u ssoid = do - mteamid <- lookupUserTeam u - case mteamid of - Just _ -> do - retry x5 $ write userSSOIdUpdate (params LocalQuorum (ssoid, u)) - pure True - Nothing -> pure False - -updateManagedBy :: (MonadClient m) => UserId -> ManagedBy -> m () -updateManagedBy u h = retry x5 $ write userManagedByUpdate (params LocalQuorum (h, u)) - -updateRichInfo :: (MonadClient m) => UserId -> RichInfoAssocList -> m () -updateRichInfo u ri = retry x5 $ write userRichInfoUpdate (params LocalQuorum (ri, u)) - -updateFeatureConferenceCalling :: (MonadClient m) => UserId -> Maybe FeatureStatus -> m () -updateFeatureConferenceCalling uid mStatus = - retry x5 $ write update (params LocalQuorum (mStatus, uid)) - where - update :: PrepQuery W (Maybe FeatureStatus, UserId) () - update = fromString "update user set feature_conference_calling = ? where id = ?" - -deleteEmailUnvalidated :: (MonadClient m) => UserId -> m () -deleteEmailUnvalidated u = retry x5 $ write userEmailUnvalidatedDelete (params LocalQuorum (Identity u)) - -updateStatus :: (MonadClient m) => UserId -> AccountStatus -> m () -updateStatus u s = - retry x5 $ write userStatusUpdate (params LocalQuorum (s, u)) - userExists :: (MonadClient m) => UserId -> m Bool userExists uid = isJust <$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) @@ -306,30 +265,12 @@ richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" teamSelect :: PrepQuery R (Identity UserId) (Identity (Maybe TeamId)) teamSelect = "SELECT team FROM user WHERE id = ?" -userEmailUpdate :: PrepQuery W (EmailAddress, UserId) () -userEmailUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = ? WHERE id = ?" - -userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () -userEmailUnvalidatedDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email_unvalidated = null WHERE id = ?" - -userSSOIdUpdate :: PrepQuery W (Maybe UserSSOId, UserId) () -userSSOIdUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET sso_id = ? WHERE id = ?" - -userManagedByUpdate :: PrepQuery W (ManagedBy, UserId) () -userManagedByUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET managed_by = ? WHERE id = ?" - -userStatusUpdate :: PrepQuery W (AccountStatus, UserId) () -userStatusUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET status = ? WHERE id = ?" - userDeactivatedUpdate :: PrepQuery W (Identity UserId) () userDeactivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = false WHERE id = ?" userActivatedUpdate :: PrepQuery W (Maybe EmailAddress, UserId) () userActivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = true, email = ? WHERE id = ?" -userRichInfoUpdate :: PrepQuery W (RichInfoAssocList, UserId) () -userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE rich_info SET json = ? WHERE user = ?" - ------------------------------------------------------------------------------- -- Conversions diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index caae071cfe7..8bc49238593 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -1003,7 +1003,7 @@ deleteBot zusr zcon bid cid = do UserStore.deleteServiceUser pid sid bid -- TODO: Consider if we can actually delete the bot user entirely, -- i.e. not just marking the account as deleted. - void . embed . runExceptT $ User.updateStatus buid Deleted + UserStore.updateAccountStatus buid Deleted pure ev validateServiceKey :: (MonadIO m) => Public.ServiceKeyPEM -> m (Maybe (Public.ServiceKey, Fingerprint Rsa)) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 2ee190aa815..02f088177d5 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -365,15 +365,15 @@ getInvitationByEmail email = do maybe (throwStd (notFound "Invitation not found")) (pure . Store.invitationFromStored Nothing) inv suspendTeam :: - ( Member (Embed HttpClientIO) r, - Member (Concurrency 'Unsafe) r, + ( Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, Member UserSubsystem r, Member TeamSubsystem r, Member Events r, Member TinyLog r, Member InvitationStore r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserStore r ) => TeamId -> (Handler r) NoContent @@ -388,13 +388,13 @@ suspendTeam tid = do pure NoContent unsuspendTeam :: - ( Member (Embed HttpClientIO) r, - Member (Concurrency 'Unsafe) r, + ( Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, Member UserSubsystem r, Member TeamSubsystem r, Member Events r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserStore r ) => TeamId -> (Handler r) NoContent @@ -407,13 +407,13 @@ unsuspendTeam tid = do -- Internal changeTeamAccountStatuses :: - ( Member (Embed HttpClientIO) r, - Member (Concurrency 'Unsafe) r, + ( Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, Member TeamSubsystem r, Member UserSubsystem r, Member Events r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserStore r ) => TeamId -> AccountStatus -> diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index e83b96afd13..847e93114a0 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -233,7 +233,8 @@ renewAccess :: Member CryptoSign r, Member Now r, Member AuthenticationSubsystem r, - Member Random r + Member Random r, + Member UserStore r ) => NE.NonEmpty (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> @@ -277,7 +278,8 @@ catchSuspendInactiveUser :: Member UserSubsystem r, Member Events r, Member (Concurrency 'Unsafe) r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserStore r ) => UserId -> e -> @@ -311,7 +313,8 @@ newAccess :: Member Now r, Member AuthenticationSubsystem r, Member CryptoSign r, - Member Random r + Member Random r, + Member UserStore r ) => UserId -> Maybe ClientId -> @@ -435,7 +438,8 @@ ssoLogin :: Member (Concurrency Unsafe) r, Member Now r, Member CryptoSign r, - Member Random r + Member Random r, + Member UserStore r ) => SsoLogin -> CookieType -> @@ -473,7 +477,8 @@ legalHoldLogin :: Member (Concurrency Unsafe) r, Member Now r, Member CryptoSign r, - Member Random r + Member Random r, + Member UserStore r ) => LegalHoldLogin -> CookieType -> From 69572661aa21b871a2c76eb483b9526f6b144f8f Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 15 Jan 2026 17:44:18 +0100 Subject: [PATCH 03/16] Brig: Use UserSubsystem to get accounts instead of queries in Brig.Data.User --- services/brig/src/Brig/API/Client.hs | 5 +- services/brig/src/Brig/API/Internal.hs | 11 +- services/brig/src/Brig/API/Public.hs | 6 +- services/brig/src/Brig/API/User.hs | 7 +- services/brig/src/Brig/Data/User.hs | 142 +------------------------ services/brig/src/Brig/Provider/API.hs | 67 +++++++++--- services/brig/src/Brig/User/EJPD.hs | 16 +-- 7 files changed, 80 insertions(+), 174 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index c3ebf2899bd..4a24904c88a 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -54,7 +54,6 @@ import Brig.API.Util import Brig.App import Brig.Data.Client qualified as Data import Brig.Data.Nonce as Nonce -import Brig.Data.User qualified as Data import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.JwtTools qualified as JwtTools import Brig.Effects.PublicKeyBundle (PublicKeyBundle) @@ -571,7 +570,7 @@ removeLegalHoldClient uid = do liftSem $ Events.generateUserEvent uid Nothing (UserLegalHoldDisabled uid) createAccessToken :: - (Member JwtTools r, Member Now r, Member PublicKeyBundle r) => + (Member JwtTools r, Member Now r, Member PublicKeyBundle r, Member UserSubsystem r) => Local UserId -> ClientId -> StdMethod -> @@ -582,7 +581,7 @@ createAccessToken luid cid method link proof = do let domain = tDomain luid let uid = tUnqualified luid (tid, handle, displayName) <- do - mUser <- lift $ wrapClient (Data.lookupUser NoPendingInvitations uid) + mUser <- lift $ liftSem (User.getLocalAccountBy NoPendingInvitations luid) except $ (,,) <$> note NotATeamUser (userTeam =<< mUser) diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 864b6f769b9..0f4a716975a 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -133,6 +133,7 @@ import Wire.UserGroupSubsystem import Wire.UserKeyStore import Wire.UserStore as UserStore import Wire.UserSubsystem +import Wire.UserSubsystem qualified as User import Wire.UserSubsystem qualified as UserSubsystem import Wire.UserSubsystem.Error import Wire.UserSubsystem.UserSubsystemConfig @@ -207,7 +208,8 @@ ejpdAPI :: Member NotificationSubsystem r, Member UserStore r, Member Rpc r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member UserSubsystem r ) => ServerT BrigIRoutes.EJPDRequest (Handler r) ejpdAPI = Named @"ejpd-request" Brig.User.EJPD.ejpdRequest @@ -488,9 +490,10 @@ getMLSClient lusr cid suiteTag = do mlsSignatureKey = Map.lookup ss keys } -getVerificationCode :: forall r. (Member VerificationCodeSubsystem r) => UserId -> VerificationAction -> Handler r (Maybe Code.Value) +getVerificationCode :: forall r. (Member VerificationCodeSubsystem r, Member UserSubsystem r) => UserId -> VerificationAction -> Handler r (Maybe Code.Value) getVerificationCode uid action = runMaybeT do - user <- MaybeT . wrapClientE $ API.lookupUser NoPendingInvitations uid + luid <- qualifyLocal uid + user <- MaybeT . lift . liftSem $ User.getLocalAccountBy NoPendingInvitations luid email <- MaybeT . pure $ userEmail user let key = mkKey email code <- MaybeT . lift . liftSem $ internalLookupCode key (scopeFromAction action) @@ -971,7 +974,7 @@ updateUserNameH uid (NameUpdate nameUpd) = NoContent <$ do luid <- qualifyLocal uid name <- either (const $ throwStd (errorToWai @'E.InvalidUser)) pure $ mkName nameUpd - lift (wrapClient $ Data.lookupUser WithPendingInvitations uid) >>= \case + lift (liftSem $ User.getLocalAccountBy WithPendingInvitations luid) >>= \case Just _ -> lift . liftSem $ updateUserProfile luid Nothing UpdateOriginScim (def {name = Just name}) Nothing -> throwStd (errorToWai @'E.InvalidUser) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index aea927fa62f..99993015f40 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -885,7 +885,8 @@ createAccessToken :: Member PublicKeyBundle r, IsElem endpoint api, HasLink endpoint, - MkLink endpoint Link ~ (ClientId -> Link) + MkLink endpoint Link ~ (ClientId -> Link), + Member UserSubsystem r ) => StdMethod -> Local UserId -> @@ -1268,7 +1269,8 @@ sendActivationCode :: Member UserKeyStore r, Member ActivationCodeStore r, Member (Error UserSubsystemError) r, - Member (Input UserSubsystemConfig) r + Member (Input UserSubsystemConfig) r, + Member UserSubsystem r ) => Public.SendActivationCode -> Handler r () diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 9ee3b90e577..57286b7f571 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -32,7 +32,6 @@ module Brig.API.User changeSingleAccountStatus, getLegalHoldStatus, Data.lookupName, - Data.lookupUser, Data.lookupRichInfoMultiUsers, revokeIdentity, deleteUserNoVerify, @@ -766,7 +765,8 @@ sendActivationCode :: Member ActivationCodeStore r, Member UserKeyStore r, Member (Polysemy.Error.Error UserSubsystemError) r, - Member (Input UserSubsystemConfig) r + Member (Input UserSubsystemConfig) r, + Member UserSubsystem r ) => EmailAddress -> Maybe Locale -> @@ -808,7 +808,8 @@ sendActivationCode email loc = do sendActivationEmail ek uc uid = do -- FUTUREWORK(fisx): we allow for 'PendingInvitations' here, but I'm not sure this -- top-level function isn't another piece of a deprecated onboarding flow? - u <- maybe (notFound uid) pure =<< lift (wrapClient $ Data.lookupUser WithPendingInvitations uid) + luid <- qualifyLocal uid + u <- maybe (notFound uid) pure =<< lift (liftSem $ User.getLocalAccountBy WithPendingInvitations luid) (aKey, aCode) <- mkPair ek (Just uc) (Just uid) let ident = userIdentity u name = userDisplayName u diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index d3d35eb5469..207ab341fe2 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -24,8 +24,6 @@ module Brig.Data.User newStoredUserViaScim, -- * Lookups - lookupUser, - lookupUsers, lookupName, lookupRichInfoMultiUsers, lookupUserTeam, @@ -40,22 +38,17 @@ where import Brig.App import Brig.Options -import Brig.Types.Intra import Cassandra hiding (Set) import Control.Error import Control.Lens hiding (from) -import Data.Domain import Data.Handle (Handle) -import Data.HavePendingInvitations import Data.Id -import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) -import Data.Qualified +import Data.Json.Util (toUTCTimeMillis) import Data.Range (fromRange) import Data.Time (addUTCTime) import Data.UUID.V4 import Imports import Wire.API.Password -import Wire.API.Provider.Service import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.RichInfo @@ -167,9 +160,6 @@ newStoredUserViaScim uid externalId tid locale name email = do userExists :: (MonadClient m) => UserId -> m Bool userExists uid = isJust <$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) -lookupUser :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> UserId -> m (Maybe User) -lookupUser hpi u = listToMaybe <$> lookupUsers hpi [u] - activateUser :: (MonadClient m) => UserId -> UserIdentity -> m () activateUser u ident = do let email = emailIdentity ident @@ -198,15 +188,6 @@ lookupUserTeam u = (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) --- | Return users with given IDs. --- --- Skips nonexistent users. /Does not/ skip users who have been deleted. -lookupUsers :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> [UserId] -> m [User] -lookupUsers hpi usrs = do - loc <- defaultUserLocale <$> asks (.settings) - domain <- viewFederationDomain - toUsers domain loc hpi <$> retry x1 (query usersSelect (params LocalQuorum (Identity usrs))) - lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe FeatureStatus) lookupFeatureConferenceCalling uid = do let q = query1 select (params LocalQuorum (Identity uid)) @@ -218,41 +199,6 @@ lookupFeatureConferenceCalling uid = do ------------------------------------------------------------------------------- -- Queries -type Activated = Bool - --- UserRow is the same as AccountRow from the user subsystem. when migrating this code there, --- consider eliminating it instead. -type UserRow = - ( UserId, - Name, - Maybe TextStatus, - Maybe Pict, - Maybe EmailAddress, - Maybe EmailAddress, - Maybe UserSSOId, - ColourId, - Maybe [Asset], - Activated, - Maybe AccountStatus, - Maybe UTCTimeMillis, - Maybe Language, - Maybe Country, - Maybe ProviderId, - Maybe ServiceId, - Maybe Handle, - Maybe TeamId, - Maybe ManagedBy, - Maybe (Set BaseProtocolTag), - Maybe Bool - ) - -usersSelect :: PrepQuery R (Identity [UserId]) UserRow -usersSelect = - "SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, \ - \activated, status, expires, language, country, provider, service, \ - \handle, team, managed_by, supported_protocols, searchable \ - \FROM user where id IN ?" - idSelect :: PrepQuery R (Identity UserId) (Identity UserId) idSelect = "SELECT id FROM user WHERE id = ?" @@ -270,89 +216,3 @@ userDeactivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDAT userActivatedUpdate :: PrepQuery W (Maybe EmailAddress, UserId) () userActivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = true, email = ? WHERE id = ?" - -------------------------------------------------------------------------------- --- Conversions - -toUsers :: Domain -> Locale -> HavePendingInvitations -> [UserRow] -> [User] -toUsers domain defLocale havePendingInvitations = fmap mk . filter fp - where - fp :: UserRow -> Bool - fp = - case havePendingInvitations of - WithPendingInvitations -> const True - NoPendingInvitations -> - ( \( _uid, - _name, - _textStatus, - _pict, - _email, - _emailUnvalidated, - _ssoid, - _accent, - _assets, - _activated, - status, - _expires, - _lan, - _con, - _pid, - _sid, - _handle, - _tid, - _managed_by, - _prots, - _searchable - ) -> status /= Just PendingInvitation - ) - - mk :: UserRow -> User - mk - ( uid, - name, - textStatus, - pict, - email, - emailUnvalidated, - ssoid, - accent, - assets, - activated, - status, - expires, - lan, - con, - pid, - sid, - handle, - tid, - managed_by, - prots, - searchable - ) = - let ident = toIdentity activated email ssoid - expiration = if status == Just Ephemeral then expires else Nothing - loc = toLocaleWithDefault defLocale (lan, con) - svc = newServiceRef <$> sid <*> pid - in User - (Qualified uid domain) - ident - emailUnvalidated - name - textStatus - (fromMaybe noPict pict) - (fromMaybe [] assets) - accent - (fromMaybe Active status) - loc - svc - handle - expiration - tid - (fromMaybe ManagedByWire managed_by) - (fromMaybe defSupportedProtocols prots) - (fromMaybe True searchable) - - toLocaleWithDefault :: Locale -> (Maybe Language, Maybe Country) -> Locale - toLocaleWithDefault _ (Just l, c) = Locale l c - toLocaleWithDefault l _ = l diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 8bc49238593..ac7c602699d 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -51,6 +51,7 @@ import Data.Code qualified as Code import Data.CommaSeparatedList (CommaSeparatedList (fromCommaSeparatedList)) import Data.Conduit (runConduit, (.|)) import Data.Conduit.List qualified as C +import Data.Default import Data.Hashable (hash) import Data.HavePendingInvitations import Data.Id @@ -137,6 +138,7 @@ import Wire.UserKeyStore (mkEmailKey) import Wire.UserStore (UserStore) import Wire.UserStore qualified as UserStore import Wire.UserSubsystem +import Wire.UserSubsystem qualified as User import Wire.UserSubsystem.Error import Wire.VerificationCode as VerificationCode import Wire.VerificationCodeGen @@ -151,7 +153,9 @@ botAPI :: Member Now r, Member CryptoSign r, Member UserStore r, - Member (Embed HttpClientIO) r + Member (Embed HttpClientIO) r, + Member UserSubsystem r, + Member (Input (Local ())) r ) => ServerT BotAPI (Handler r) botAPI = @@ -178,7 +182,9 @@ servicesAPI :: Member TeamSubsystem r, Member (Concurrency Unsafe) r, Member (Embed HttpClientIO) r, - Member UserStore r + Member UserStore r, + Member (Input (Local ())) r, + Member UserSubsystem r ) => ServerT ServicesAPI (Handler r) servicesAPI = @@ -577,7 +583,9 @@ deleteService pid sid del = do finishDeleteService :: ( Member UserStore r, Member (Embed HttpClientIO) r, - Member (Concurrency Unsafe) r + Member (Concurrency Unsafe) r, + Member (Input (Local ())) r, + Member UserSubsystem r ) => ProviderId -> ServiceId -> @@ -688,7 +696,9 @@ updateServiceWhitelist :: Member (Error UserSubsystemError) r, Member (Concurrency Unsafe) r, Member (Embed HttpClientIO) r, - Member UserStore r + Member UserStore r, + Member (Input (Local ())) r, + Member UserSubsystem r ) => UserId -> ConnId -> @@ -742,7 +752,8 @@ addBot :: Member (Input AuthenticationSubsystemConfig) r, Member Now r, Member CryptoSign r, - Member UserStore r + Member UserStore r, + Member UserSubsystem r ) => UserId -> ConnId -> @@ -751,7 +762,8 @@ addBot :: (Handler r) Public.AddBotResponse addBot zuid zcon cid add = do guardSecondFactorDisabled (Just zuid) - zusr <- lift (wrapClient $ User.lookupUser NoPendingInvitations zuid) >>= maybeInvalidUser + luid <- qualifyLocal zuid + zusr <- lift (liftSem $ User.getLocalAccountBy NoPendingInvitations luid) >>= maybeInvalidUser let pid = addBotProvider add let sid = addBotService add -- Get the conversation and check preconditions @@ -862,7 +874,14 @@ addBot zuid zcon cid add = do Public.rsAddBotEvent = ev } -removeBot :: (Member GalleyAPIAccess r, Member (Embed HttpClientIO) r, Member UserStore r) => UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) +removeBot :: + ( Member GalleyAPIAccess r, + Member (Embed HttpClientIO) r, + Member UserStore r, + Member (Input (Local ())) r, + Member UserSubsystem r + ) => + UserId -> ConnId -> ConvId -> BotId -> Handler r (Maybe Public.RemoveBotResponse) removeBot zusr zcon cid bid = do guardSecondFactorDisabled (Just zusr) -- Get the conversation and check preconditions @@ -890,9 +909,10 @@ guardConvAdmin conv = do let selfMember = cmSelf . cnvMembers $ conv unless (memConvRoleName selfMember == roleNameWireAdmin) $ (throwStd (errorToWai @'E.AccessDenied)) -botGetSelf :: BotId -> (Handler r) Public.UserProfile +botGetSelf :: (Member UserSubsystem r) => BotId -> Handler r Public.UserProfile botGetSelf bot = do - p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot) + lbuid <- qualifyLocal (botUserId bot) + p <- lift . liftSem $ User.getLocalAccountBy NoPendingInvitations lbuid maybe (throwStd (errorToWai @'E.UserNotFound)) (\u -> pure $ Public.mkUserProfile EmailVisibleToSelf UserTypeBot u UserLegalHoldNoConsent) p botGetClient :: (Member GalleyAPIAccess r) => BotId -> (Handler r) (Maybe Public.Client) @@ -934,10 +954,18 @@ botClaimUsersPrekeys _ body = do throwStd (errorToWai @'E.TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError -botListUserProfiles :: (Member GalleyAPIAccess r) => BotId -> (CommaSeparatedList UserId) -> (Handler r) [Public.BotUserView] +botListUserProfiles :: (Member GalleyAPIAccess r, Member UserSubsystem r) => BotId -> (CommaSeparatedList UserId) -> Handler r [Public.BotUserView] botListUserProfiles _ uids = do guardSecondFactorDisabled Nothing -- should we check all user ids? - us <- lift . wrapClient $ User.lookupUsers NoPendingInvitations (fromCommaSeparatedList uids) + localUnit <- qualifyLocal () + us <- + lift . liftSem $ + User.getAccountsBy $ + qualifyAs localUnit $ + def + { getByUserId = fromCommaSeparatedList uids, + includePendingInvitations = NoPendingInvitations + } pure (map mkBotUserView us) botGetUserClients :: (Member GalleyAPIAccess r) => BotId -> UserId -> (Handler r) [Public.PubClient] @@ -947,10 +975,18 @@ botGetUserClients _ uid = do where pubClient c = Public.PubClient c.clientId c.clientClass -botDeleteSelf :: (Member GalleyAPIAccess r, Member (Embed HttpClientIO) r, Member UserStore r) => BotId -> ConvId -> (Handler r) () +botDeleteSelf :: + ( Member GalleyAPIAccess r, + Member (Embed HttpClientIO) r, + Member UserStore r, + Member UserSubsystem r, + Member (Input (Local ())) r + ) => + BotId -> ConvId -> Handler r () botDeleteSelf bid cid = do guardSecondFactorDisabled (Just (botUserId bid)) - bot <- lift . wrapClient $ User.lookupUser NoPendingInvitations (botUserId bid) + lbuid <- qualifyLocal (botUserId bid) + bot <- lift . liftSem $ User.getLocalAccountBy NoPendingInvitations lbuid _ <- maybe (throwStd (errorToWai @'E.InvalidBot)) pure $ (userService =<< bot) _ <- lift . liftSem $ deleteBot (botUserId bid) Nothing bid cid pure () @@ -984,7 +1020,7 @@ activate pid old new = do wrapClientE $ DB.insertKey pid (mkEmailKey <$> old) emailKey deleteBot :: - (Member (Embed HttpClientIO) r, Member UserStore r) => + (Member (Embed HttpClientIO) r, Member UserStore r, Member (Input (Local ())) r, Member UserSubsystem r) => UserId -> Maybe ConnId -> BotId -> @@ -995,7 +1031,8 @@ deleteBot zusr zcon bid cid = do ev <- embed $ RPC.removeBotMember zusr zcon cid bid -- Delete the bot user and client let buid = botUserId bid - mbUser <- embed $ User.lookupUser NoPendingInvitations buid + lbuid <- qualifyLocal' buid + mbUser <- User.getLocalAccountBy NoPendingInvitations lbuid embed $ User.lookupClients buid >>= mapM_ (User.rmClient buid . (.clientId)) for_ (userService =<< mbUser) $ \sref -> do let pid = sref ^. serviceRefProvider diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index a4ee60709e8..228d13d785d 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -26,7 +26,6 @@ import Brig.API.Handler import Brig.API.User (lookupHandle) import Brig.App import Brig.Data.Connection qualified as Conn -import Brig.Data.User (lookupUser) import Control.Error hiding (bool) import Control.Lens (view, (^.)) import Data.Aeson qualified as A @@ -52,6 +51,8 @@ import Wire.Rpc import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserStore (UserStore) +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as User -- FUTUREWORK(mangoiv): this uses 'UserStore' and should hence go to 'UserSubSystem' ejpdRequest :: @@ -60,7 +61,8 @@ ejpdRequest :: Member NotificationSubsystem r, Member UserStore r, Member Rpc r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member UserSubsystem r ) => Maybe Bool -> EJPDRequestBody -> @@ -71,8 +73,8 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do -- find uid given handle responseItemForHandle :: Handle -> AppT r (Maybe EJPDResponseItemRoot) responseItemForHandle hdl = do - mbUid <- liftSem $ lookupHandle hdl - mbUsr <- maybe (pure Nothing) (wrapClient . lookupUser NoPendingInvitations) mbUid + mbUid <- traverse qualifyLocal =<< liftSem (lookupHandle hdl) + mbUsr <- maybe (pure Nothing) (liftSem . User.getLocalAccountBy NoPendingInvitations) mbUid maybe (pure Nothing) (fmap Just . responseItemForExistingUser includeContacts) mbUsr -- construct response item given uid @@ -94,7 +96,8 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do localContacts <- catMaybes <$> do forM contacts $ \(uid', relationDropHistory -> rel) -> do - mbUsr <- wrapClient $ lookupUser NoPendingInvitations uid' -- FUTUREWORK: use polysemy effect, not wrapClient + luid' <- qualifyLocal uid' + mbUsr <- liftSem $ User.getLocalAccountBy NoPendingInvitations luid' -- FUTUREWORK: use polysemy effect, not wrapClient maybe (pure Nothing) (fmap (Just . EJPDContactFound rel . toEJPDResponseItemLeaf) . responseItemForExistingUser False) mbUsr pure . Just . Set.fromList $ localContacts @@ -109,7 +112,8 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do contactsFull <- forM members $ \uid' -> do - mbUsr <- wrapClient $ lookupUser NoPendingInvitations uid' + luid' <- qualifyLocal uid' + mbUsr <- liftSem $ User.getLocalAccountBy NoPendingInvitations luid' maybe (pure Nothing) (fmap Just . responseItemForExistingUser False) mbUsr let listType = Team.toNewListType (memberList ^. Team.teamMemberListType) From 05ac780bcac9de66655c0b72609565ebbc1c5e05 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 15 Jan 2026 17:55:35 +0100 Subject: [PATCH 04/16] Brig.Data.User.userExists -> Wire.UserStore.doesUserExist --- libs/wire-subsystems/src/Wire/UserStore.hs | 1 + libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs | 8 ++++++++ .../test/unit/Wire/MockInterpreters/UserStore.hs | 1 + services/brig/src/Brig/API/User.hs | 2 +- services/brig/src/Brig/Data/User.hs | 7 ------- services/brig/test/integration/API/Internal.hs | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 693ed42a19b..42707c8e828 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -71,6 +71,7 @@ data StoredUserUpdateError = StoredUserUpdateHandleExists data UserStore m a where CreateUser :: NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> UserStore m () GetIndexUser :: UserId -> UserStore m (Maybe IndexUser) + DoesUserExist :: UserId -> UserStore m Bool GetIndexUsersPaginated :: Int32 -> Maybe PagingState -> UserStore m (PageWithState IndexUser) GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 846445ca3b5..8dda6d1ef9e 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -45,6 +45,7 @@ interpretUserStoreCassandra casClient = runEmbedded (runClient casClient) . embed . \case CreateUser new mbConv -> createUserImpl new mbConv GetUsers uids -> getUsersImpl uids + DoesUserExist uid -> doesUserExistImpl uid GetIndexUser uid -> getIndexUserImpl uid GetIndexUsersPaginated pageSize mPagingState -> getIndexUserPaginatedImpl pageSize mPagingState UpdateUser uid update -> updateUserImpl uid update @@ -101,6 +102,13 @@ getUsersImpl usrs = map asRecord <$> retry x1 (query selectUsers (params LocalQuorum (Identity usrs))) +doesUserExistImpl :: UserId -> Client Bool +doesUserExistImpl uid = + isJust <$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) + where + idSelect :: PrepQuery R (Identity UserId) (Identity UserId) + idSelect = "SELECT id FROM user WHERE id = ?" + getIndexUserImpl :: UserId -> Client (Maybe IndexUser) getIndexUserImpl u = do mIndexUserTuple <- retry x1 $ query1 cql (params LocalQuorum (Identity u)) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index 7de0daeeb6f..c50d012a72a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -42,6 +42,7 @@ inMemoryUserStoreInterpreter :: inMemoryUserStoreInterpreter = interpret $ \case CreateUser new _ -> modify (newStoredUserToStoredUser new :) GetUsers uids -> gets $ filter (\user -> user.id `elem` uids) + DoesUserExist uid -> gets (any (\u -> u.id == uid)) UpdateUser uid update -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 57286b7f571..429d09ce6da 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -627,7 +627,7 @@ changeSingleAccountStatus :: AccountStatus -> ExceptT AccountStatusError (AppT r) () changeSingleAccountStatus uid status = do - unlessM (wrapClientE $ Data.userExists uid) $ throwE AccountNotFound + unlessM (lift . liftSem $ UserStore.doesUserExist uid) $ throwE AccountNotFound ev <- mkUserEvent (NonEmpty.singleton uid) status lift . liftSem $ do UserStore.updateAccountStatus uid status diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 207ab341fe2..528afadf9ba 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -28,7 +28,6 @@ module Brig.Data.User lookupRichInfoMultiUsers, lookupUserTeam, lookupFeatureConferenceCalling, - userExists, -- * Updates activateUser, @@ -157,9 +156,6 @@ newStoredUserViaScim uid externalId tid locale name email = do searchable = True } -userExists :: (MonadClient m) => UserId -> m Bool -userExists uid = isJust <$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) - activateUser :: (MonadClient m) => UserId -> UserIdentity -> m () activateUser u ident = do let email = emailIdentity ident @@ -199,9 +195,6 @@ lookupFeatureConferenceCalling uid = do ------------------------------------------------------------------------------- -- Queries -idSelect :: PrepQuery R (Identity UserId) (Identity UserId) -idSelect = "SELECT id FROM user WHERE id = ?" - nameSelect :: PrepQuery R (Identity UserId) (Identity Name) nameSelect = "SELECT name FROM user WHERE id = ?" diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index f02fd2c33c3..555a136b483 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -23,9 +23,9 @@ module API.Internal ) where +import API.UserPendingActivation (userExists) import Bilge import Bilge.Assert -import Brig.Data.User import Brig.Options qualified as Opt import Cassandra qualified as C import Cassandra qualified as Cass From 9e457773d1c47e43853a1350275f0eae1a397793 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 19 Jan 2026 11:35:35 +0100 Subject: [PATCH 05/16] Move all remaining DB functions from Brig.Data.User to Wire.UserStore --- libs/wire-subsystems/src/Wire/UserStore.hs | 5 ++ .../src/Wire/UserStore/Cassandra.hs | 48 +++++++++++++ .../unit/Wire/MockInterpreters/UserStore.hs | 9 ++- services/brig/src/Brig/API/Connection.hs | 25 ++++--- .../brig/src/Brig/API/Connection/Remote.hs | 9 +-- services/brig/src/Brig/API/Federation.hs | 4 +- services/brig/src/Brig/API/Internal.hs | 15 ++-- services/brig/src/Brig/API/Public.hs | 11 +-- services/brig/src/Brig/API/User.hs | 14 ++-- services/brig/src/Brig/Data/Activation.hs | 3 +- services/brig/src/Brig/Data/User.hs | 68 ------------------- services/brig/src/Brig/Provider/API.hs | 6 +- services/brig/src/Brig/User/API/Handle.hs | 6 +- 13 files changed, 108 insertions(+), 115 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 42707c8e828..a7bc2e9fcdb 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -83,7 +83,10 @@ data UserStore m a where UpdateSSOId :: UserId -> Maybe UserSSOId -> UserStore m Bool UpdateManagedBy :: UserId -> ManagedBy -> UserStore m () UpdateAccountStatus :: UserId -> AccountStatus -> UserStore m () + ActivateUser :: UserId -> UserIdentity -> UserStore m () + DeactivateUser :: UserId -> UserStore m () DeleteUser :: User -> UserStore m () + LookupName :: UserId -> UserStore m (Maybe Name) -- | This operation looks up a handle but is guaranteed to not give you stale locks. -- It is potentially slower and less resilient than 'GlimpseHandle'. LookupHandle :: Handle -> UserStore m (Maybe UserId) @@ -102,10 +105,12 @@ data UserStore m a where UpdateUserTeam :: UserId -> TeamId -> UserStore m () GetActivityTimestamps :: UserId -> UserStore m [Maybe UTCTime] GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) + LookupRichInfos :: [UserId] -> UserStore m [(UserId, RichInfo)] UpdateRichInfo :: UserId -> RichInfoAssocList -> UserStore m () GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) SetUserSearchable :: UserId -> SetSearchable -> UserStore m () UpdateFeatureConferenceCalling :: UserId -> Maybe FeatureStatus -> UserStore m () + LookupFeatureConferenceCalling :: UserId -> UserStore m (Maybe FeatureStatus) DeleteServiceUser :: ProviderId -> ServiceId -> BotId -> UserStore m () LookupServiceUsers :: ProviderId -> ServiceId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId, Maybe TeamId)) LookupServiceUsersForTeam :: ProviderId -> ServiceId -> TeamId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId)) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 8dda6d1ef9e..54f62183317 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -56,9 +56,13 @@ interpretUserStoreCassandra casClient = UpdateSSOId uid ssoId -> updateSSOIdImpl uid ssoId UpdateManagedBy uid managedBy -> updateManagedByImpl uid managedBy UpdateAccountStatus uid accountStatus -> updateAccountStatusImpl uid accountStatus + ActivateUser uid identity -> activateUserImpl uid identity + DeactivateUser uid -> deactivateUserImpl uid UpdateRichInfo uid richInfo -> updateRichInfoImpl uid richInfo UpdateFeatureConferenceCalling uid feat -> updateFeatureConferenceCallingImpl uid feat + LookupFeatureConferenceCalling uid -> lookupFeatureConferenceCallingImpl uid DeleteUser user -> deleteUserImpl user + LookupName uid -> lookupNameImpl uid LookupHandle hdl -> lookupHandleImpl LocalQuorum hdl GlimpseHandle hdl -> lookupHandleImpl One hdl LookupStatus uid -> lookupStatusImpl uid @@ -68,6 +72,7 @@ interpretUserStoreCassandra casClient = UpdateUserTeam uid tid -> updateUserTeamImpl uid tid GetActivityTimestamps uid -> getActivityTimestampsImpl uid GetRichInfo uid -> getRichInfoImpl uid + LookupRichInfos uids -> lookupRichInfosImpl uids GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid DeleteEmail uid -> deleteEmailImpl uid SetUserSearchable uid searchable -> setUserSearchableImpl uid searchable @@ -212,6 +217,49 @@ updateAccountStatusImpl u s = userStatusUpdate :: PrepQuery W (AccountStatus, UserId) () userStatusUpdate = "UPDATE user SET status = ? WHERE id = ?" +activateUserImpl :: (MonadClient m) => UserId -> UserIdentity -> m () +activateUserImpl u ident = do + let email = emailIdentity ident + retry x5 $ write userActivatedUpdate (params LocalQuorum (email, u)) + where + userActivatedUpdate :: PrepQuery W (Maybe EmailAddress, UserId) () + userActivatedUpdate = "UPDATE user SET activated = true, email = ? WHERE id = ?" + +deactivateUserImpl :: (MonadClient m) => UserId -> m () +deactivateUserImpl u = + retry x5 $ write userDeactivatedUpdate (params LocalQuorum (Identity u)) + where + userDeactivatedUpdate :: PrepQuery W (Identity UserId) () + userDeactivatedUpdate = "UPDATE user SET activated = false WHERE id = ?" + +lookupNameImpl :: (MonadClient m) => UserId -> m (Maybe Name) +lookupNameImpl u = + fmap runIdentity + <$> retry x1 (query1 nameSelect (params LocalQuorum (Identity u))) + where + nameSelect :: PrepQuery R (Identity UserId) (Identity Name) + nameSelect = "SELECT name FROM user WHERE id = ?" + +-- | Returned rich infos are in the same order as users +lookupRichInfosImpl :: (MonadClient m) => [UserId] -> m [(UserId, RichInfo)] +lookupRichInfosImpl users = do + mapMaybe (\(uid, mbRi) -> (uid,) . RichInfo <$> mbRi) + <$> retry x1 (query richInfoSelectMulti (params LocalQuorum (Identity users))) + where + richInfoSelectMulti :: PrepQuery R (Identity [UserId]) (UserId, Maybe RichInfoAssocList) + richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" + +lookupFeatureConferenceCallingImpl :: (MonadClient m) => UserId -> m (Maybe FeatureStatus) +lookupFeatureConferenceCallingImpl uid = do + let q = query1 select (params LocalQuorum (Identity uid)) + (>>= runIdentity) <$> retry x1 q + where + select :: PrepQuery R (Identity UserId) (Identity (Maybe FeatureStatus)) + select = fromString "select feature_conference_calling from user where id = ?" + +------------------------------------------------------------------------------- +-- Queries + updateRichInfoImpl :: (MonadClient m) => UserId -> RichInfoAssocList -> m () updateRichInfoImpl u ri = retry x5 $ write userRichInfoUpdate (params LocalQuorum (ri, u)) where diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index c50d012a72a..1aa38695158 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -83,8 +83,10 @@ inMemoryUserStoreInterpreter = interpret $ \case gets (any (\u -> u.id == uid)) UpdateManagedBy uid managedBy -> updateUserInStore uid (\u -> u {managedBy = Just managedBy}) UpdateAccountStatus uid accountStatus -> updateUserInStore uid (\u -> u {status = Just accountStatus}) - UpdateRichInfo {} -> error "UpdateRichInfo: Not implemented" + ActivateUser uid identity -> updateUserInStore uid (\u -> u {activated = True, email = emailIdentity identity}) + DeactivateUser uid -> updateUserInStore uid (\u -> u {activated = False}) UpdateFeatureConferenceCalling {} -> error "UpdateFeatureConferenceCalling: Not implemented" + LookupFeatureConferenceCalling {} -> error "FeatureConferenceCalling: Not implemented" GetIndexUser uid -> do mUser <- gets @[StoredUser] $ find (\user -> user.id == uid) pure $ storedUserToIndexUser <$> mUser @@ -111,6 +113,7 @@ inMemoryUserStoreInterpreter = interpret $ \case us' <- f us put us' DeleteUser user -> modify @[StoredUser] $ filter (\u -> u.id /= User.userId user) + LookupName uid -> (.name) <$$> gets (find $ \u -> u.id == uid) LookupHandle h -> lookupHandleImpl h GlimpseHandle h -> lookupHandleImpl h LookupStatus uid -> lookupStatusImpl uid @@ -121,7 +124,9 @@ inMemoryUserStoreInterpreter = interpret $ \case map (\u -> if u.id == uid then u {teamId = Just tid} :: StoredUser else u) GetActivityTimestamps _ -> pure [] - GetRichInfo _ -> error "rich info not implemented" + GetRichInfo _ -> error "GetRichInfo: not implemented" + LookupRichInfos _ -> error "LookupRichInfos: not implemented" + UpdateRichInfo {} -> error "UpdateRichInfo: Not implemented" GetUserAuthenticationInfo _uid -> error "Not implemented" DeleteEmail uid -> modify (map doUpdate) where diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 63a9526e42f..ce2390f04cc 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -40,7 +40,6 @@ import Brig.API.User (getLegalHoldStatus) import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.Types (resultHasMore, resultList) -import Brig.Data.User qualified as Data import Brig.IO.Intra qualified as Intra import Brig.IO.Logging import Brig.Options @@ -72,6 +71,7 @@ import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.NotificationSubsystem import Wire.TeamSubsystem (TeamSubsystem) import Wire.UserStore +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem ensureNotSameTeam :: (Member GalleyAPIAccess r) => Local UserId -> Local UserId -> (ConnectionM r) () @@ -141,7 +141,7 @@ createConnectionToLocalUser self conn target = do s2o' <- wrapClient $ Data.insertConnection self (tUntagged target) SentWithHistory qcnv o2s' <- wrapClient $ Data.insertConnection target (tUntagged self) PendingWithHistory qcnv e2o <- - ConnectionUpdated o2s' <$> wrapClient (Data.lookupName (tUnqualified self)) + ConnectionUpdated o2s' <$> liftSem (UserStore.lookupName (tUnqualified self)) let e2s = ConnectionUpdated s2o' Nothing liftSem $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure s2o' @@ -176,8 +176,8 @@ createConnectionToLocalUser self conn target = do then Data.updateConnection o2s BlockedWithHistory else Data.updateConnection o2s AcceptedWithHistory e2o <- - lift . wrapClient $ - ConnectionUpdated o2s' <$> Data.lookupName (tUnqualified self) + lift . liftSem $ + ConnectionUpdated o2s' <$> UserStore.lookupName (tUnqualified self) let e2s = ConnectionUpdated s2o' Nothing lift $ liftSem $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure $ Existed s2o' @@ -232,7 +232,8 @@ updateConnection :: Member NotificationSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member UserStore r ) => Local UserId -> Qualified UserId -> @@ -257,7 +258,8 @@ updateConnectionToLocalUser :: ( Member (Embed HttpClientIO) r, Member GalleyAPIAccess r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member UserStore r ) => -- | From Local UserId -> @@ -337,7 +339,7 @@ updateConnectionToLocalUser self other newStatus conn = do else Data.updateConnection o2s BlockedWithHistory e2o <- ConnectionUpdated o2s' - <$> wrapClient (Data.lookupName (tUnqualified self)) + <$> liftSem (UserStore.lookupName (tUnqualified self)) liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o AcceptedWithHistory @@ -376,9 +378,9 @@ updateConnectionToLocalUser self other newStatus conn = do then Data.updateConnection o2s AcceptedWithHistory else Data.updateConnection o2s BlockedWithHistory e2o :: ConnectionEvent <- - wrapClient $ + liftSem $ ConnectionUpdated o2s' - <$> Data.lookupName (tUnqualified self) + <$> UserStore.lookupName (tUnqualified self) -- TODO: is this correct? shouldnt o2s be sent to other? liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o (mkRelationWithHistory (error "impossible") new) @@ -431,7 +433,8 @@ updateConnectionInternal :: ( Member GalleyAPIAccess r, Member NotificationSubsystem r, Member TinyLog r, - Member (Embed HttpClientIO) r + Member (Embed HttpClientIO) r, + Member UserStore r ) => UpdateConnectionsInternal -> ExceptT ConnectionError (AppT r) () @@ -499,7 +502,7 @@ updateConnectionInternal = \case void . lift . liftSem . for (ucConvId uconn) $ unblockConversation lfrom Nothing uconnRevRel :: RelationWithHistory <- relationWithHistory lfrom (ucTo uconnRev) uconnRev' <- lift . wrapClient $ Data.updateConnection uconnRev (undoRelationHistory uconnRevRel) - connName <- lift . wrapClient $ Data.lookupName (tUnqualified lfrom) + connName <- lift . liftSem $ UserStore.lookupName (tUnqualified lfrom) let connEvent = ConnectionUpdated { ucConn = uconnRev', diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 0bf4c398d5f..5b4cbd54333 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -27,7 +27,6 @@ import Brig.API.Connection.Util import Brig.API.Types (ConnectionError (..)) import Brig.App import Brig.Data.Connection qualified as Data -import Brig.Data.User qualified as Data import Brig.Federation.Client as Federation import Brig.IO.Intra qualified as Intra import Brig.Options @@ -53,6 +52,7 @@ import Wire.FederationConfigStore import Wire.GalleyAPIAccess import Wire.NotificationSubsystem import Wire.UserStore +import Wire.UserStore qualified as UserStore data LocalConnectionAction = LocalConnect @@ -224,7 +224,7 @@ pushEvent self mzcon connection = do liftSem $ Intra.onConnectionEvent (tUnqualified self) mzcon event performLocalAction :: - (Member GalleyAPIAccess r, Member NotificationSubsystem r) => + (Member GalleyAPIAccess r, Member NotificationSubsystem r, Member UserStore r) => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -236,7 +236,7 @@ performLocalAction self mzcon other mconnection action = do checkLimitForLocalAction self rel0 action mrel2 <- for (transition (LCA action) rel0) $ \rel1 -> do mreaction <- fmap join . for (remoteAction action) $ \ra -> do - mSelfTeam <- lift . wrapClient . Data.lookupUserTeam . tUnqualified $ self + mSelfTeam <- lift . liftSem . UserStore.getUserTeam . tUnqualified $ self response <- sendConnectionAction self @@ -316,7 +316,8 @@ createConnectionToRemoteUser self zcon other = do updateConnectionToRemoteUser :: ( Member GalleyAPIAccess r, Member NotificationSubsystem r, - Member FederationConfigStore r + Member FederationConfigStore r, + Member UserStore r ) => Local UserId -> Remote UserId -> diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 1ef9898fee0..7951cf09cf5 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -30,7 +30,6 @@ import Brig.API.MLS.Util import Brig.API.User qualified as API import Brig.App import Brig.Data.Connection qualified as Data -import Brig.Data.User qualified as Data import Brig.IO.Intra (notify) import Brig.Options import Brig.User.API.Handle @@ -76,6 +75,7 @@ import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.UserStore +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem (UserSubsystem) import Wire.UserSubsystem qualified as UserSubsystem @@ -267,7 +267,7 @@ searchUsers domain (SearchRequest searchTerm mTeam mOnlyInTeams) = do case maybeOwnerId of Nothing -> pure [] Just foundUser -> do - mFoundUserTeamId <- lift $ wrapClient $ Data.lookupUserTeam foundUser + mFoundUserTeamId <- lift $ liftSem $ UserStore.getUserTeam foundUser localFoundUser <- qualifyLocal foundUser if isTeamAllowed mOnlyInTeams mFoundUserTeamId then lift $ liftSem $ (fmap contactFromProfile . maybeToList) <$> UserSubsystem.getLocalUserProfile localFoundUser diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 0f4a716975a..0d456de2b0c 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -37,7 +37,6 @@ import Brig.Data.Activation import Brig.Data.Client qualified as Data import Brig.Data.Connection qualified as Data import Brig.Data.MLS.KeyPackage qualified as Data -import Brig.Data.User qualified as Data import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options hiding (internalEvents) import Brig.Provider.API qualified as Provider @@ -437,9 +436,9 @@ updateFederationRemote dom fedcfg = do "keeping track of remote domains in the brig config file is deprecated, but as long as we \ \do that, removing or updating items listed in the config file is not allowed." -getAccountConferenceCallingConfig :: UserId -> Handler r (Feature ConferenceCallingConfig) +getAccountConferenceCallingConfig :: (Member UserStore r) => UserId -> Handler r (Feature ConferenceCallingConfig) getAccountConferenceCallingConfig uid = do - mStatus <- lift $ wrapClient $ Data.lookupFeatureConferenceCalling uid + mStatus <- lift $ liftSem $ UserStore.lookupFeatureConferenceCalling uid mDefStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNull) pure $ def {status = mStatus <|> mDefStatus ?: (def :: LockableFeature ConferenceCallingConfig).status} @@ -837,7 +836,8 @@ getConnectionsStatus (ConnectionsStatusRequestV2 froms mtos mrel) = do revokeIdentityH :: ( Member UserSubsystem r, - Member UserKeyStore r + Member UserKeyStore r, + Member UserStore r ) => EmailAddress -> Handler r NoContent @@ -847,7 +847,8 @@ updateConnectionInternalH :: ( Member GalleyAPIAccess r, Member NotificationSubsystem r, Member TinyLog r, - Member (Embed HttpClientIO) r + Member (Embed HttpClientIO) r, + Member UserStore r ) => UpdateConnectionsInternal -> (Handler r) NoContent @@ -951,9 +952,9 @@ getRichInfoH uid = RichInfo . fromMaybe mempty <$> lift (liftSem $ UserStore.getRichInfo uid) -getRichInfoMultiH :: Maybe (CommaSeparatedList UserId) -> Handler r BrigIRoutes.GetRichInfoMultiResponse +getRichInfoMultiH :: (Member UserStore r) => Maybe (CommaSeparatedList UserId) -> Handler r BrigIRoutes.GetRichInfoMultiResponse getRichInfoMultiH (maybe [] fromCommaSeparatedList -> uids) = - lift $ wrapClient $ BrigIRoutes.GetRichInfoMultiResponse <$> API.lookupRichInfoMultiUsers uids + lift $ liftSem $ BrigIRoutes.GetRichInfoMultiResponse <$> UserStore.lookupRichInfos uids updateHandleH :: (Member UserSubsystem r) => diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 99993015f40..89466550dbc 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -43,7 +43,6 @@ import Brig.App import Brig.Calling.API qualified as Calling import Brig.Data.Connection qualified as Data import Brig.Data.Nonce as Nonce -import Brig.Data.User qualified as Data import Brig.Effects.ConnectionStore import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) @@ -1329,7 +1328,8 @@ updateLocalConnection :: ( Member GalleyAPIAccess r, Member NotificationSubsystem r, Member TinyLog r, - Member (Embed HttpClientIO) r + Member (Embed HttpClientIO) r, + Member UserStore r ) => UserId -> ConnId -> @@ -1347,7 +1347,8 @@ updateConnection :: Member NotificationSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member UserStore r ) => UserId -> ConnId -> @@ -1478,9 +1479,9 @@ updateUserEmail :: Public.EmailUpdate -> (Handler r) () updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do - maybeZuserTeamId <- lift $ wrapClient $ Data.lookupUserTeam zuserId + maybeZuserTeamId <- lift . liftSem $ UserStore.getUserTeam zuserId whenM (not <$> assertHasPerm maybeZuserTeamId) $ throwStd insufficientTeamPermissions - maybeEmailOwnerTeamId <- lift $ wrapClient $ Data.lookupUserTeam emailOwnerId + maybeEmailOwnerTeamId <- lift . liftSem $ UserStore.getUserTeam emailOwnerId checkSameTeam maybeZuserTeamId maybeEmailOwnerTeamId lEmailOwnerId <- qualifyLocal emailOwnerId void . lift . liftSem $ diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 429d09ce6da..b7c2a15c873 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -31,8 +31,6 @@ module Brig.API.User changeAccountStatus, changeSingleAccountStatus, getLegalHoldStatus, - Data.lookupName, - Data.lookupRichInfoMultiUsers, revokeIdentity, deleteUserNoVerify, deleteUsersNoVerify, @@ -73,7 +71,6 @@ import Brig.Data.Client qualified as Data import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data import Brig.Data.User -import Brig.Data.User qualified as Data import Brig.Effects.ConnectionStore import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore @@ -249,7 +246,7 @@ createUserSpar new = do unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do - wrapClient $ activateUser uid ident + liftSem $ UserStore.activateUser uid ident void $ onActivated (AccountActivated account) liftSem $ Log.info $ @@ -472,7 +469,7 @@ createUser rateLimitKey new = do throwE RegisterErrorTooManyTeamMembers lift $ do -- ('insertAccount' sets column activated to False; here it is set to True.) - wrapClient $ activateUser uid ident + liftSem $ UserStore.activateUser uid ident void $ onActivated (AccountActivated account) liftSem do Log.info $ @@ -489,7 +486,7 @@ createUser rateLimitKey new = do unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do - wrapClient $ activateUser uid ident + liftSem $ UserStore.activateUser uid ident void $ onActivated (AccountActivated account) liftSem $ Log.info $ @@ -579,7 +576,8 @@ checkRestrictedUserCreation new = do -- boils down to deactivating the user. revokeIdentity :: ( Member UserSubsystem r, - Member UserKeyStore r + Member UserKeyStore r, + Member UserStore r ) => EmailAddress -> AppT r () @@ -587,7 +585,7 @@ revokeIdentity key = do mu <- liftSem . lookupKey . mkEmailKey $ key for_ mu $ \u -> do deactivate <- maybe False (not . isSSOIdentity) <$> fetchUserIdentity u - when deactivate . wrapClient . Data.deactivateUser $ u + when deactivate . liftSem . UserStore.deactivateUser $ u ------------------------------------------------------------------------------- -- Change Account Status diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 6e548e72b0a..3fcfc77d8d9 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -27,7 +27,6 @@ module Brig.Data.Activation where import Brig.App (AppT, adhocUserKeyStoreInterpreter, liftSem, qualifyLocal, wrapClientE) -import Brig.Data.User import Brig.Types.Intra import Cassandra import Control.Error @@ -97,7 +96,7 @@ activateKey k c u = do Nothing -> do claim key uid let ident = EmailIdentity (emailKeyOrig key) - wrapClientE (activateUser uid ident) + lift . liftSem $ UserStore.activateUser uid ident let a' = a {userIdentity = Just ident} pure . Just $ AccountActivated a' Just _ -> do diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 528afadf9ba..59d69fab06e 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -22,24 +22,12 @@ module Brig.Data.User ( -- * Creation newStoredUser, newStoredUserViaScim, - - -- * Lookups - lookupName, - lookupRichInfoMultiUsers, - lookupUserTeam, - lookupFeatureConferenceCalling, - - -- * Updates - activateUser, - deactivateUser, ) where import Brig.App import Brig.Options -import Cassandra hiding (Set) import Control.Error -import Control.Lens hiding (from) import Data.Handle (Handle) import Data.Id import Data.Json.Util (toUTCTimeMillis) @@ -48,9 +36,7 @@ import Data.Time (addUTCTime) import Data.UUID.V4 import Imports import Wire.API.Password -import Wire.API.Team.Feature import Wire.API.User -import Wire.API.User.RichInfo import Wire.AuthenticationSubsystem.Config import Wire.StoredUser @@ -155,57 +141,3 @@ newStoredUserViaScim uid externalId tid locale name email = do supportedProtocols = defSupportedProtocols, searchable = True } - -activateUser :: (MonadClient m) => UserId -> UserIdentity -> m () -activateUser u ident = do - let email = emailIdentity ident - retry x5 $ write userActivatedUpdate (params LocalQuorum (email, u)) - -deactivateUser :: (MonadClient m) => UserId -> m () -deactivateUser u = - retry x5 $ write userDeactivatedUpdate (params LocalQuorum (Identity u)) - -lookupName :: (MonadClient m) => UserId -> m (Maybe Name) -lookupName u = - fmap runIdentity - <$> retry x1 (query1 nameSelect (params LocalQuorum (Identity u))) - --- | Returned rich infos are in the same order as users -lookupRichInfoMultiUsers :: (MonadClient m) => [UserId] -> m [(UserId, RichInfo)] -lookupRichInfoMultiUsers users = do - mapMaybe (\(uid, mbRi) -> (uid,) . RichInfo <$> mbRi) - <$> retry x1 (query richInfoSelectMulti (params LocalQuorum (Identity users))) - --- | Lookup user (no matter what status) and return 'TeamId'. Safe to use for authorization: --- suspended / deleted / ... users can't login, so no harm done if we authorize them *after* --- successful login. -lookupUserTeam :: (MonadClient m) => UserId -> m (Maybe TeamId) -lookupUserTeam u = - (runIdentity =<<) - <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) - -lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe FeatureStatus) -lookupFeatureConferenceCalling uid = do - let q = query1 select (params LocalQuorum (Identity uid)) - (>>= runIdentity) <$> retry x1 q - where - select :: PrepQuery R (Identity UserId) (Identity (Maybe FeatureStatus)) - select = fromString "select feature_conference_calling from user where id = ?" - -------------------------------------------------------------------------------- --- Queries - -nameSelect :: PrepQuery R (Identity UserId) (Identity Name) -nameSelect = "SELECT name FROM user WHERE id = ?" - -richInfoSelectMulti :: PrepQuery R (Identity [UserId]) (UserId, Maybe RichInfoAssocList) -richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" - -teamSelect :: PrepQuery R (Identity UserId) (Identity (Maybe TeamId)) -teamSelect = "SELECT team FROM user WHERE id = ?" - -userDeactivatedUpdate :: PrepQuery W (Identity UserId) () -userDeactivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = false WHERE id = ?" - -userActivatedUpdate :: PrepQuery W (Maybe EmailAddress, UserId) () -userActivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = true, email = ? WHERE id = ?" diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index ac7c602699d..db179ce7fab 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -33,7 +33,6 @@ import Brig.API.Handler import Brig.API.Types (PasswordResetError (..)) import Brig.App import Brig.Data.Client qualified as User -import Brig.Data.User qualified as User import Brig.Options (Settings (..)) import Brig.Options qualified as Opt import Brig.Provider.DB (ServiceConn (..)) @@ -665,19 +664,20 @@ searchServiceProfiles _ Nothing Nothing _ = do -- NB: unlike 'searchServiceProfiles', we don't filter by service provider here searchTeamServiceProfiles :: + (Member UserStore r) => UserId -> TeamId -> Maybe (Range 1 128 Text) -> Maybe Bool -> Maybe (Range 10 100 Int32) -> - (Handler r) Public.ServiceProfilePage + Handler r Public.ServiceProfilePage searchTeamServiceProfiles uid tid prefix mFilterDisabled mSize = do -- Check that the user actually belong to the team they claim they -- belong to. (Note: the 'tid' team might not even exist but we'll throw -- 'insufficientTeamPermissions' anyway) let filterDisabled = fromMaybe True mFilterDisabled let size = fromMaybe (unsafeRange 20) mSize - teamId <- lift $ wrapClient $ User.lookupUserTeam uid + teamId <- lift $ liftSem $ UserStore.getUserTeam uid unless (Just tid == teamId) $ throwStd insufficientTeamPermissions -- Get search results diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index ecad744a942..b93fe3df3a6 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -27,7 +27,6 @@ import Brig.API.Error (fedError) import Brig.API.Handler (Handler) import Brig.API.User qualified as API import Brig.App -import Brig.Data.User qualified as Data import Brig.Federation.Client qualified as Federation import Brig.Options (searchSameTeamOnly) import Data.Handle (Handle, fromHandle) @@ -42,6 +41,7 @@ import Wire.API.User qualified as Public import Wire.API.User.Search import Wire.API.User.Search qualified as Public import Wire.UserStore (UserStore) +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem getHandleInfo :: @@ -83,12 +83,12 @@ getLocalHandleInfo self handle = do pure $ listToMaybe owner -- | Checks search permissions and filters accordingly -filterHandleResults :: Local UserId -> [Public.UserProfile] -> (Handler r) [Public.UserProfile] +filterHandleResults :: (Member UserStore r) => Local UserId -> [Public.UserProfile] -> Handler r [Public.UserProfile] filterHandleResults searchingUser us = do sameTeamSearchOnly <- fromMaybe False <$> asks (.settings.searchSameTeamOnly) if sameTeamSearchOnly then do - fromTeam <- lift . wrapClient $ Data.lookupUserTeam (tUnqualified searchingUser) + fromTeam <- lift . liftSem $ UserStore.getUserTeam (tUnqualified searchingUser) pure $ case fromTeam of Just team -> filter (\x -> Public.profileTeam x == Just team) us Nothing -> us From 5c2a02490a9cee2bc4aeb737c8d13d7cec0c7fd9 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 20 Jan 2026 17:00:36 +0100 Subject: [PATCH 06/16] hlint --- services/brig/src/Brig/API/User.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index b7c2a15c873..9cf82bd3724 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -215,9 +215,8 @@ createUserSpar new = do -- FUTUREWORK: make this transactional if possible UserStore.createUser account Nothing - case unRichInfo <$> newUserSparRichInfo new of - Just richInfo -> UserStore.updateRichInfo uid richInfo - Nothing -> pure () -- Nothing to do + for_ new.newUserSparRichInfo $ + UserStore.updateRichInfo uid . unRichInfo GalleyAPIAccess.createSelfConv uid User.internalUpdateSearchIndex uid Events.generateUserEvent uid Nothing (UserCreated u) From ab9b38d1a945d559fb02069e867f2be38d1cf3e7 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 19 Jan 2026 16:09:10 +0100 Subject: [PATCH 07/16] UserStore.IndexUser: Simplify type by only tracking overall created and updated timestamps This way adapting it for postgresql will be much easier --- .../IndexedUserStore/Bulk/ElasticSearch.hs | 4 +- .../src/Wire/UserStore/Cassandra.hs | 4 +- .../src/Wire/UserStore/IndexUser.hs | 140 +++++++----------- .../src/Wire/UserSubsystem/Interpreter.hs | 6 +- .../unit/Wire/MockInterpreters/UserStore.hs | 28 ++-- 5 files changed, 77 insertions(+), 105 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs index e5585735bdd..41e905f9cff 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs @@ -116,7 +116,7 @@ syncAllUsersWithVersion mkVersion = -- contains User, Maybe Role, UserType, ..., and pass around -- ExtendedUser. this should make the code less convoluted. - let teams :: Map TeamId [IndexUser] = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) . value <$> u.teamId) page + let teams :: Map TeamId [IndexUser] = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) <$> u.teamId) page teamIds = Map.keys teams visMap <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 teamIds $ \t -> (t,) <$> teamSearchVisibilityInbound t @@ -125,7 +125,7 @@ syncAllUsersWithVersion mkVersion = roles :: Map UserId (WithWritetime Role) <- fmap (Map.fromList . concat) . unsafePooledForConcurrentlyN 16 (Map.toList teams) $ \(t, us) -> do tms <- (.members) <$> selectTeamMemberInfos t (fmap (.userId) us) pure $ mapMaybe mkRoleWithWriteTime tms - let vis indexUser = fromMaybe defaultSearchVisibilityInbound $ (flip Map.lookup visMap . value =<< indexUser.teamId) + let vis indexUser = fromMaybe defaultSearchVisibilityInbound $ (flip Map.lookup visMap =<< indexUser.teamId) mkUserDoc indexUser = indexUserToDoc (vis indexUser) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 54f62183317..98b930f0cf1 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -117,14 +117,14 @@ doesUserExistImpl uid = getIndexUserImpl :: UserId -> Client (Maybe IndexUser) getIndexUserImpl u = do mIndexUserTuple <- retry x1 $ query1 cql (params LocalQuorum (Identity u)) - pure $ asRecord <$> mIndexUserTuple + pure $ indexUserFromTuple <$> mIndexUserTuple where cql :: PrepQuery R (Identity UserId) (TupleType IndexUser) cql = prepared . QueryString $ getIndexUserBaseQuery <> " WHERE id = ?" getIndexUserPaginatedImpl :: Int32 -> Maybe PagingState -> Client (PageWithState IndexUser) getIndexUserPaginatedImpl pageSize mPagingState = - asRecord <$$> paginateWithState cql (paramsPagingState LocalQuorum () pageSize mPagingState) + indexUserFromTuple <$$> paginateWithState cql (paramsPagingState LocalQuorum () pageSize mPagingState) where cql :: PrepQuery R () (TupleType IndexUser) cql = prepared $ QueryString getIndexUserBaseQuery diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs index 824fe49e242..ce3d9221f2a 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -28,6 +28,7 @@ import Data.Json.Util import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error qualified as Text import Data.Text.ICU.Translit +import Data.Time import Database.CQL.Protocol import Imports import SAML2.WebSSO qualified as SAML @@ -44,19 +45,20 @@ data WithWritetime a = WithWriteTime {value :: a, writetime :: Writetime a} data IndexUser = IndexUser { userId :: UserId, - teamId :: Maybe (WithWritetime TeamId), - name :: WithWritetime Name, - accountStatus :: Maybe (WithWritetime AccountStatus), - handle :: Maybe (WithWritetime Handle), - email :: Maybe (WithWritetime EmailAddress), - colourId :: WithWritetime ColourId, - activated :: WithWritetime Activated, - serviceId :: Maybe (WithWritetime ServiceId), - managedBy :: Maybe (WithWritetime ManagedBy), - ssoId :: Maybe (WithWritetime UserSSOId), - unverifiedEmail :: Maybe (WithWritetime EmailAddress), - searchable :: Maybe (WithWritetime Bool), - writeTimeBumper :: Maybe (Writetime WriteTimeBumper) + teamId :: Maybe TeamId, + name :: Name, + accountStatus :: Maybe AccountStatus, + handle :: Maybe Handle, + email :: Maybe EmailAddress, + colourId :: ColourId, + activated :: Activated, + serviceId :: Maybe ServiceId, + managedBy :: Maybe ManagedBy, + ssoId :: Maybe UserSSOId, + unverifiedEmail :: Maybe EmailAddress, + searchable :: Maybe Bool, + createdAt :: UTCTime, + updatedAt :: UTCTime } deriving (Eq, Show) @@ -79,75 +81,45 @@ type instance Maybe (Writetime WriteTimeBumper) ) -instance Record IndexUser where - asTuple (IndexUser {..}) = +indexUserFromTuple :: TupleType IndexUser -> IndexUser +indexUserFromTuple ( userId, - value <$> teamId, writetime <$> teamId, - name.value, name.writetime, - value <$> accountStatus, writetime <$> accountStatus, - value <$> handle, writetime <$> handle, - value <$> email, writetime <$> email, - colourId.value, colourId.writetime, - activated.value, activated.writetime, - value <$> serviceId, writetime <$> serviceId, - value <$> managedBy, writetime <$> managedBy, - value <$> ssoId, writetime <$> ssoId, - value <$> unverifiedEmail, writetime <$> unverifiedEmail, - value <$> searchable, writetime <$> searchable, - writeTimeBumper - ) - - asRecord - ( u, - mTeam, tTeam, + teamId, tTeam, name, tName, - status, tStatus, + accountStatus, tStatus, handle, tHandle, email, tEmail, - colour, tColour, + colourId, tColour, activated, tActivated, - service, tService, + serviceId, tService, managedBy, tManagedBy, ssoId, tSsoId, - emailUnvalidated, tEmailUnvalidated, + unverifiedEmail, tEmailUnvalidated, searchable, tSearchable, tWriteTimeBumper ) = IndexUser { - userId = u, - teamId = WithWriteTime <$> mTeam <*> tTeam, - name = WithWriteTime name tName, - accountStatus = WithWriteTime <$> status <*> tStatus, - handle = WithWriteTime <$> handle <*> tHandle, - email = WithWriteTime <$> email <*> tEmail, - colourId = WithWriteTime colour tColour, - activated = WithWriteTime activated tActivated, - serviceId = WithWriteTime <$> service <*> tService, - managedBy = WithWriteTime <$> managedBy <*> tManagedBy, - ssoId = WithWriteTime <$> ssoId <*> tSsoId, - unverifiedEmail = WithWriteTime <$> emailUnvalidated <*> tEmailUnvalidated, - searchable = WithWriteTime <$> searchable <*> tSearchable, - writeTimeBumper = tWriteTimeBumper + createdAt = writetimeToUTC tActivated, + updatedAt = maximum $ catMaybes [writetimeToUTC <$> tTeam, + Just $ writetimeToUTC tName, + writetimeToUTC <$> tStatus, + writetimeToUTC <$> tHandle, + writetimeToUTC <$> tEmail, + Just $ writetimeToUTC tColour, + Just $ writetimeToUTC tActivated, + writetimeToUTC <$> tService, + writetimeToUTC <$> tManagedBy, + writetimeToUTC <$> tSsoId, + writetimeToUTC <$> tEmailUnvalidated, + writetimeToUTC <$> tSearchable, + writetimeToUTC <$> tWriteTimeBumper + ], + .. } {- ORMOLU_ENABLE -} indexUserToVersion :: Maybe (WithWritetime Role) -> IndexUser -> IndexVersion -indexUserToVersion role IndexUser {..} = - mkIndexVersion - [ const () <$$> Just name.writetime, - const () <$$> fmap writetime teamId, - const () <$$> fmap writetime accountStatus, - const () <$$> fmap writetime handle, - const () <$$> fmap writetime email, - const () <$$> Just colourId.writetime, - const () <$$> Just activated.writetime, - const () <$$> fmap writetime serviceId, - const () <$$> fmap writetime managedBy, - const () <$$> fmap writetime ssoId, - const () <$$> fmap writetime unverifiedEmail, - const () <$$> fmap writetime role, - const () <$$> fmap writetime searchable, - const () <$$> writeTimeBumper - ] +indexUserToVersion role iu = + mkIndexVersion [Just $ Writetime iu.updatedAt, const () <$$> fmap writetime role] indexUserToDoc :: SearchVisibilityInbound -> Maybe UserType -> Maybe Role -> IndexUser -> UserDoc indexUserToDoc searchVisInbound mUserType mRole IndexUser {..} = @@ -156,22 +128,22 @@ indexUserToDoc searchVisInbound mUserType mRole IndexUser {..} = UserDoc { udId = userId, udType = mUserType, - udSearchable = value <$> searchable, - udEmailUnvalidated = value <$> unverifiedEmail, - udSso = sso . value =<< ssoId, - udScimExternalId = join $ scimExternalId <$> (value <$> managedBy) <*> (value <$> ssoId), + udSearchable = searchable, + udEmailUnvalidated = unverifiedEmail, + udSso = sso =<< ssoId, + udScimExternalId = join $ scimExternalId <$> (managedBy) <*> (ssoId), udSearchVisibilityInbound = Just searchVisInbound, udRole = mRole, - udCreatedAt = Just . toUTCTimeMillis $ writetimeToUTC activated.writetime, - udManagedBy = value <$> managedBy, - udSAMLIdP = idpUrl . value =<< ssoId, - udAccountStatus = value <$> accountStatus, - udColourId = Just colourId.value, - udEmail = value <$> email, - udHandle = value <$> handle, - udNormalized = Just $ normalized name.value.fromName, - udName = Just name.value, - udTeam = value <$> teamId + udCreatedAt = Just . toUTCTimeMillis $ createdAt, + udManagedBy = managedBy, + udSAMLIdP = idpUrl =<< ssoId, + udAccountStatus = accountStatus, + udColourId = Just colourId, + udEmail = email, + udHandle = handle, + udNormalized = Just $ normalized name.fromName, + udName = Just name, + udTeam = teamId } else -- We insert a tombstone-style user here, as it's easier than -- deleting the old one. It's mostly empty, but having the status here @@ -179,7 +151,7 @@ indexUserToDoc searchVisInbound mUserType mRole IndexUser {..} = emptyUserDoc userId where shouldIndex = - ( case value <$> accountStatus of + ( case accountStatus of Nothing -> True Just Active -> True Just Suspended -> True @@ -187,7 +159,7 @@ indexUserToDoc searchVisInbound mUserType mRole IndexUser {..} = Just Ephemeral -> False Just PendingInvitation -> False ) - && activated.value -- FUTUREWORK: how is this adding to the first case? + && activated -- FUTUREWORK: how is this adding to the first case? && isNothing serviceId idpUrl :: UserSSOId -> Maybe Text diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index c6524d8e6b4..1db607a9614 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -729,10 +729,10 @@ syncUserIndex uid = vis <- maybe (pure defaultSearchVisibilityInbound) - (teamSearchVisibilityInbound . value) + teamSearchVisibilityInbound indexUser.teamId - tm <- maybe (pure Nothing) (selectTeamMember . value) indexUser.teamId - userType <- getUserType indexUser.userId (indexUser.teamId <&> (.value)) (indexUser.serviceId <&> (.value)) + tm <- maybe (pure Nothing) selectTeamMember indexUser.teamId + userType <- getUserType indexUser.userId indexUser.teamId indexUser.serviceId let mRole = tm >>= mkRoleWithWriteTime userDoc = indexUserToDoc vis (Just userType) (value <$> mRole) indexUser version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index 1aa38695158..3711c0a4692 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -19,7 +19,6 @@ module Wire.MockInterpreters.UserStore where -import Cassandra.Util import Data.Handle import Data.Id import Data.Time @@ -150,22 +149,23 @@ storedUserToIndexUser :: StoredUser -> IndexUser storedUserToIndexUser storedUser = -- If we really care about this, we could start storing the writetimes, but we -- don't need it right now - let withDefaultTime x = WithWriteTime x $ Writetime $ UTCTime (YearDay 0 1) 0 + let defaultTime = UTCTime (YearDay 0 1) 0 in IndexUser { userId = storedUser.id, - teamId = withDefaultTime <$> storedUser.teamId, - name = withDefaultTime storedUser.name, - accountStatus = withDefaultTime <$> storedUser.status, - handle = withDefaultTime <$> storedUser.handle, - email = withDefaultTime <$> storedUser.email, - colourId = withDefaultTime storedUser.accentId, - activated = withDefaultTime storedUser.activated, - serviceId = withDefaultTime <$> storedUser.serviceId, - managedBy = withDefaultTime <$> storedUser.managedBy, - ssoId = withDefaultTime <$> storedUser.ssoId, + teamId = storedUser.teamId, + name = storedUser.name, + accountStatus = storedUser.status, + handle = storedUser.handle, + email = storedUser.email, + colourId = storedUser.accentId, + activated = storedUser.activated, + serviceId = storedUser.serviceId, + managedBy = storedUser.managedBy, + ssoId = storedUser.ssoId, unverifiedEmail = Nothing, - searchable = withDefaultTime <$> storedUser.searchable, - writeTimeBumper = Nothing + searchable = storedUser.searchable, + createdAt = defaultTime, + updatedAt = defaultTime } lookupLocaleImpl :: (Member (State [StoredUser]) r) => UserId -> Sem r (Maybe ((Maybe Language, Maybe Country))) From e148c6bf7bc54a6a0cf9c17fc19687c8e5d7c358 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 20 Jan 2026 11:41:17 +0100 Subject: [PATCH 08/16] Introduce GeneralPaginationState type to be able to paginate over cassandra or postgres --- libs/cassandra-util/src/Cassandra.hs | 3 ++ libs/cassandra-util/src/Cassandra/Exec.hs | 35 ++++++++++++++----- .../src/Wire/Sem/Paging/Cassandra.hs | 2 +- libs/wire-api/src/Wire/API/Team/Member.hs | 9 +++-- libs/wire-subsystems/src/Wire/UserStore.hs | 8 ++--- .../src/Wire/UserStore/Cassandra.hs | 12 +++---- services/brig/src/Brig/API/Public.hs | 4 +-- services/brig/src/Brig/Data/Connection.hs | 4 +-- services/galley/src/Galley/API/Teams.hs | 4 +-- services/galley/src/Galley/Cassandra/Team.hs | 2 +- 10 files changed, 54 insertions(+), 29 deletions(-) diff --git a/libs/cassandra-util/src/Cassandra.hs b/libs/cassandra-util/src/Cassandra.hs index 74dcdfc45f4..24406334813 100644 --- a/libs/cassandra-util/src/Cassandra.hs +++ b/libs/cassandra-util/src/Cassandra.hs @@ -58,6 +58,7 @@ import Cassandra.Exec as C ( BatchM, Client, ClientState, + GeneralPaginationState (..), MonadClient, Page (..), PageWithState (..), @@ -74,6 +75,8 @@ import Cassandra.Exec as C paginate, paginateC, paginateWithState, + paginationStateCassandra, + paginationStatePostgres, params, paramsP, paramsPagingState, diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index 8ef7d64337c..b5a6eb02bc8 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -25,6 +25,9 @@ module Cassandra.Exec x5, x1, paginateC, + GeneralPaginationState (..), + paginationStateCassandra, + paginationStatePostgres, PageWithState (..), paginateWithState, paginateWithStateC, @@ -97,9 +100,23 @@ paginateC q p r = go =<< lift (retry r (paginate q p)) when (hasMore page) $ go =<< lift (retry r (liftClient (nextPage page))) -data PageWithState a = PageWithState - { pwsResults :: [a], - pwsState :: Maybe Protocol.PagingState +data GeneralPaginationState a + = PaginationStateCassandra Protocol.PagingState + | PaginationStatePostgres a + +paginationStateCassandra :: GeneralPaginationState pgState -> Maybe Protocol.PagingState +paginationStateCassandra = \case + PaginationStateCassandra state -> Just state + PaginationStatePostgres {} -> Nothing + +paginationStatePostgres :: GeneralPaginationState pgState -> Maybe pgState +paginationStatePostgres = \case + PaginationStatePostgres pgState -> Just pgState + PaginationStateCassandra {} -> Nothing + +data PageWithState state res = PageWithState + { pwsResults :: [res], + pwsState :: Maybe (GeneralPaginationState state) } deriving (Functor) @@ -107,13 +124,13 @@ data PageWithState a = PageWithState -- serialised and sent to consumers of the API. The state is not good for long -- term storage as the bytestring format may change when the schema of a table -- changes or when cassandra is upgraded. -paginateWithState :: (MonadClient m, Tuple a, Tuple b, RunQ q) => q R a b -> QueryParams a -> m (PageWithState b) +paginateWithState :: (MonadClient m, Tuple a, Tuple b, RunQ q) => q R a b -> QueryParams a -> m (PageWithState x b) paginateWithState q p = do let p' = p {Protocol.pageSize = Protocol.pageSize p <|> Just 10000} r <- runQ q p' getResult r >>= \case Protocol.RowsResult m b -> - pure $ PageWithState b (pagingState m) + pure $ PageWithState b (PaginationStateCassandra <$> pagingState m) _ -> throwM $ UnexpectedResponse (hrHost r) (hrResponse r) -- | Like 'paginateWithState' but returns a conduit instead of one page. @@ -128,20 +145,20 @@ paginateWithState q p = do -- where -- getUsers state = paginateWithState getUsersQuery (paramsPagingState Quorum () 10000 state) -- @ -paginateWithStateC :: forall m a. (Monad m) => (Maybe Protocol.PagingState -> m (PageWithState a)) -> ConduitT () [a] m () +paginateWithStateC :: forall m res state. (Monad m) => (Maybe (GeneralPaginationState state) -> m (PageWithState state res)) -> ConduitT () [res] m () paginateWithStateC getPage = do go =<< lift (getPage Nothing) where - go :: PageWithState a -> ConduitT () [a] m () + go :: PageWithState state res -> ConduitT () [res] m () go page = do unless (null page.pwsResults) $ yield (page.pwsResults) when (pwsHasMore page) $ - go =<< lift (getPage page.pwsState) + go =<< lift (getPage $ page.pwsState) paramsPagingState :: Consistency -> a -> Int32 -> Maybe Protocol.PagingState -> QueryParams a paramsPagingState c p n state = QueryParams c False p (Just n) state Nothing Nothing {-# INLINE paramsPagingState #-} -pwsHasMore :: PageWithState a -> Bool +pwsHasMore :: PageWithState a b -> Bool pwsHasMore = isJust . pwsState diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs index 12210c3c8a3..a775a56beb3 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs @@ -43,7 +43,7 @@ data CassandraPaging type instance E.PagingState CassandraPaging a = PagingState -type instance E.Page CassandraPaging a = PageWithState a +type instance E.Page CassandraPaging a = PageWithState Void a type instance E.PagingBounds CassandraPaging TeamId = Range 1 100 Int32 diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 2d75ce2e04d..55453872d7c 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -263,8 +263,13 @@ instance ToSchema TeamMembersPage where type TeamMembersPagingState = MultiTablePagingState TeamMembersPagingName TeamMembersTable -teamMemberPagingState :: PageWithState TeamMember -> TeamMembersPagingState -teamMemberPagingState p = MultiTablePagingState TeamMembersTable (LBS.toStrict . C.unPagingState <$> pwsState p) +teamMemberPagingState :: PageWithState Void TeamMember -> TeamMembersPagingState +teamMemberPagingState p = + MultiTablePagingState + TeamMembersTable + ( LBS.toStrict . C.unPagingState + <$> (C.paginationStateCassandra =<< p.pwsState) + ) instance ToParamSchema TeamMembersPagingState where toParamSchema _ = toParamSchema (Proxy @Text) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index a7bc2e9fcdb..df314e5fb00 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -19,7 +19,7 @@ module Wire.UserStore where -import Cassandra (PageWithState (..), PagingState) +import Cassandra (GeneralPaginationState, PageWithState (..)) import Data.Default import Data.Handle import Data.Id @@ -72,7 +72,7 @@ data UserStore m a where CreateUser :: NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> UserStore m () GetIndexUser :: UserId -> UserStore m (Maybe IndexUser) DoesUserExist :: UserId -> UserStore m Bool - GetIndexUsersPaginated :: Int32 -> Maybe PagingState -> UserStore m (PageWithState IndexUser) + GetIndexUsersPaginated :: Int32 -> Maybe (GeneralPaginationState Void) -> UserStore m (PageWithState Void IndexUser) GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () UpdateEmail :: UserId -> EmailAddress -> UserStore m () @@ -112,8 +112,8 @@ data UserStore m a where UpdateFeatureConferenceCalling :: UserId -> Maybe FeatureStatus -> UserStore m () LookupFeatureConferenceCalling :: UserId -> UserStore m (Maybe FeatureStatus) DeleteServiceUser :: ProviderId -> ServiceId -> BotId -> UserStore m () - LookupServiceUsers :: ProviderId -> ServiceId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId, Maybe TeamId)) - LookupServiceUsersForTeam :: ProviderId -> ServiceId -> TeamId -> Maybe PagingState -> UserStore m (PageWithState (BotId, ConvId)) + LookupServiceUsers :: ProviderId -> ServiceId -> Maybe (GeneralPaginationState Void) -> UserStore m (PageWithState Void (BotId, ConvId, Maybe TeamId)) + LookupServiceUsersForTeam :: ProviderId -> ServiceId -> TeamId -> Maybe (GeneralPaginationState Void) -> UserStore m (PageWithState Void (BotId, ConvId)) makeSem ''UserStore diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 98b930f0cf1..ce3da6246a7 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -47,7 +47,7 @@ interpretUserStoreCassandra casClient = GetUsers uids -> getUsersImpl uids DoesUserExist uid -> doesUserExistImpl uid GetIndexUser uid -> getIndexUserImpl uid - GetIndexUsersPaginated pageSize mPagingState -> getIndexUserPaginatedImpl pageSize mPagingState + GetIndexUsersPaginated pageSize mPagingState -> getIndexUserPaginatedImpl pageSize (paginationStateCassandra =<< mPagingState) UpdateUser uid update -> updateUserImpl uid update UpdateEmail uid email -> updateEmailImpl uid email UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid email @@ -77,8 +77,8 @@ interpretUserStoreCassandra casClient = DeleteEmail uid -> deleteEmailImpl uid SetUserSearchable uid searchable -> setUserSearchableImpl uid searchable DeleteServiceUser pid sid bid -> deleteServiceUserImpl pid sid bid - LookupServiceUsers pid sid mPagingState -> lookupServiceUsersImpl pid sid mPagingState - LookupServiceUsersForTeam pid sid tid mPagingState -> lookupServiceUsersForTeamImpl pid sid tid mPagingState + LookupServiceUsers pid sid mPagingState -> lookupServiceUsersImpl pid sid (paginationStateCassandra =<< mPagingState) + LookupServiceUsersForTeam pid sid tid mPagingState -> lookupServiceUsersForTeamImpl pid sid tid (paginationStateCassandra =<< mPagingState) createUserImpl :: NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> Client () createUserImpl new mbConv = retry x5 . batch $ do @@ -122,7 +122,7 @@ getIndexUserImpl u = do cql :: PrepQuery R (Identity UserId) (TupleType IndexUser) cql = prepared . QueryString $ getIndexUserBaseQuery <> " WHERE id = ?" -getIndexUserPaginatedImpl :: Int32 -> Maybe PagingState -> Client (PageWithState IndexUser) +getIndexUserPaginatedImpl :: Int32 -> Maybe PagingState -> Client (PageWithState x IndexUser) getIndexUserPaginatedImpl pageSize mPagingState = indexUserFromTuple <$$> paginateWithState cql (paramsPagingState LocalQuorum () pageSize mPagingState) where @@ -414,7 +414,7 @@ lookupServiceUsersImpl :: ProviderId -> ServiceId -> Maybe PagingState -> - Client (PageWithState (BotId, ConvId, Maybe TeamId)) + Client (PageWithState Void (BotId, ConvId, Maybe TeamId)) lookupServiceUsersImpl pid sid mPagingState = paginateWithState cql (paramsPagingState LocalQuorum (pid, sid) 100 mPagingState) where @@ -428,7 +428,7 @@ lookupServiceUsersForTeamImpl :: ServiceId -> TeamId -> Maybe PagingState -> - Client (PageWithState (BotId, ConvId)) + Client (PageWithState Void (BotId, ConvId)) lookupServiceUsersForTeamImpl pid sid tid mPagingState = paginateWithState cql (paramsPagingState LocalQuorum (pid, sid, tid) 100 mPagingState) where diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 89466550dbc..c3f262d8a95 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1380,14 +1380,14 @@ listConnections uid Public.GetMultiTablePageRequest {..} = do Just (Public.ConnectionPagingState Public.PagingRemotes stateBS) -> remotesOnly self (mkState <$> stateBS) (fromRange gmtprSize) _ -> localsAndRemotes self (fmap mkState . Public.mtpsState =<< gmtprState) gmtprSize where - pageToConnectionsPage :: Public.LocalOrRemoteTable -> Data.PageWithState Public.UserConnection -> Public.ConnectionsPage + pageToConnectionsPage :: Public.LocalOrRemoteTable -> Data.PageWithState Void Public.UserConnection -> Public.ConnectionsPage pageToConnectionsPage table page@Data.PageWithState {..} = Public.MultiTablePage { mtpResults = pwsResults, mtpHasMore = C.pwsHasMore page, -- FUTUREWORK confusingly, using 'ConversationPagingState' instead of 'ConnectionPagingState' doesn't fail any tests. -- Is this type actually useless? Or the tests not good enough? - mtpPagingState = Public.ConnectionPagingState table (LBS.toStrict . C.unPagingState <$> pwsState) + mtpPagingState = Public.ConnectionPagingState table (LBS.toStrict . C.unPagingState <$> (Data.paginationStateCassandra =<< pwsState)) } mkState :: ByteString -> C.PagingState diff --git a/services/brig/src/Brig/Data/Connection.hs b/services/brig/src/Brig/Data/Connection.hs index fbe8221018e..3a1525b8b32 100644 --- a/services/brig/src/Brig/Data/Connection.hs +++ b/services/brig/src/Brig/Data/Connection.hs @@ -176,7 +176,7 @@ lookupLocalConnectionsPage :: Local UserId -> Maybe PagingState -> Range 1 1000 Int32 -> - m (PageWithState UserConnection) + m (PageWithState Void UserConnection) lookupLocalConnectionsPage self pagingState (fromRange -> size) = fmap (toLocalUserConnection self) <$> paginateWithState connectionsSelect (paramsPagingState LocalQuorum (Identity (tUnqualified self)) size pagingState) @@ -186,7 +186,7 @@ lookupRemoteConnectionsPage :: Local UserId -> Maybe PagingState -> Int32 -> - m (PageWithState UserConnection) + m (PageWithState Void UserConnection) lookupRemoteConnectionsPage self pagingState size = fmap (toRemoteUserConnection self) <$> paginateWithState diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 7bee76d5937..b8d6f5105a0 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -417,7 +417,7 @@ getTeamMembers lzusr tid mbMaxResults mbPagingState = do let mLimit = fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults if member `hasPermission` SearchContacts then do - pws :: PageWithState TeamMember <- E.listTeamMembers @CassandraPaging tid mState mLimit + pws :: PageWithState Void TeamMember <- E.listTeamMembers @CassandraPaging tid mState mLimit -- FUTUREWORK: Remove this via-Brig filtering when user and -- team_member tables are migrated to Postgres. We currently -- can't filter in the database because Cassandra doesn't @@ -447,7 +447,7 @@ getTeamMembers lzusr tid mbMaxResults mbPagingState = do let uids = uid : maybeToList invitee TeamSubsystem.internalSelectTeamMembers tid uids <&> toTeamSingleMembersPage member where - toTeamMembersPage :: TeamMember -> C.PageWithState TeamMember -> TeamMembersPage + toTeamMembersPage :: TeamMember -> C.PageWithState Void TeamMember -> TeamMembersPage toTeamMembersPage member p = let withPerms = (member `canSeePermsOf`) in TeamMembersPage $ diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index 40513789326..e541e7b136a 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -177,7 +177,7 @@ teamMembersPageFrom :: TeamId -> Maybe PagingState -> Range 1 HardTruncationLimit Int32 -> - Client (PageWithState TeamMember) + Client (PageWithState Void TeamMember) teamMembersPageFrom lh tid pagingState (fromRange -> max) = do page <- paginateWithState Cql.selectTeamMembers (paramsPagingState LocalQuorum (Identity tid) max pagingState) members <- mapM (newTeamMember' lh tid) (pwsResults page) From 11ecb3fe15371e44a3399e67dda787b4639272fc Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 13 Jan 2026 17:10:57 +0100 Subject: [PATCH 09/16] UserStore.Postgres: Implement createUser --- libs/wire-api/src/Wire/API/Asset.hs | 7 + libs/wire-api/src/Wire/API/Locale.hs | 17 ++ libs/wire-api/src/Wire/API/Password.hs | 16 +- .../wire-api/src/Wire/API/PostgresMarshall.hs | 29 +++ libs/wire-api/src/Wire/API/User.hs | 41 +++- .../src/Wire/API/User/EmailAddress.hs | 9 + libs/wire-api/src/Wire/API/User/Identity.hs | 2 + libs/wire-api/src/Wire/API/User/Profile.hs | 31 ++- .../20260113140936-create-user-tables.sql | 45 ++++ .../src/Wire/PostgresMigrations.hs | 3 +- .../src/Wire/UserStore/Postgres.hs | 228 ++++++++++++++++++ libs/wire-subsystems/wire-subsystems.cabal | 1 + 12 files changed, 403 insertions(+), 26 deletions(-) create mode 100644 libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql create mode 100644 libs/wire-subsystems/src/Wire/UserStore/Postgres.hs diff --git a/libs/wire-api/src/Wire/API/Asset.hs b/libs/wire-api/src/Wire/API/Asset.hs index ac51bde02f9..d822626e0e3 100644 --- a/libs/wire-api/src/Wire/API/Asset.hs +++ b/libs/wire-api/src/Wire/API/Asset.hs @@ -94,6 +94,7 @@ import Imports import Servant import URI.ByteString import Wire.API.Error +import Wire.API.PostgresMarshall import Wire.API.Routes.MultiVerb import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) @@ -200,6 +201,12 @@ instance C.Cql AssetKey where fromCql (C.CqlText txt) = runParser parser . T.encodeUtf8 $ txt fromCql _ = Left "AssetKey: Text expected" +instance PostgresMarshall Text AssetKey where + postgresMarshall = assetKeyToText + +instance PostgresUnmarshall Text AssetKey where + postgresUnmarshall = mapLeft (\e -> "failed to parse AssetKey: " <> T.pack e) . runParser parser . T.encodeUtf8 + -------------------------------------------------------------------------------- -- AssetToken diff --git a/libs/wire-api/src/Wire/API/Locale.hs b/libs/wire-api/src/Wire/API/Locale.hs index 576c7eeeb10..a9005d0c549 100644 --- a/libs/wire-api/src/Wire/API/Locale.hs +++ b/libs/wire-api/src/Wire/API/Locale.hs @@ -47,6 +47,7 @@ import Data.Time.Format import Data.Time.LocalTime (TimeZone (..), utc) import Imports import Test.QuickCheck +import Wire.API.PostgresMarshall import Wire.API.User.Orphans () import Wire.Arbitrary @@ -181,6 +182,14 @@ instance C.Cql Language where Nothing -> Left "Language: ISO 639-1 expected." fromCql _ = Left "Language: ASCII expected" +instance PostgresMarshall Text Language where + postgresMarshall = lan2Text + +instance PostgresUnmarshall Text Language where + postgresUnmarshall = + mapLeft (\e -> "failed to parse Language: " <> Text.pack e) + . parseOnly languageParser + languageParser :: Parser Language languageParser = codeParser "language" $ fmap Language . checkAndConvert isLower @@ -206,6 +215,14 @@ instance C.Cql Country where Nothing -> Left "Country: ISO 3166-1-alpha2 expected." fromCql _ = Left "Country: ASCII expected" +instance PostgresMarshall Text Country where + postgresMarshall = con2Text + +instance PostgresUnmarshall Text Country where + postgresUnmarshall = + mapLeft (\e -> "failed to parse Country: " <> Text.pack e) + . parseOnly countryParser + countryParser :: Parser Country countryParser = codeParser "country" $ fmap Country . checkAndConvert isUpper diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs index dc5cb9b9df9..57820e48993 100644 --- a/libs/wire-api/src/Wire/API/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -36,11 +36,13 @@ import Data.ByteString.Lazy (fromStrict, toStrict) import Data.Misc import Data.OpenApi qualified as S import Data.Schema +import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Imports import OpenSSL.Random (randBytes) import Wire.API.Password.Argon2id import Wire.API.Password.Scrypt +import Wire.API.PostgresMarshall -- | A derived, stretched password that can be safely stored. data Password @@ -56,11 +58,15 @@ instance Cql Password where fromCql (CqlBlob lbs) = parsePassword . Text.decodeUtf8 . toStrict $ lbs fromCql _ = Left "password: expected blob" - toCql pw = CqlBlob . fromStrict $ Text.encodeUtf8 encoded - where - encoded = case pw of - Argon2Password argon2pw -> encodeArgon2HashedPassword argon2pw - ScryptPassword scryptpw -> encodeScryptPassword scryptpw + toCql = CqlBlob . fromStrict . Text.encodeUtf8 . postgresMarshall + +instance PostgresMarshall Text Password where + postgresMarshall = \case + Argon2Password argon2pw -> encodeArgon2HashedPassword argon2pw + ScryptPassword scryptpw -> encodeScryptPassword scryptpw + +instance PostgresUnmarshall Text Password where + postgresUnmarshall = mapLeft Text.pack . parsePassword ------------------------------------------------------------------------------- diff --git a/libs/wire-api/src/Wire/API/PostgresMarshall.hs b/libs/wire-api/src/Wire/API/PostgresMarshall.hs index ee783843128..15b1ecfb5d0 100644 --- a/libs/wire-api/src/Wire/API/PostgresMarshall.hs +++ b/libs/wire-api/src/Wire/API/PostgresMarshall.hs @@ -18,6 +18,7 @@ module Wire.API.PostgresMarshall ( PostgresMarshall (..), PostgresUnmarshall (..), + StoreAsJSON (..), lmapPG, rmapPG, dimapPG, @@ -29,12 +30,15 @@ import Data.Bifunctor (first) import Data.ByteString qualified as BS import Data.ByteString.Conversion qualified as BSC import Data.Domain +import Data.Handle import Data.Id +import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis), toUTCTimeMillis) import Data.Misc import Data.Profunctor import Data.Set qualified as Set import Data.Text qualified as Text import Data.Text.Encoding qualified as Text +import Data.Time (UTCTime) import Data.UUID import Data.Vector (Vector) import Data.Vector qualified as V @@ -518,6 +522,12 @@ instance PostgresMarshall Int64 Milliseconds where instance PostgresMarshall Text Domain where postgresMarshall = domainText +instance PostgresMarshall Text Handle where + postgresMarshall = fromHandle + +instance PostgresMarshall UTCTime UTCTimeMillis where + postgresMarshall = fromUTCTimeMillis + instance (PostgresMarshall a b) => PostgresMarshall (Maybe a) (Maybe b) where postgresMarshall = fmap postgresMarshall @@ -855,6 +865,12 @@ instance (PostgresUnmarshall a b, Ord b) => PostgresUnmarshall (Vector a) (Set b instance PostgresUnmarshall Int64 Milliseconds where postgresUnmarshall = Right . int64ToMs +instance PostgresUnmarshall Text Handle where + postgresUnmarshall = mapLeft Text.pack . parseHandleEither + +instance PostgresUnmarshall UTCTime UTCTimeMillis where + postgresUnmarshall = Right . toUTCTimeMillis + --- lmapPG :: (PostgresMarshall db domain, Profunctor p) => p db x -> p domain x @@ -868,3 +884,16 @@ dimapPG :: Statement dbIn dbOut -> Statement domainIn domainOut dimapPG = refineResult postgresUnmarshall . lmapPG + +--- + +newtype StoreAsJSON a = StoreAsJSON a + +instance (ToJSON a) => PostgresMarshall Value (StoreAsJSON a) where + postgresMarshall (StoreAsJSON a) = toJSON a + +instance (FromJSON a) => PostgresUnmarshall Value (StoreAsJSON a) where + postgresUnmarshall v = + case fromJSON v of + Error e -> Left $ Text.pack e + Success a -> Right $ StoreAsJSON a diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index defa322a590..4bef9277ccd 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -185,6 +185,7 @@ import Data.Schema import Data.Schema qualified as Schema import Data.Set qualified as Set import Data.Text qualified as T +import Data.Text qualified as Text import Data.Text.Ascii import Data.Text.Encoding qualified as T import Data.Text.Encoding.Error @@ -205,6 +206,7 @@ import Wire.API.Error.Brig import Wire.API.Error.Brig qualified as E import Wire.API.Locale import Wire.API.Password +import Wire.API.PostgresMarshall import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.MultiVerb import Wire.API.Team @@ -1838,21 +1840,28 @@ instance Schema.ToSchema AccountStatus where instance C.Cql AccountStatus where ctype = C.Tagged C.IntColumn - toCql Active = C.CqlInt 0 - toCql Suspended = C.CqlInt 1 - toCql Deleted = C.CqlInt 2 - toCql Ephemeral = C.CqlInt 3 - toCql PendingInvitation = C.CqlInt 4 - - fromCql (C.CqlInt i) = case i of - 0 -> pure Active - 1 -> pure Suspended - 2 -> pure Deleted - 3 -> pure Ephemeral - 4 -> pure PendingInvitation - n -> Left $ "unexpected account status: " ++ show n + toCql = C.CqlInt . postgresMarshall + + fromCql (C.CqlInt i) = mapLeft Text.unpack $ postgresUnmarshall i fromCql _ = Left "account status: int expected" +instance PostgresMarshall Int32 AccountStatus where + postgresMarshall = \case + Active -> 0 + Suspended -> 1 + Deleted -> 2 + Ephemeral -> 3 + PendingInvitation -> 4 + +instance PostgresUnmarshall Int32 AccountStatus where + postgresUnmarshall = \case + 0 -> Right Active + 1 -> Right Suspended + 2 -> Right Deleted + 3 -> Right Ephemeral + 4 -> Right PendingInvitation + n -> Left $ "unexpected account status: " <> Text.show n + data AccountStatusResp = AccountStatusResp {fromAccountStatusResp :: AccountStatus} deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform AccountStatusResp) @@ -1992,6 +2001,12 @@ instance C.Cql (Imports.Set BaseProtocolTag) where fromCql (C.CqlInt bits) = pure $ protocolSetFromBits (fromIntegral bits) fromCql _ = Left "Protocol set: Int expected" +instance PostgresMarshall Int32 (Imports.Set BaseProtocolTag) where + postgresMarshall = fromIntegral . protocolSetBits + +instance PostgresUnmarshall Int32 (Imports.Set BaseProtocolTag) where + postgresUnmarshall = Right . protocolSetFromBits . fromIntegral + baseProtocolMask :: BaseProtocolTag -> Word32 baseProtocolMask BaseProtocolProteusTag = 1 baseProtocolMask BaseProtocolMLSTag = 2 diff --git a/libs/wire-api/src/Wire/API/User/EmailAddress.hs b/libs/wire-api/src/Wire/API/User/EmailAddress.hs index 1b3a58554e1..9bde18007ec 100644 --- a/libs/wire-api/src/Wire/API/User/EmailAddress.hs +++ b/libs/wire-api/src/Wire/API/User/EmailAddress.hs @@ -50,6 +50,7 @@ import Servant.API qualified as S import Test.QuickCheck import Text.Email.Parser import Text.Email.Validate +import Wire.API.PostgresMarshall -------------------------------------------------------------------------------- -- Email @@ -103,6 +104,14 @@ instance C.Cql EmailAddress where toCql = C.toCql . fromEmail +instance PostgresMarshall Text EmailAddress where + postgresMarshall = fromEmail + +instance PostgresUnmarshall Text EmailAddress where + postgresUnmarshall t = case emailAddressText t of + Just e -> Right e + Nothing -> Left "postgresUnmarshall: Invalid email" + fromEmail :: EmailAddress -> Text fromEmail = decodeUtf8 . toByteString diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 97a3c503e59..edcc3c3d842 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -71,6 +71,7 @@ import Text.Email.Parser import URI.ByteString qualified as URI import URI.ByteString.QQ (uri) import Web.Scim.Schema.User.Email () +import Wire.API.PostgresMarshall import Wire.API.User.EmailAddress import Wire.API.User.Phone import Wire.API.User.Profile (fromName, mkName) @@ -150,6 +151,7 @@ data UserSSOId | UserScimExternalId Text deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserSSOId) + deriving (PostgresMarshall A.Value, PostgresUnmarshall A.Value) via (StoreAsJSON UserSSOId) isUserSSOId :: UserSSOId -> Bool isUserSSOId (UserSSOId _) = True diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index d3634799df6..36cc322af0c 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -58,6 +58,7 @@ import Data.Text.Encoding qualified as TE import Imports import Web.HttpApiData (FromHttpApiData (..), ToHttpApiData (..)) import Wire.API.Asset (AssetKey (..)) +import Wire.API.PostgresMarshall import Wire.API.User.Orphans () import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -69,7 +70,7 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) newtype Name = Name {fromName :: Text} deriving stock (Eq, Ord, Show, Generic) - deriving newtype (FromByteString, ToByteString) + deriving newtype (FromByteString, ToByteString, PostgresMarshall Text, PostgresUnmarshall Text) deriving (Arbitrary) via (Ranged 1 128 Text) deriving (FromJSON, ToJSON, S.ToSchema) via Schema Name @@ -88,7 +89,7 @@ deriving instance C.Cql Name newtype TextStatus = TextStatus {fromTextStatus :: Text} deriving stock (Eq, Ord, Show, Generic) - deriving newtype (FromByteString, ToByteString) + deriving newtype (FromByteString, ToByteString, PostgresMarshall Text, PostgresUnmarshall Text) deriving (Arbitrary) via (Ranged 1 256 Text) deriving (FromJSON, ToJSON, S.ToSchema) via Schema TextStatus @@ -105,7 +106,7 @@ deriving instance C.Cql TextStatus newtype ColourId = ColourId {fromColourId :: Int32} deriving stock (Eq, Ord, Show, Generic) - deriving newtype (Num, ToSchema, Arbitrary) + deriving newtype (Num, ToSchema, Arbitrary, PostgresMarshall Int32, PostgresUnmarshall Int32) deriving (FromJSON, ToJSON, S.ToSchema) via Schema ColourId defaultAccentId :: ColourId @@ -193,12 +194,21 @@ instance ToSchema AssetSize where instance C.Cql AssetSize where ctype = C.Tagged C.IntColumn - fromCql (C.CqlInt 0) = pure AssetPreview - fromCql (C.CqlInt 1) = pure AssetComplete + fromCql (C.CqlInt n) = mapLeft Text.unpack $ postgresUnmarshall n fromCql n = Left $ "Unexpected asset size: " ++ show n - toCql AssetPreview = C.CqlInt 0 - toCql AssetComplete = C.CqlInt 1 + toCql = C.CqlInt . postgresMarshall + +instance PostgresMarshall Int32 AssetSize where + postgresMarshall = \case + AssetPreview -> 0 + AssetComplete -> 1 + +instance PostgresUnmarshall Int32 AssetSize where + postgresUnmarshall = \case + 0 -> Right AssetPreview + 1 -> Right AssetComplete + n -> Left $ "Unexpected asset size: " <> Text.show n -------------------------------------------------------------------------------- -- ManagedBy @@ -260,6 +270,12 @@ instance C.Cql ManagedBy where toCql = C.CqlInt . managedByToInt32 +instance PostgresMarshall Int32 ManagedBy where + postgresMarshall = managedByToInt32 + +instance PostgresUnmarshall Int32 ManagedBy where + postgresUnmarshall = managedByFromInt32 + defaultManagedBy :: ManagedBy defaultManagedBy = ManagedByWire @@ -281,6 +297,7 @@ managedByFromInt32 = \case newtype Pict = Pict {fromPict :: [A.Object]} deriving stock (Eq, Ord, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via Schema Pict + deriving (PostgresMarshall A.Value, PostgresUnmarshall A.Value) via StoreAsJSON Pict instance ToSchema Pict where schema = diff --git a/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql b/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql new file mode 100644 index 00000000000..b5d11bba292 --- /dev/null +++ b/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql @@ -0,0 +1,45 @@ +CREATE TABLE wire_user ( + id uuid PRIMARY KEY, + accent_id integer NOT NULL, + activated boolean NOT NULL, + country text, + email text, + email_unvalidated text, + expires timestamptz, + feature_conference_calling integer, + handle text, + language text, + managed_by integer, + name text NOT NULL, + password text, + picture jsonb, + provider uuid, + service uuid, + searchable boolean, + sso_id jsonb, + account_status integer, + supported_protocols integer, + team uuid, + text_status text, + write_time_bumper integer +); + +CREATE INDEX wire_user_service_idx ON wire_user(provider, service); + +CREATE TABLE asset ( + user_id uuid NOT NULL, + typ integer NOT NULL, + key text NOT NULL, + size integer +); + +CREATE INDEX asset_user_id_idx ON asset (user_id); + +CREATE TABLE bot_conv ( + id uuid PRIMARY KEY, + conv uuid NOT NULL, + conv_team uuid +); + +CREATE INDEX bot_conv_conv_idx ON bot_conv (conv); +CREATE INDEX bot_conv_team_idx ON bot_conv (conv_team); diff --git a/libs/wire-subsystems/src/Wire/PostgresMigrations.hs b/libs/wire-subsystems/src/Wire/PostgresMigrations.hs index df3313ccc76..b24057974a5 100644 --- a/libs/wire-subsystems/src/Wire/PostgresMigrations.hs +++ b/libs/wire-subsystems/src/Wire/PostgresMigrations.hs @@ -41,7 +41,8 @@ instance Exception PostgresMigrationError runAllMigrations :: Pool -> Logger -> IO () runAllMigrations pool logger = do let session = do - Log.info logger $ Log.msg (Log.val "Running migrations") + Log.info logger $ + Log.msg (Log.val "Running migrations") transaction Serializable Write $ do forM_ (MigrationInitialization : allMigrations) $ \migrationCmd -> do mErr <- runMigration migrationCmd diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs new file mode 100644 index 00000000000..07970bb1820 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -0,0 +1,228 @@ +{-# OPTIONS_GHC -Wwarn #-} + +module Wire.UserStore.Postgres where + +import Cassandra (PageWithState, paginationStatePostgres) +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Time +import Data.Vector (Vector) +import Hasql.Statement qualified as Hasql +import Hasql.TH (resultlessStatement) +import Hasql.Transaction qualified as Transaction +import Hasql.Transaction.Sessions +import Imports +import Polysemy +import Wire.API.Asset +import Wire.API.Password +import Wire.API.PostgresMarshall +import Wire.API.Team.Feature (FeatureStatus) +import Wire.API.User hiding (DeleteUser) +import Wire.API.User.RichInfo +import Wire.API.User.Search +import Wire.Postgres +import Wire.StoredUser +import Wire.UserStore +import Wire.UserStore.IndexUser + +interpretUserStoreCassandra :: (PGConstraints r) => InterpreterFor UserStore r +interpretUserStoreCassandra = + interpret $ \case + CreateUser new mbConv -> createUserImpl new mbConv + GetUsers uids -> getUsersImpl uids + GetIndexUser uid -> getIndexUserImpl uid + GetIndexUsersPaginated pageSize mPagingState -> getIndexUsersPaginatedImpl pageSize (paginationStatePostgres =<< mPagingState) + UpdateUser uid update -> updateUserImpl uid update + UpdateEmail uid email -> updateEmailImpl uid email + UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid email + DeleteEmailUnvalidated uid -> deleteEmailUnvalidatedImpl uid + UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update + UpdateSSOId uid ssoId -> updateSSOIdImpl uid ssoId + UpdateManagedBy uid managedBy -> updateManagedByImpl uid managedBy + UpdateAccountStatus uid accountStatus -> updateAccountStatusImpl uid accountStatus + UpdateRichInfo uid richInfo -> updateRichInfoImpl uid richInfo + UpdateFeatureConferenceCalling uid feat -> updateFeatureConferenceCallingImpl uid feat + DeleteUser user -> deleteUserImpl user + LookupHandle hdl -> lookupHandleImpl hdl + GlimpseHandle hdl -> lookupHandleImpl hdl + LookupStatus uid -> lookupStatusImpl uid + IsActivated uid -> isActivatedImpl uid + LookupLocale uid -> lookupLocaleImpl uid + GetUserTeam uid -> getUserTeamImpl uid + UpdateUserTeam uid tid -> updateUserTeamImpl uid tid + GetActivityTimestamps uid -> getActivityTimestampsImpl uid + GetRichInfo uid -> getRichInfoImpl uid + GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid + DeleteEmail uid -> deleteEmailImpl uid + SetUserSearchable uid searchable -> setUserSearchableImpl uid searchable + DeleteServiceUser pid sid bid -> deleteServiceUserImpl pid sid bid + LookupServiceUsers pid sid mPagingState -> lookupServiceUsersImpl pid sid (paginationStatePostgres =<< mPagingState) + LookupServiceUsersForTeam pid sid tid mPagingState -> lookupServiceUsersForTeamImpl pid sid tid (paginationStatePostgres =<< mPagingState) + +{- ORMOLU_DISABLE -} +type InsertUserRow = + ( UserId, Name, Maybe TextStatus, Pict, Maybe EmailAddress, + Maybe UserSSOId, ColourId, Maybe Password, Bool, AccountStatus, + Maybe UTCTimeMillis, Language, Maybe Country, Maybe ProviderId, Maybe ServiceId, + Maybe Handle, Maybe TeamId, ManagedBy, Set BaseProtocolTag, Bool + ) +{- ORMOLU_ENABLE -} + +createUserImpl :: (PGConstraints r) => NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> Sem r () +createUserImpl new mbConv = + runTransaction Serializable Write $ do + Transaction.statement userRow insertUser + Transaction.statement assetRows insertAssets + for_ mbConv $ \(convId, mTeamId) -> do + Transaction.statement (new.id, convId, mTeamId) insertBotConv + where + userRow = + ( new.id, + new.name, + new.textStatus, + new.pict, + new.email, + new.ssoId, + new.accentId, + new.password, + new.activated, + new.status, + new.expires, + new.language, + new.country, + new.providerId, + new.serviceId, + new.handle, + new.teamId, + new.managedBy, + new.supportedProtocols, + new.searchable + ) + + insertUser :: Hasql.Statement InsertUserRow () + insertUser = + lmapPG + [resultlessStatement| + INSERT INTO wire_user + (id, name, text_status, picture, email, + sso_id, accent_id, password, activated, account_status, + expires, language, country, provider, service, + handle, team, managed_by, supported_protocols, searchable) + VALUES + ($1 :: uuid, $2 :: text, $3 :: text?, $4 :: jsonb, $5 :: text?, + $6 :: jsonb?, $7 :: integer, $8 :: text?, $9 :: boolean, $10 :: integer, + $11 :: timestamptz?, $12 :: text, $13 :: text?, $14 :: uuid?, $15 :: uuid?, + $16 :: text?, $17 :: uuid?, $18 :: integer, $19 :: integer, $20 :: boolean) + |] + + assetRows :: ([UserId], [Int32], [AssetKey], [Maybe AssetSize]) + assetRows = + unzip4 $ + map (\asset -> (new.id, 0, asset.assetKey, asset.assetSize)) new.assets + + insertAssets :: Hasql.Statement ([UserId], [Int32], [AssetKey], [Maybe AssetSize]) () + insertAssets = + lmapPG @(Vector _, Vector _, Vector _, Vector _) + [resultlessStatement| + INSERT INTO asset + (user_id, typ, key, size) + SELECT UNNEST ($1 :: uuid[], $2 :: integer[], $3 :: text[], $4 :: integer?[]) + |] + + insertBotConv :: Hasql.Statement (UserId, ConvId, Maybe TeamId) () + insertBotConv = + lmapPG + [resultlessStatement| + INSERT INTO bot_conv + (id, conv, conv_team) + VALUES + ($1 :: uuid, $2 :: uuid, $3 :: uuid?) + |] + +getIndexUserImpl :: (PGConstraints r) => UserId -> Sem r (Maybe IndexUser) +getIndexUserImpl = todo "getIndexUserImpl: unimplemented" + +getIndexUsersPaginatedImpl :: (PGConstraints r) => Int32 -> Maybe Void -> Sem r (PageWithState Void IndexUser) +getIndexUsersPaginatedImpl = todo "getIndexUsersPaginatedImpl: unimplemented" + +getUsersImpl :: (PGConstraints r) => [UserId] -> Sem r [StoredUser] +getUsersImpl = todo "getUsersImpl: unimplemented" + +updateUserImpl :: (PGConstraints r) => UserId -> StoredUserUpdate -> Sem r () +updateUserImpl = todo "updateUserImpl: unimplemented" + +updateEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () +updateEmailUnvalidatedImpl = todo "updateEmailUnvalidatedImpl: unimplemented" + +updateUserHandleEitherImpl :: (PGConstraints r) => UserId -> StoredUserHandleUpdate -> Sem r (Either StoredUserUpdateError ()) +updateUserHandleEitherImpl = todo "updateUserHandleEitherImpl: unimplemented" + +deleteUserImpl :: (PGConstraints r) => User -> Sem r () +deleteUserImpl = todo "deleteUserImpl: unimplemented" + +lookupHandleImpl :: (PGConstraints r) => Handle -> Sem r (Maybe UserId) +lookupHandleImpl = todo "lookupHandleImpl: unimplemented" + +glimpseHandleImpl :: (PGConstraints r) => Handle -> Sem r (Maybe UserId) +glimpseHandleImpl = todo "glimpseHandleImpl: unimplemented" + +lookupStatusImpl :: (PGConstraints r) => UserId -> Sem r (Maybe AccountStatus) +lookupStatusImpl = todo "lookupStatusImpl: unimplemented" + +isActivatedImpl :: (PGConstraints r) => UserId -> Sem r Bool +isActivatedImpl = todo "isActivatedImpl: unimplemented" + +lookupLocaleImpl :: (PGConstraints r) => UserId -> Sem r (Maybe (Maybe Language, Maybe Country)) +lookupLocaleImpl = todo "lookupLocaleImpl: unimplemented" + +getUserTeamImpl :: (PGConstraints r) => UserId -> Sem r (Maybe TeamId) +getUserTeamImpl = todo "getUserTeamImpl: unimplemented" + +updateUserTeamImpl :: (PGConstraints r) => UserId -> TeamId -> Sem r () +updateUserTeamImpl = todo "updateUserTeamImpl: unimplemented" + +getActivityTimestampsImpl :: (PGConstraints r) => UserId -> Sem r [Maybe UTCTime] +getActivityTimestampsImpl = todo "getActivityTimestampsImpl: unimplemented" + +getRichInfoImpl :: (PGConstraints r) => UserId -> Sem r (Maybe RichInfoAssocList) +getRichInfoImpl = todo "getRichInfoImpl: unimplemented" + +getUserAuthenticationInfoImpl :: (PGConstraints r) => UserId -> Sem r (Maybe (Maybe Password, AccountStatus)) +getUserAuthenticationInfoImpl = todo "getUserAuthenticationInfoImpl: unimplemented" + +deleteEmailImpl :: (PGConstraints r) => UserId -> Sem r () +deleteEmailImpl = todo "deleteEmailImpl: unimplemented" + +setUserSearchableImpl :: (PGConstraints r) => UserId -> SetSearchable -> Sem r () +setUserSearchableImpl = todo "setUserSearchableImpl: unimplemented" + +deleteServiceUserImpl :: ProviderId -> ServiceId -> BotId -> Sem r () +deleteServiceUserImpl = todo "deleteServiceUserImpl: unimplemented" + +lookupServiceUsersImpl :: ProviderId -> ServiceId -> Maybe Void -> Sem r (PageWithState Void (BotId, ConvId, Maybe TeamId)) +lookupServiceUsersImpl = todo "lookupServiceUsersImpl: unimplemented" + +lookupServiceUsersForTeamImpl :: ProviderId -> ServiceId -> TeamId -> Maybe Void -> Sem r (PageWithState Void (BotId, ConvId)) +lookupServiceUsersForTeamImpl = todo "lookupServiceUsersForTeamImpl: unimplemented" + +updateEmailImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () +updateEmailImpl = todo "updateEmailImpl: unimplemented" + +deleteEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> Sem r () +deleteEmailUnvalidatedImpl = todo "deleteEmailUnvalidatedImpl: unimplemented" + +updateSSOIdImpl :: (PGConstraints r) => UserId -> Maybe UserSSOId -> Sem r Bool +updateSSOIdImpl = todo "updateSSOIdImpl: unimplemented" + +updateManagedByImpl :: (PGConstraints r) => UserId -> ManagedBy -> Sem r () +updateManagedByImpl = todo "updateManagedByImpl: unimplemented" + +updateAccountStatusImpl :: (PGConstraints r) => UserId -> AccountStatus -> Sem r () +updateAccountStatusImpl = todo "updateAccountStatusImpl: unimplemented" + +updateRichInfoImpl :: (PGConstraints r) => UserId -> RichInfoAssocList -> Sem r () +updateRichInfoImpl = todo "updateRichInfoImpl: unimplemented" + +updateFeatureConferenceCallingImpl :: (PGConstraints r) => UserId -> Maybe FeatureStatus -> Sem r () +updateFeatureConferenceCallingImpl = todo "updateFeatureConferenceCallingImpl: unimplemented" diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index a1991ca4b93..0cb92909ed2 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -346,6 +346,7 @@ library Wire.UserStore Wire.UserStore.Cassandra Wire.UserStore.IndexUser + Wire.UserStore.Postgres Wire.UserStore.Unique Wire.UserSubsystem Wire.UserSubsystem.Error From 88605e06f52539e4eb0a15d05863af782cf9f6ac Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 19 Jan 2026 14:15:23 +0100 Subject: [PATCH 10/16] UserStore.Postgres: Implement getUsers --- .../src/Wire/UserStore/Postgres.hs | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index 07970bb1820..078040b4962 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} {-# OPTIONS_GHC -Wwarn #-} module Wire.UserStore.Postgres where @@ -6,10 +8,12 @@ import Cassandra (PageWithState, paginationStatePostgres) import Data.Handle import Data.Id import Data.Json.Util +import Data.Map qualified as Map import Data.Time import Data.Vector (Vector) +import Hasql.Pipeline qualified as Pipeline import Hasql.Statement qualified as Hasql -import Hasql.TH (resultlessStatement) +import Hasql.TH (resultlessStatement, vectorStatement) import Hasql.Transaction qualified as Transaction import Hasql.Transaction.Sessions import Imports @@ -67,6 +71,20 @@ type InsertUserRow = Maybe UTCTimeMillis, Language, Maybe Country, Maybe ProviderId, Maybe ServiceId, Maybe Handle, Maybe TeamId, ManagedBy, Set BaseProtocolTag, Bool ) +type + SelectUserRow = + ( UserId, Name, Maybe TextStatus, Maybe Pict, Maybe EmailAddress, Maybe EmailAddress, + Maybe UserSSOId, ColourId, Bool, Maybe AccountStatus, + Maybe UTCTimeMillis, Maybe Language, Maybe Country, Maybe ProviderId, Maybe ServiceId, + Maybe Handle, Maybe TeamId, Maybe ManagedBy, Maybe (Set BaseProtocolTag), Maybe Bool + ) + +storedUserFromRow :: SelectUserRow -> StoredUser +storedUserFromRow (id_, name, textStatus, pict, email, emailUnvalidated, + ssoId, accentId, activated, status, + expires, language, country, providerId, serviceId, + handle, teamId, managedBy, supportedProtocols, searchable) + = let assets = Nothing in StoredUser{id = id_, ..} {- ORMOLU_ENABLE -} createUserImpl :: (PGConstraints r) => NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> Sem r () @@ -140,15 +158,54 @@ createUserImpl new mbConv = ($1 :: uuid, $2 :: uuid, $3 :: uuid?) |] +getUsersImpl :: (PGConstraints r) => [UserId] -> Sem r [StoredUser] +getUsersImpl uids = do + (userRows, assetRows) <- + runPipeline $ + (,) + <$> Pipeline.statement uids selectUsers + <*> Pipeline.statement uids selectAssets + let assetMap = + foldr + (\(uid, _, key, size) -> Map.insertWith (<>) uid [ImageAsset key size]) + Map.empty + assetRows + pure $ + map + ( \row -> + let storedUser = storedUserFromRow row + in storedUser {assets = Map.lookup storedUser.id assetMap} :: StoredUser + ) + userRows + where + selectUsers :: Hasql.Statement [UserId] [SelectUserRow] + selectUsers = + dimapPG @(Vector _) + [vectorStatement| + SELECT + id :: uuid, name :: text, text_status :: text?, picture :: jsonb?, email :: text?, email_unvalidated :: text?, + sso_id :: jsonb?, accent_id :: integer, activated :: boolean, account_status :: integer?, + expires :: timestamptz?, language :: text?, country :: text?, provider :: uuid?, service :: uuid?, + handle :: text?, team :: uuid?, managed_by :: integer?, supported_protocols :: integer?, searchable :: boolean? + FROM wire_user + WHERE id = ANY($1 :: uuid[]) + |] + + selectAssets :: Hasql.Statement [UserId] [(UserId, Int32, AssetKey, Maybe AssetSize)] + selectAssets = + dimapPG @(Vector _) + [vectorStatement| + SELECT user_id :: uuid, typ :: integer, key :: text, size :: integer? + FROM asset + WHERE id = ANY($1 :: uuid[]) + |] + getIndexUserImpl :: (PGConstraints r) => UserId -> Sem r (Maybe IndexUser) getIndexUserImpl = todo "getIndexUserImpl: unimplemented" getIndexUsersPaginatedImpl :: (PGConstraints r) => Int32 -> Maybe Void -> Sem r (PageWithState Void IndexUser) getIndexUsersPaginatedImpl = todo "getIndexUsersPaginatedImpl: unimplemented" -getUsersImpl :: (PGConstraints r) => [UserId] -> Sem r [StoredUser] -getUsersImpl = todo "getUsersImpl: unimplemented" - updateUserImpl :: (PGConstraints r) => UserId -> StoredUserUpdate -> Sem r () updateUserImpl = todo "updateUserImpl: unimplemented" From b755f60b70364ba32e0301470cbc909f5e176c26 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 19 Jan 2026 16:10:22 +0100 Subject: [PATCH 11/16] UserStore.Postgres: Implement getIndexUser --- .../20260113140936-create-user-tables.sql | 13 ++++++++- .../src/Wire/UserStore/Postgres.hs | 28 +++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql b/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql index b5d11bba292..4e510d0a83c 100644 --- a/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql +++ b/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql @@ -21,11 +21,22 @@ CREATE TABLE wire_user ( supported_protocols integer, team uuid, text_status text, - write_time_bumper integer + created_at timestamptz NOT NULL DEFAULT current_timestamp, + updated_at timestamptz NOT NULL DEFAULT current_timestamp ); CREATE INDEX wire_user_service_idx ON wire_user(provider, service); +CREATE OR REPLACE FUNCTION update_updated_at() + RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_user_updated_at BEFORE UPDATE ON wire_user FOR EACH ROW EXECUTE PROCEDURE update_updated_at(); + CREATE TABLE asset ( user_id uuid NOT NULL, typ integer NOT NULL, diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index 078040b4962..be3a7a31b82 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -13,7 +13,7 @@ import Data.Time import Data.Vector (Vector) import Hasql.Pipeline qualified as Pipeline import Hasql.Statement qualified as Hasql -import Hasql.TH (resultlessStatement, vectorStatement) +import Hasql.TH import Hasql.Transaction qualified as Transaction import Hasql.Transaction.Sessions import Imports @@ -85,6 +85,17 @@ storedUserFromRow (id_, name, textStatus, pict, email, emailUnvalidated, expires, language, country, providerId, serviceId, handle, teamId, managedBy, supportedProtocols, searchable) = let assets = Nothing in StoredUser{id = id_, ..} + +type SelectIndexUserRow = + (UserId, Maybe TeamId, Name, Maybe AccountStatus, Maybe Handle, + Maybe EmailAddress, Maybe EmailAddress, ColourId, Bool, Maybe ServiceId, + Maybe ManagedBy, Maybe UserSSOId, Maybe Bool, UTCTime, UTCTime) + +indexUserFromRow :: SelectIndexUserRow -> IndexUser +indexUserFromRow ( uid, teamId, name, accountStatus, handle, + email, unverifiedEmail, colourId, activated, serviceId, + managedBy, ssoId, searchable, createdAt, updatedAt + ) = IndexUser{userId = uid, ..} {- ORMOLU_ENABLE -} createUserImpl :: (PGConstraints r) => NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> Sem r () @@ -201,7 +212,20 @@ getUsersImpl uids = do |] getIndexUserImpl :: (PGConstraints r) => UserId -> Sem r (Maybe IndexUser) -getIndexUserImpl = todo "getIndexUserImpl: unimplemented" +getIndexUserImpl uid = do + indexUserFromRow <$$> runStatement uid selectUser + where + selectUser :: Hasql.Statement UserId (Maybe SelectIndexUserRow) + selectUser = + dimapPG + [maybeStatement| + SELECT + id :: uuid, team :: uuid?, name :: text, account_status :: integer?, handle :: text?, + email :: text?, email_unvalidated :: text?, accent_id :: integer, activated :: Bool, serviceId :: uuid?, + managed_by :: integer?, sso_id :: jsonb?, searchable :: boolean?, created_at :: timestamptz, updated_at :: timestamptz + FROM user + WHERE id = $1 :: uuid + |] getIndexUsersPaginatedImpl :: (PGConstraints r) => Int32 -> Maybe Void -> Sem r (PageWithState Void IndexUser) getIndexUsersPaginatedImpl = todo "getIndexUsersPaginatedImpl: unimplemented" From c11d1baad487ac2c2b144431b3149cafdf4d98dc Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 20 Jan 2026 15:18:44 +0100 Subject: [PATCH 12/16] UserStore.Postgres: Implement getIndexUsersPaginated --- libs/wire-subsystems/src/Wire/UserStore.hs | 2 +- .../src/Wire/UserStore/Postgres.hs | 43 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index df314e5fb00..12f6804f0d4 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -72,7 +72,7 @@ data UserStore m a where CreateUser :: NewStoredUser -> Maybe (ConvId, Maybe TeamId) -> UserStore m () GetIndexUser :: UserId -> UserStore m (Maybe IndexUser) DoesUserExist :: UserId -> UserStore m Bool - GetIndexUsersPaginated :: Int32 -> Maybe (GeneralPaginationState Void) -> UserStore m (PageWithState Void IndexUser) + GetIndexUsersPaginated :: Int32 -> Maybe (GeneralPaginationState UserId) -> UserStore m (PageWithState UserId IndexUser) GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () UpdateEmail :: UserId -> EmailAddress -> UserStore m () diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index be3a7a31b82..f5af31bc34d 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -4,7 +4,8 @@ module Wire.UserStore.Postgres where -import Cassandra (PageWithState, paginationStatePostgres) +import Cassandra (GeneralPaginationState (PaginationStatePostgres), PageWithState (..), paginationStatePostgres) +import Control.Error (lastMay) import Data.Handle import Data.Id import Data.Json.Util @@ -227,8 +228,44 @@ getIndexUserImpl uid = do WHERE id = $1 :: uuid |] -getIndexUsersPaginatedImpl :: (PGConstraints r) => Int32 -> Maybe Void -> Sem r (PageWithState Void IndexUser) -getIndexUsersPaginatedImpl = todo "getIndexUsersPaginatedImpl: unimplemented" +getIndexUsersPaginatedImpl :: (PGConstraints r) => Int32 -> Maybe UserId -> Sem r (PageWithState UserId IndexUser) +getIndexUsersPaginatedImpl lim mState = do + rows <- case mState of + Nothing -> runStatement lim selectStart + Just startId -> runStatement (startId, lim) selectFrom + let results = indexUserFromRow <$> rows + pure + PageWithState + { pwsResults = results, + pwsState = PaginationStatePostgres . (.userId) <$> lastMay results + } + where + selectStart :: Hasql.Statement Int32 [SelectIndexUserRow] + selectStart = + dimapPG + [vectorStatement| + SELECT + id :: uuid, team :: uuid?, name :: text, account_status :: integer?, handle :: text?, + email :: text?, email_unvalidated :: text?, accent_id :: integer, activated :: Bool, serviceId :: uuid?, + managed_by :: integer?, sso_id :: jsonb?, searchable :: boolean?, created_at :: timestamptz, updated_at :: timestamptz + FROM user + ORDER BY id ASC + LIMIT ($1 :: integer) + |] + + selectFrom :: Hasql.Statement (UserId, Int32) [SelectIndexUserRow] + selectFrom = + dimapPG + [vectorStatement| + SELECT + id :: uuid, team :: uuid?, name :: text, account_status :: integer?, handle :: text?, + email :: text?, email_unvalidated :: text?, accent_id :: integer, activated :: Bool, serviceId :: uuid?, + managed_by :: integer?, sso_id :: jsonb?, searchable :: boolean?, created_at :: timestamptz, updated_at :: timestamptz + FROM user + WHERE id > ($1 :: uuid) + ORDER BY id ASC + LIMIT ($2 :: integer) + |] updateUserImpl :: (PGConstraints r) => UserId -> StoredUserUpdate -> Sem r () updateUserImpl = todo "updateUserImpl: unimplemented" From 2954ea844b13a82e471fb4569ed64f0dab613bc8 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 20 Jan 2026 15:52:16 +0100 Subject: [PATCH 13/16] UserStore.Postgres: Implement updateUser --- .../src/Wire/UserStore/Postgres.hs | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index f5af31bc34d..cb48850634c 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -19,7 +19,7 @@ import Hasql.Transaction qualified as Transaction import Hasql.Transaction.Sessions import Imports import Polysemy -import Wire.API.Asset +import Wire.API.Asset hiding (Asset) import Wire.API.Password import Wire.API.PostgresMarshall import Wire.API.Team.Feature (FeatureStatus) @@ -103,7 +103,7 @@ createUserImpl :: (PGConstraints r) => NewStoredUser -> Maybe (ConvId, Maybe Tea createUserImpl new mbConv = runTransaction Serializable Write $ do Transaction.statement userRow insertUser - Transaction.statement assetRows insertAssets + Transaction.statement (mkAssetRows new.id new.assets) insertAssetsStatement for_ mbConv $ \(convId, mTeamId) -> do Transaction.statement (new.id, convId, mTeamId) insertBotConv where @@ -146,20 +146,6 @@ createUserImpl new mbConv = $16 :: text?, $17 :: uuid?, $18 :: integer, $19 :: integer, $20 :: boolean) |] - assetRows :: ([UserId], [Int32], [AssetKey], [Maybe AssetSize]) - assetRows = - unzip4 $ - map (\asset -> (new.id, 0, asset.assetKey, asset.assetSize)) new.assets - - insertAssets :: Hasql.Statement ([UserId], [Int32], [AssetKey], [Maybe AssetSize]) () - insertAssets = - lmapPG @(Vector _, Vector _, Vector _, Vector _) - [resultlessStatement| - INSERT INTO asset - (user_id, typ, key, size) - SELECT UNNEST ($1 :: uuid[], $2 :: integer[], $3 :: text[], $4 :: integer?[]) - |] - insertBotConv :: Hasql.Statement (UserId, ConvId, Maybe TeamId) () insertBotConv = lmapPG @@ -170,6 +156,20 @@ createUserImpl new mbConv = ($1 :: uuid, $2 :: uuid, $3 :: uuid?) |] +mkAssetRows :: UserId -> [Asset] -> ([UserId], [Int32], [AssetKey], [Maybe AssetSize]) +mkAssetRows uid assets = + unzip4 $ + map (\asset -> (uid, 0, asset.assetKey, asset.assetSize)) assets + +insertAssetsStatement :: Hasql.Statement ([UserId], [Int32], [AssetKey], [Maybe AssetSize]) () +insertAssetsStatement = + lmapPG @(Vector _, Vector _, Vector _, Vector _) + [resultlessStatement| + INSERT INTO asset + (user_id, typ, key, size) + SELECT UNNEST ($1 :: uuid[], $2 :: integer[], $3 :: text[], $4 :: integer?[]) + |] + getUsersImpl :: (PGConstraints r) => [UserId] -> Sem r [StoredUser] getUsersImpl uids = do (userRows, assetRows) <- @@ -268,7 +268,34 @@ getIndexUsersPaginatedImpl lim mState = do |] updateUserImpl :: (PGConstraints r) => UserId -> StoredUserUpdate -> Sem r () -updateUserImpl = todo "updateUserImpl: unimplemented" +updateUserImpl uid MkStoredUserUpdate {..} = + runTransaction ReadCommitted Write $ do + Transaction.statement + (uid, name, textStatus, pict, accentId, lLanguage <$> locale, lCountry =<< locale, supportedProtocols) + updateUserFields + for_ assets $ \newAssets -> do + Transaction.statement uid deleteAssets + Transaction.statement (mkAssetRows uid newAssets) insertAssetsStatement + where + updateUserFields :: Hasql.Statement (UserId, Maybe Name, Maybe TextStatus, Maybe Pict, Maybe ColourId, Maybe Language, Maybe Country, Maybe (Set BaseProtocolTag)) () + updateUserFields = + lmapPG + [resultlessStatement| + UPDATE wire_user + SET name = COALESCE($2 :: text?, name), + text_status = COALESCE($3 :: text?, text_status), + picture = COALESCE($4 :: jsonb?, picture), + accent_id = COALESCE($5 :: integer?, accent_id), + language = COALESCE($6 :: text?, language), + country = COALESCE($7 :: text?, country), + supported_protocols = COALESCE($8 :: integer?, supported_protocols) + WHERE id = ($1 :: uuid) + |] + + deleteAssets :: Hasql.Statement UserId () + deleteAssets = + lmapPG + [resultlessStatement|DELETE FROM asset where user_id = $1 :: uuid|] updateEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () updateEmailUnvalidatedImpl = todo "updateEmailUnvalidatedImpl: unimplemented" From fcb2866da41deb93c09bc500887a77790a539b02 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 20 Jan 2026 16:05:45 +0100 Subject: [PATCH 14/16] UserStore.Postgres: Implement {update,delete}Email{,unvalidated} --- .../src/Wire/UserStore/Postgres.hs | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index cb48850634c..7f71e2bee29 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -40,6 +40,7 @@ interpretUserStoreCassandra = GetIndexUsersPaginated pageSize mPagingState -> getIndexUsersPaginatedImpl pageSize (paginationStatePostgres =<< mPagingState) UpdateUser uid update -> updateUserImpl uid update UpdateEmail uid email -> updateEmailImpl uid email + DeleteEmail uid -> deleteEmailImpl uid UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid email DeleteEmailUnvalidated uid -> deleteEmailUnvalidatedImpl uid UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update @@ -59,7 +60,6 @@ interpretUserStoreCassandra = GetActivityTimestamps uid -> getActivityTimestampsImpl uid GetRichInfo uid -> getRichInfoImpl uid GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid - DeleteEmail uid -> deleteEmailImpl uid SetUserSearchable uid searchable -> setUserSearchableImpl uid searchable DeleteServiceUser pid sid bid -> deleteServiceUserImpl pid sid bid LookupServiceUsers pid sid mPagingState -> lookupServiceUsersImpl pid sid (paginationStatePostgres =<< mPagingState) @@ -298,7 +298,40 @@ updateUserImpl uid MkStoredUserUpdate {..} = [resultlessStatement|DELETE FROM asset where user_id = $1 :: uuid|] updateEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () -updateEmailUnvalidatedImpl = todo "updateEmailUnvalidatedImpl: unimplemented" +updateEmailUnvalidatedImpl uid email = + runStatement (uid, email) update + where + update :: Hasql.Statement (UserId, EmailAddress) () + update = + lmapPG + [resultlessStatement|UPDATE wire_user SET email_unvalidated = ($2 :: text) WHERE id = ($1 :: uuid)|] + +deleteEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> Sem r () +deleteEmailUnvalidatedImpl uid = + runStatement uid del + where + del :: Hasql.Statement UserId () + del = + lmapPG + [resultlessStatement|UPDATE wire_user SET email_unvalidated = NULL WHERE id = ($1 :: uuid)|] + +updateEmailImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () +updateEmailImpl uid email = + runStatement (uid, email) update + where + update :: Hasql.Statement (UserId, EmailAddress) () + update = + lmapPG + [resultlessStatement|UPDATE wire_user SET email = ($2 :: text) WHERE id = ($1 :: uuid)|] + +deleteEmailImpl :: (PGConstraints r) => UserId -> Sem r () +deleteEmailImpl uid = + runStatement uid del + where + del :: Hasql.Statement UserId () + del = + lmapPG + [resultlessStatement|UPDATE wire_user SET email = NULL WHERE id = ($1 :: uuid)|] updateUserHandleEitherImpl :: (PGConstraints r) => UserId -> StoredUserHandleUpdate -> Sem r (Either StoredUserUpdateError ()) updateUserHandleEitherImpl = todo "updateUserHandleEitherImpl: unimplemented" @@ -336,9 +369,6 @@ getRichInfoImpl = todo "getRichInfoImpl: unimplemented" getUserAuthenticationInfoImpl :: (PGConstraints r) => UserId -> Sem r (Maybe (Maybe Password, AccountStatus)) getUserAuthenticationInfoImpl = todo "getUserAuthenticationInfoImpl: unimplemented" -deleteEmailImpl :: (PGConstraints r) => UserId -> Sem r () -deleteEmailImpl = todo "deleteEmailImpl: unimplemented" - setUserSearchableImpl :: (PGConstraints r) => UserId -> SetSearchable -> Sem r () setUserSearchableImpl = todo "setUserSearchableImpl: unimplemented" @@ -351,12 +381,6 @@ lookupServiceUsersImpl = todo "lookupServiceUsersImpl: unimplemented" lookupServiceUsersForTeamImpl :: ProviderId -> ServiceId -> TeamId -> Maybe Void -> Sem r (PageWithState Void (BotId, ConvId)) lookupServiceUsersForTeamImpl = todo "lookupServiceUsersForTeamImpl: unimplemented" -updateEmailImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () -updateEmailImpl = todo "updateEmailImpl: unimplemented" - -deleteEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> Sem r () -deleteEmailUnvalidatedImpl = todo "deleteEmailUnvalidatedImpl: unimplemented" - updateSSOIdImpl :: (PGConstraints r) => UserId -> Maybe UserSSOId -> Sem r Bool updateSSOIdImpl = todo "updateSSOIdImpl: unimplemented" From 1a833e1e46dcb67df1969da55b6dee63ee4ad910 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 20 Jan 2026 16:08:22 +0100 Subject: [PATCH 15/16] UserStore.Postgres: Simplify {update,delete}Email{,unvalidated} --- .../src/Wire/UserStore/Postgres.hs | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index 7f71e2bee29..3e20ae47ace 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -39,10 +39,10 @@ interpretUserStoreCassandra = GetIndexUser uid -> getIndexUserImpl uid GetIndexUsersPaginated pageSize mPagingState -> getIndexUsersPaginatedImpl pageSize (paginationStatePostgres =<< mPagingState) UpdateUser uid update -> updateUserImpl uid update - UpdateEmail uid email -> updateEmailImpl uid email - DeleteEmail uid -> deleteEmailImpl uid - UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid email - DeleteEmailUnvalidated uid -> deleteEmailUnvalidatedImpl uid + UpdateEmail uid email -> updateEmailImpl uid (Just email) + DeleteEmail uid -> updateEmailImpl uid Nothing + UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid (Just email) + DeleteEmailUnvalidated uid -> updateEmailUnvalidatedImpl uid Nothing UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update UpdateSSOId uid ssoId -> updateSSOIdImpl uid ssoId UpdateManagedBy uid managedBy -> updateManagedByImpl uid managedBy @@ -297,41 +297,23 @@ updateUserImpl uid MkStoredUserUpdate {..} = lmapPG [resultlessStatement|DELETE FROM asset where user_id = $1 :: uuid|] -updateEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () +updateEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> Maybe EmailAddress -> Sem r () updateEmailUnvalidatedImpl uid email = runStatement (uid, email) update where - update :: Hasql.Statement (UserId, EmailAddress) () + update :: Hasql.Statement (UserId, Maybe EmailAddress) () update = lmapPG - [resultlessStatement|UPDATE wire_user SET email_unvalidated = ($2 :: text) WHERE id = ($1 :: uuid)|] + [resultlessStatement|UPDATE wire_user SET email_unvalidated = ($2 :: text?) WHERE id = ($1 :: uuid)|] -deleteEmailUnvalidatedImpl :: (PGConstraints r) => UserId -> Sem r () -deleteEmailUnvalidatedImpl uid = - runStatement uid del - where - del :: Hasql.Statement UserId () - del = - lmapPG - [resultlessStatement|UPDATE wire_user SET email_unvalidated = NULL WHERE id = ($1 :: uuid)|] - -updateEmailImpl :: (PGConstraints r) => UserId -> EmailAddress -> Sem r () +updateEmailImpl :: (PGConstraints r) => UserId -> Maybe EmailAddress -> Sem r () updateEmailImpl uid email = runStatement (uid, email) update where - update :: Hasql.Statement (UserId, EmailAddress) () + update :: Hasql.Statement (UserId, Maybe EmailAddress) () update = lmapPG - [resultlessStatement|UPDATE wire_user SET email = ($2 :: text) WHERE id = ($1 :: uuid)|] - -deleteEmailImpl :: (PGConstraints r) => UserId -> Sem r () -deleteEmailImpl uid = - runStatement uid del - where - del :: Hasql.Statement UserId () - del = - lmapPG - [resultlessStatement|UPDATE wire_user SET email = NULL WHERE id = ($1 :: uuid)|] + [resultlessStatement|UPDATE wire_user SET email = ($2 :: text?) WHERE id = ($1 :: uuid)|] updateUserHandleEitherImpl :: (PGConstraints r) => UserId -> StoredUserHandleUpdate -> Sem r (Either StoredUserUpdateError ()) updateUserHandleEitherImpl = todo "updateUserHandleEitherImpl: unimplemented" From edefb6e914419b9b988422b269cde89a4eac5866 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 20 Jan 2026 16:28:42 +0100 Subject: [PATCH 16/16] UserStore.Postgres: Implement {lookup,glimpse}Handle and updateUserHandleEither --- .../20260113140936-create-user-tables.sql | 2 +- .../src/Wire/UserStore/Postgres.hs | 39 ++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql b/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql index 4e510d0a83c..fce6678985a 100644 --- a/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql +++ b/libs/wire-subsystems/postgres-migrations/20260113140936-create-user-tables.sql @@ -7,7 +7,7 @@ CREATE TABLE wire_user ( email_unvalidated text, expires timestamptz, feature_conference_calling integer, - handle text, + handle text UNIQUE, language text, managed_by integer, name text NOT NULL, diff --git a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs index 3e20ae47ace..c379265b23a 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Postgres.hs @@ -43,6 +43,8 @@ interpretUserStoreCassandra = DeleteEmail uid -> updateEmailImpl uid Nothing UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid (Just email) DeleteEmailUnvalidated uid -> updateEmailUnvalidatedImpl uid Nothing + LookupHandle hdl -> lookupHandleImpl hdl + GlimpseHandle hdl -> lookupHandleImpl hdl UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update UpdateSSOId uid ssoId -> updateSSOIdImpl uid ssoId UpdateManagedBy uid managedBy -> updateManagedByImpl uid managedBy @@ -50,8 +52,6 @@ interpretUserStoreCassandra = UpdateRichInfo uid richInfo -> updateRichInfoImpl uid richInfo UpdateFeatureConferenceCalling uid feat -> updateFeatureConferenceCallingImpl uid feat DeleteUser user -> deleteUserImpl user - LookupHandle hdl -> lookupHandleImpl hdl - GlimpseHandle hdl -> lookupHandleImpl hdl LookupStatus uid -> lookupStatusImpl uid IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid @@ -315,18 +315,39 @@ updateEmailImpl uid email = lmapPG [resultlessStatement|UPDATE wire_user SET email = ($2 :: text?) WHERE id = ($1 :: uuid)|] +lookupHandleImpl :: (PGConstraints r) => Handle -> Sem r (Maybe UserId) +lookupHandleImpl h = runStatement h selectUserIdByHandleStatement + +selectUserIdByHandleStatement :: Hasql.Statement Handle (Maybe UserId) +selectUserIdByHandleStatement = + dimapPG + [maybeStatement| + SELECT id :: uuid + FROM wire_user + WHERE handle = $1 :: text + |] + updateUserHandleEitherImpl :: (PGConstraints r) => UserId -> StoredUserHandleUpdate -> Sem r (Either StoredUserUpdateError ()) -updateUserHandleEitherImpl = todo "updateUserHandleEitherImpl: unimplemented" +updateUserHandleEitherImpl uid upd = + runTransaction ReadCommitted Write $ do + mOwner <- Transaction.statement upd.new selectUserIdByHandleStatement + case mOwner of + Just uid' | uid' /= uid -> pure $ Left StoredUserUpdateHandleExists + Just _ -> pure $ Right () + Nothing -> Right <$> Transaction.statement (uid, upd.new) update + where + update :: Hasql.Statement (UserId, Handle) () + update = + lmapPG + [resultlessStatement| + UPDATE wire_user + SET handle = $2 :: text + WHERE id = $1 :: uuid + |] deleteUserImpl :: (PGConstraints r) => User -> Sem r () deleteUserImpl = todo "deleteUserImpl: unimplemented" -lookupHandleImpl :: (PGConstraints r) => Handle -> Sem r (Maybe UserId) -lookupHandleImpl = todo "lookupHandleImpl: unimplemented" - -glimpseHandleImpl :: (PGConstraints r) => Handle -> Sem r (Maybe UserId) -glimpseHandleImpl = todo "glimpseHandleImpl: unimplemented" - lookupStatusImpl :: (PGConstraints r) => UserId -> Sem r (Maybe AccountStatus) lookupStatusImpl = todo "lookupStatusImpl: unimplemented"