diff --git a/src/Share/Notifications/Queries.hs b/src/Share/Notifications/Queries.hs index 41b46de1..7e090d45 100644 --- a/src/Share/Notifications/Queries.hs +++ b/src/Share/Notifications/Queries.hs @@ -106,7 +106,7 @@ listNotificationHubEntryPayloads notificationUserId mayLimit mayCursor statusFil LIMIT #{limit} |] -hasUnreadNotifications :: UserId -> Transaction e Bool +hasUnreadNotifications :: (PG.QueryA m) => UserId -> m Bool hasUnreadNotifications notificationUserId = do queryExpect1Col [sql| diff --git a/src/Share/Postgres/Authorization/Queries.hs b/src/Share/Postgres/Authorization/Queries.hs index d63cdfcb..a0ce817d 100644 --- a/src/Share/Postgres/Authorization/Queries.hs +++ b/src/Share/Postgres/Authorization/Queries.hs @@ -19,12 +19,12 @@ module Share.Postgres.Authorization.Queries ) where -import Share.Prelude import Control.Lens import Data.Set qualified as Set import Share.IDs import Share.Postgres qualified as PG import Share.Postgres.IDs (CausalId) +import Share.Prelude import Share.Web.Authorization.Types -- | A user has access if they own the repo, or if they're a member of an org which owns it. @@ -42,7 +42,7 @@ checkIsUserMaintainer requestingUserId codebaseOwnerUserId ) |] -isSuperadmin :: UserId -> PG.Transaction e Bool +isSuperadmin :: (PG.QueryA m) => UserId -> m Bool isSuperadmin uid = do PG.queryExpect1Col [PG.sql| @@ -155,7 +155,7 @@ permissionsForProject mayUserId projectId = do |] <&> Set.fromList -permissionsForOrg :: Maybe UserId -> OrgId -> PG.Transaction e (Set RolePermission) +permissionsForOrg :: (PG.QueryA m) => Maybe UserId -> OrgId -> m (Set RolePermission) permissionsForOrg mayUserId orgId = do PG.queryListCol @RolePermission [PG.sql| diff --git a/src/Share/Postgres/Queries.hs b/src/Share/Postgres/Queries.hs index b4d7be11..7a7127ba 100644 --- a/src/Share/Postgres/Queries.hs +++ b/src/Share/Postgres/Queries.hs @@ -195,7 +195,7 @@ searchProjects caller userIdFilter (Query query) psk limit = do pure (results <&> \(project PG.:. PG.Only handle) -> (project, handle)) -- | Returns the list of tours the user has completed. -getCompletedToursForUser :: UserId -> PG.Transaction e [TourId] +getCompletedToursForUser :: (PG.QueryA m) => UserId -> m [TourId] getCompletedToursForUser uid = do PG.queryListCol [PG.sql| @@ -973,13 +973,13 @@ isUnisonEmployee uid = do |] -- | Returns the handles of all orgs the provided user is a member of. -organizationMemberships :: UserId -> PG.Transaction e [UserHandle] +organizationMemberships :: (PG.QueryA m) => UserId -> m [OrgId] organizationMemberships uid = do PG.queryListCol [PG.sql| - SELECT org_user.handle FROM users AS org_user - JOIN org_members ON organization_user_id = org_user.id - WHERE member_user_id = #{uid} + SELECT om.org_id + FROM org_members om + WHERE om.member_user_id = #{uid} |] releaseByProjectReleaseShortHand :: ProjectReleaseShortHand -> PG.Transaction e (Maybe (Release CausalId UserId)) diff --git a/src/Share/Postgres/Users/Queries.hs b/src/Share/Postgres/Users/Queries.hs index 2f68b45a..7e25a68d 100644 --- a/src/Share/Postgres/Users/Queries.hs +++ b/src/Share/Postgres/Users/Queries.hs @@ -297,7 +297,7 @@ searchUsersByNameOrHandlePrefix (Query prefix) usk (Limit limit) = do Just orgId -> UnifiedOrg orgId Nothing -> UnifiedUser userId -joinOrgIdsToUserIdsOf :: Traversal s t UserId (UserId, Maybe OrgId) -> s -> PG.Transaction e t +joinOrgIdsToUserIdsOf :: (PG.QueryA m) => Traversal s t UserId (UserId, Maybe OrgId) -> s -> m t joinOrgIdsToUserIdsOf trav s = do s & asListOf trav %%~ \userIds -> do @@ -342,7 +342,7 @@ allUsers = do SELECT id FROM users |] -userSubscriptionTier :: UserId -> PG.Transaction e PlanTier +userSubscriptionTier :: (PG.QueryA m) => UserId -> m PlanTier userSubscriptionTier userId = fromMaybe Free <$> do PG.query1Col diff --git a/src/Share/Web/Share/DisplayInfo/Queries.hs b/src/Share/Web/Share/DisplayInfo/Queries.hs index ed0ecf09..1e90c335 100644 --- a/src/Share/Web/Share/DisplayInfo/Queries.hs +++ b/src/Share/Web/Share/DisplayInfo/Queries.hs @@ -1,11 +1,11 @@ module Share.Web.Share.DisplayInfo.Queries (userLikeDisplayInfoOf, unifiedDisplayInfoForUserOf) where -import Share.Prelude import Control.Lens import Share.IDs import Share.Postgres (Transaction) import Share.Postgres.Users.Queries qualified as UserQ import Share.Postgres.Users.Queries qualified as UsersQ +import Share.Prelude import Share.Web.Share.DisplayInfo.Types import Share.Web.Share.Orgs.Queries qualified as OrgsQ diff --git a/src/Share/Web/Share/Impl.hs b/src/Share/Web/Share/Impl.hs index 2ad23dba..11392805 100644 --- a/src/Share/Web/Share/Impl.hs +++ b/src/Share/Web/Share/Impl.hs @@ -24,6 +24,7 @@ import Share.Notifications.Queries qualified as NotifQ import Share.OAuth.Session import Share.OAuth.Types (UserId) import Share.Postgres qualified as PG +import Share.Postgres.Authorization.Queries qualified as AuthQ import Share.Postgres.Authorization.Queries qualified as AuthZQ import Share.Postgres.Causal.Queries qualified as CausalQ import Share.Postgres.IDs (CausalHash) @@ -57,6 +58,7 @@ import Share.Web.Share.Contributions.Impl qualified as Contributions import Share.Web.Share.DefinitionSearch qualified as DefinitionSearch import Share.Web.Share.DisplayInfo.Queries qualified as DisplayInfoQ import Share.Web.Share.DisplayInfo.Types (OrgDisplayInfo (..), UserLike (..)) +import Share.Web.Share.Orgs.Queries qualified as OrgsQ import Share.Web.Share.Projects.Impl qualified as Projects import Share.Web.Share.Types import Share.Web.Share.Users.API qualified as Users @@ -571,22 +573,26 @@ accountInfoEndpoint :: Session -> WebApp UserAccountInfo accountInfoEndpoint Session {sessionUserId} = do User {user_email, user_id} <- PGO.expectUserById sessionUserId PG.runTransaction $ do - completedTours <- Q.getCompletedToursForUser user_id - organizationMemberships <- Q.organizationMemberships user_id - isSuperadmin <- AuthZQ.isSuperadmin user_id displayInfo <- DisplayInfoQ.unifiedDisplayInfoForUserOf id user_id - planTier <- UserQ.userSubscriptionTier user_id - hasUnreadNotifications <- NotifQ.hasUnreadNotifications user_id - pure $ - UserAccountInfo - { primaryEmail = user_email, - completedTours, - organizationMemberships, - isSuperadmin, - displayInfo, - planTier, - hasUnreadNotifications - } + memberOfOrgs <- Q.organizationMemberships user_id + PG.pipelined $ do + orgDisplayInfos <- OrgsQ.orgDisplayInfoOf traversed memberOfOrgs + orgPermissions <- for memberOfOrgs \orgId -> do + AuthQ.permissionsForOrg (Just user_id) orgId + completedTours <- Q.getCompletedToursForUser user_id + isSuperadmin <- AuthZQ.isSuperadmin user_id + planTier <- UserQ.userSubscriptionTier user_id + hasUnreadNotifications <- NotifQ.hasUnreadNotifications user_id + pure $ + UserAccountInfo + { primaryEmail = user_email, + completedTours, + organizationMemberships = zipWith OrgMembershipInfo orgDisplayInfos orgPermissions, + isSuperadmin, + displayInfo, + planTier, + hasUnreadNotifications + } completeToursEndpoint :: Session -> NonEmpty TourId -> WebApp NoContent completeToursEndpoint Session {sessionUserId} flows = do diff --git a/src/Share/Web/Share/Types.hs b/src/Share/Web/Share/Types.hs index a83b3e76..4a4824b9 100644 --- a/src/Share/Web/Share/Types.hs +++ b/src/Share/Web/Share/Types.hs @@ -277,11 +277,30 @@ instance PG.DecodeValue PlanTier where "Pro" -> Pro _ -> Free +data OrgMembershipInfo = OrgMembershipInfo + { org :: OrgDisplayInfo, + permissions :: Set RolePermission + } + deriving (Show, Eq, Ord) + +instance ToJSON OrgMembershipInfo where + toJSON OrgMembershipInfo {..} = + Aeson.object + [ "org" .= org, + "permissions" .= permissions + ] + +instance FromJSON OrgMembershipInfo where + parseJSON = Aeson.withObject "OrgMembershipInfo" $ \o -> do + org <- o Aeson..: "org" + permissions <- o Aeson..: "permissions" + pure OrgMembershipInfo {org, permissions} + data UserAccountInfo = UserAccountInfo { primaryEmail :: Maybe Email, -- List of tours which the user has completed. completedTours :: [TourId], - organizationMemberships :: [UserHandle], + organizationMemberships :: [OrgMembershipInfo], isSuperadmin :: Bool, planTier :: PlanTier, displayInfo :: UnifiedDisplayInfo, diff --git a/transcripts/share-apis/code-browse/account.json b/transcripts/share-apis/code-browse/account.json index 1e522584..1c90dee4 100644 --- a/transcripts/share-apis/code-browse/account.json +++ b/transcripts/share-apis/code-browse/account.json @@ -8,7 +8,33 @@ "kind": "user", "name": "The Transcript User", "organizationMemberships": [ - "unison" + { + "org": { + "isCommercial": false, + "orgId": "ORG-", + "user": { + "avatarUrl": "https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", + "handle": "unison", + "name": "Unison Org", + "userId": "U-" + } + }, + "permissions": [ + "project:view", + "project:contribute", + "project:maintain", + "project:create", + "org:view", + "org:edit", + "team:view", + "notification_hub_entry:view", + "notification_hub_entry:update", + "notification_subscription:view", + "notification_subscription:manage", + "notification_delivery_method:view", + "notification_delivery_method:manage" + ] + } ], "planTier": "Free", "primaryEmail": "transcripts@example.com",