diff --git a/changelog.d/2-features/WPB-12098 b/changelog.d/2-features/WPB-12098 new file mode 100644 index 00000000000..b70c5cb7773 --- /dev/null +++ b/changelog.d/2-features/WPB-12098 @@ -0,0 +1 @@ +Added inviter's email to `GET /teams/invitation/info` endpoint. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index d084bdf542d..5cd0e2053ba 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -818,6 +818,12 @@ listInvitations user tid = do req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", tid, "invitations"] submit "GET" req +-- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/get-team-invitation-info +getInvitationByCode :: (HasCallStack, MakesValue user) => user -> String -> App Response +getInvitationByCode user code = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", "invitations", "info"] + submit "GET" (req & addQueryParams [("code", code)]) + passwordReset :: (HasCallStack, MakesValue domain) => domain -> String -> App Response passwordReset domain email = do req <- baseRequest domain Brig Versioned "password-reset" diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index aa197ac5493..0633463ca16 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -67,6 +67,10 @@ testInvitePersonalUserToTeam = do checkListInvitations owner tid email code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString inv %. "url" & asString >>= assertUrlContainsCode code + bindResponse (getInvitationByCode user code) $ \resp -> do + resp.status `shouldMatchInt` 200 + ownersEmail <- owner %. "email" & asString + resp.json %. "created_by_email" `shouldMatch` ownersEmail acceptTeamInvitation user code Nothing >>= assertStatus 400 acceptTeamInvitation user code (Just "wrong-password") >>= assertStatus 403 @@ -123,7 +127,11 @@ testInvitePersonalUserToTeam = do checkListInvitations :: Value -> String -> String -> App () checkListInvitations owner tid email = do newUserEmail <- randomEmail - void $ postInvitation owner (PostInvitation (Just newUserEmail) Nothing) >>= assertSuccess + inv <- postInvitation owner (PostInvitation (Just newUserEmail) Nothing) >>= getJSON 201 + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + bindResponse (getInvitationByCode owner code) $ \resp -> do + resp.status `shouldMatchInt` 200 + lookupField resp.json "created_by_email" `shouldMatch` (Nothing :: Maybe Value) bindResponse (listInvitations owner tid) $ \resp -> do resp.status `shouldMatchInt` 200 invitations <- resp.json %. "invitations" >>= asList diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 8af642848d8..c08b688d032 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1626,7 +1626,7 @@ type TeamsAPI = :> MultiVerb1 'GET '[JSON] - (Respond 200 "Invitation info" Invitation) + (Respond 200 "Invitation info" InvitationUserView) ) -- FUTUREWORK: Add another endpoint to allow resending of invitation codes :<|> Named diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 49fe051705a..5c5b5c455a5 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -21,6 +21,7 @@ module Wire.API.Team.Invitation ( InvitationRequest (..), Invitation (..), + InvitationUserView (..), InvitationList (..), InvitationLocation (..), AcceptTeamInvitation (..), @@ -98,27 +99,31 @@ instance ToSchema Invitation where schema = objectWithDocModifier "Invitation" - (description ?~ "An invitation to join a team on Wire") - $ Invitation - <$> (.team) - .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema - <*> (.role) - -- clients, when leaving "role" empty, can leave the default role choice to us - .= (fromMaybe defaultRole <$> optFieldWithDocModifier "role" (description ?~ "Role of the invited user") schema) - <*> (.invitationId) - .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema - <*> (.createdAt) - .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema - <*> (.createdBy) - .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) - <*> (.inviteeEmail) - .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema - <*> (.inviteeName) - .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) - <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inviteeUrl) - .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) - where - urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) + (description ?~ "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.") + invitationObjectSchema + +invitationObjectSchema :: ObjectSchema SwaggerDoc Invitation +invitationObjectSchema = + Invitation + <$> (.team) + .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema + <*> (.role) + -- clients, when leaving "role" empty, can leave the default role choice to us + .= (fromMaybe defaultRole <$> optFieldWithDocModifier "role" (description ?~ "Role of the invited user") schema) + <*> (.invitationId) + .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema + <*> (.createdAt) + .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema + <*> (.createdBy) + .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) + <*> (.inviteeEmail) + .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema + <*> (.inviteeName) + .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) + <*> (fmap (TE.decodeUtf8 . serializeURIRef') . (.inviteeUrl)) + .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) + where + urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) newtype InvitationLocation = InvitationLocation { unInvitationLocation :: ByteString @@ -175,10 +180,8 @@ instance ToSchema InvitationList where schema = objectWithDocModifier "InvitationList" (description ?~ "A list of sent team invitations.") $ InvitationList - <$> ilInvitations - .= field "invitations" (array schema) - <*> ilHasMore - .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema + <$> ilInvitations .= field "invitations" (array schema) + <*> ilHasMore .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema -------------------------------------------------------------------------------- -- AcceptTeamInvitation @@ -196,3 +199,18 @@ instance ToSchema AcceptTeamInvitation where AcceptTeamInvitation <$> code .= fieldWithDocModifier "code" (description ?~ "Invitation code to accept.") schema <*> password .= fieldWithDocModifier "password" (description ?~ "The user account password.") schema + +data InvitationUserView = InvitationUserView + { invitation :: Invitation, + inviterEmail :: Maybe EmailAddress + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform InvitationUserView) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema InvitationUserView) + +instance ToSchema InvitationUserView where + schema = + object "InvitationUserView" $ + InvitationUserView + <$> invitation .= invitationObjectSchema + <*> inviterEmail .= maybe_ (optField "created_by_email" schema) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 63fbe936877..7bd3fdc9952 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -1293,7 +1293,28 @@ tests = testGroup "Golden: InvitationRequest_team" $ testObjects [(Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_1, "testObject_InvitationRequest_team_1.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_2, "testObject_InvitationRequest_team_2.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_3, "testObject_InvitationRequest_team_3.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_4, "testObject_InvitationRequest_team_4.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_5, "testObject_InvitationRequest_team_5.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_6, "testObject_InvitationRequest_team_6.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_7, "testObject_InvitationRequest_team_7.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_8, "testObject_InvitationRequest_team_8.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_9, "testObject_InvitationRequest_team_9.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_10, "testObject_InvitationRequest_team_10.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_11, "testObject_InvitationRequest_team_11.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_12, "testObject_InvitationRequest_team_12.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_13, "testObject_InvitationRequest_team_13.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_14, "testObject_InvitationRequest_team_14.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_15, "testObject_InvitationRequest_team_15.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_16, "testObject_InvitationRequest_team_16.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_17, "testObject_InvitationRequest_team_17.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_18, "testObject_InvitationRequest_team_18.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_19, "testObject_InvitationRequest_team_19.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_20, "testObject_InvitationRequest_team_20.json")], testGroup "Golden: Invitation_team" $ - testObjects [(Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_1, "testObject_Invitation_team_1.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_2, "testObject_Invitation_team_2.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_3, "testObject_Invitation_team_3.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_4, "testObject_Invitation_team_4.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_5, "testObject_Invitation_team_5.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_6, "testObject_Invitation_team_6.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_7, "testObject_Invitation_team_7.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_8, "testObject_Invitation_team_8.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_9, "testObject_Invitation_team_9.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_10, "testObject_Invitation_team_10.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_11, "testObject_Invitation_team_11.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_12, "testObject_Invitation_team_12.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_13, "testObject_Invitation_team_13.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_14, "testObject_Invitation_team_14.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_15, "testObject_Invitation_team_15.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_16, "testObject_Invitation_team_16.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_17, "testObject_Invitation_team_17.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_18, "testObject_Invitation_team_18.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_19, "testObject_Invitation_team_19.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_20, "testObject_Invitation_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_1, "testObject_Invitation_team_1.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_2, "testObject_Invitation_team_2.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_3, "testObject_Invitation_team_3.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_4, "testObject_Invitation_team_4.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_5, "testObject_Invitation_team_5.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_6, "testObject_Invitation_team_6.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_7, "testObject_Invitation_team_7.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_8, "testObject_Invitation_team_8.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_9, "testObject_Invitation_team_9.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_10, "testObject_Invitation_team_10.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_11, "testObject_Invitation_team_11.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_12, "testObject_Invitation_team_12.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_13, "testObject_Invitation_team_13.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_14, "testObject_Invitation_team_14.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_15, "testObject_Invitation_team_15.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_16, "testObject_Invitation_team_16.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_17, "testObject_Invitation_team_17.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_18, "testObject_Invitation_team_18.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_19, "testObject_Invitation_team_19.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_20, "testObject_Invitation_team_20.json") + ], testGroup "Golden: InvitationList_team" $ testObjects [(Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_1, "testObject_InvitationList_team_1.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_2, "testObject_InvitationList_team_2.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_3, "testObject_InvitationList_team_3.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_4, "testObject_InvitationList_team_4.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_5, "testObject_InvitationList_team_5.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_6, "testObject_InvitationList_team_6.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_7, "testObject_InvitationList_team_7.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_8, "testObject_InvitationList_team_8.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_9, "testObject_InvitationList_team_9.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_10, "testObject_InvitationList_team_10.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_11, "testObject_InvitationList_team_11.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_12, "testObject_InvitationList_team_12.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_13, "testObject_InvitationList_team_13.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_14, "testObject_InvitationList_team_14.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_15, "testObject_InvitationList_team_15.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_16, "testObject_InvitationList_team_16.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_17, "testObject_InvitationList_team_17.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_18, "testObject_InvitationList_team_18.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_19, "testObject_InvitationList_team_19.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_20, "testObject_InvitationList_team_20.json")], testGroup "Golden: NewLegalHoldService_team" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs index c63ff90e0ee..16a84144a6d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs @@ -20,7 +20,7 @@ module Test.Wire.API.Golden.Generated.Invitation_team where import Data.Id (Id (Id)) import Data.Json.Util (readUTCTimeMillis) import Data.UUID qualified as UUID (fromString) -import Imports (Maybe (Just, Nothing), fromJust) +import Imports import Wire.API.Team.Invitation (Invitation (..)) import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) import Wire.API.User.Identity diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index 3a898d764ce..afe0fb45da2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -39,6 +39,7 @@ import Test.Wire.API.Golden.Manual.FederationRestriction import Test.Wire.API.Golden.Manual.FederationStatus import Test.Wire.API.Golden.Manual.GetPaginatedConversationIds import Test.Wire.API.Golden.Manual.GroupId +import Test.Wire.API.Golden.Manual.InvitationUserView import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.ListUsersById import Test.Wire.API.Golden.Manual.LoginId_user @@ -311,5 +312,10 @@ tests = (testObject_Activate_user_2, "testObject_Activate_user_2.json"), (testObject_Activate_user_3, "testObject_Activate_user_3.json"), (testObject_Activate_user_4, "testObject_Activate_user_4.json") + ], + testGroup "InvitationUserView" $ + testObjects + [ (testObject_InvitationUserView_team_1, "testObject_InvitationUserView_team_1.json"), + (testObject_InvitationUserView_team_2, "testObject_InvitationUserView_team_2.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/InvitationUserView.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/InvitationUserView.hs new file mode 100644 index 00000000000..be52d020ff9 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/InvitationUserView.hs @@ -0,0 +1,61 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.InvitationUserView where + +import Data.Id (Id (Id)) +import Data.Json.Util (readUTCTimeMillis) +import Data.UUID qualified as UUID (fromString) +import Imports +import Wire.API.Team.Invitation +import Wire.API.Team.Role +import Wire.API.User.Identity +import Wire.API.User.Profile (Name (Name, fromName)) + +testObject_InvitationUserView_team_1 :: InvitationUserView +testObject_InvitationUserView_team_1 = + InvitationUserView + { invitation = + Invitation + { team = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000002")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-11T20:13:15.856Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing + }, + inviterEmail = Just $ unsafeEmailAddress "some" "example" + } + +testObject_InvitationUserView_team_2 :: InvitationUserView +testObject_InvitationUserView_team_2 = + InvitationUserView + { invitation = + Invitation + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T14:47:35.551Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just (Name {fromName = "\1067847} 2pGEW+\rT\171609p\174643\157218&\146145v0\b"}), + inviteeUrl = Nothing + }, + inviterEmail = Nothing + } diff --git a/libs/wire-api/test/golden/testObject_InvitationUserView_team_1.json b/libs/wire-api/test/golden/testObject_InvitationUserView_team_1.json new file mode 100644 index 00000000000..7932e3f38be --- /dev/null +++ b/libs/wire-api/test/golden/testObject_InvitationUserView_team_1.json @@ -0,0 +1,11 @@ +{ + "created_at": "1864-05-11T20:13:15.856Z", + "created_by": "00000002-0000-0000-0000-000100000001", + "created_by_email": "some@example", + "email": "some@example", + "id": "00000002-0000-0001-0000-000200000000", + "name": null, + "role": "admin", + "team": "00000002-0000-0001-0000-000200000002", + "url": null +} diff --git a/libs/wire-api/test/golden/testObject_InvitationUserView_team_2.json b/libs/wire-api/test/golden/testObject_InvitationUserView_team_2.json new file mode 100644 index 00000000000..c03242304f4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_InvitationUserView_team_2.json @@ -0,0 +1,10 @@ +{ + "created_at": "1864-05-12T14:47:35.551Z", + "created_by": "00000002-0000-0001-0000-000200000001", + "email": "some@example", + "id": "00000002-0000-0001-0000-000100000002", + "name": "􄭇} 2pGEW+\rT𩹙p𪨳𦘢&𣫡v0\u0008", + "role": "partner", + "team": "00000000-0000-0001-0000-000000000000", + "url": null +} diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 1b61c678a71..861fd9ce87b 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -595,6 +595,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.FederationStatus Test.Wire.API.Golden.Manual.GetPaginatedConversationIds Test.Wire.API.Golden.Manual.GroupId + Test.Wire.API.Golden.Manual.InvitationUserView Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.ListUsersById Test.Wire.API.Golden.Manual.Login_user diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index ef03dadc7db..c07ca6ee8c0 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -3,6 +3,7 @@ module Wire.EmailSubsystem.Interpreter ( emailSubsystemInterpreter, mkMimeAddress, + renderInvitationUrl, ) where diff --git a/libs/wire-subsystems/src/Wire/InvitationStore.hs b/libs/wire-subsystems/src/Wire/InvitationStore.hs index e691f516bf7..04a35c3ce36 100644 --- a/libs/wire-subsystems/src/Wire/InvitationStore.hs +++ b/libs/wire-subsystems/src/Wire/InvitationStore.hs @@ -40,6 +40,7 @@ data StoredInvitation = MkStoredInvitation invitationId :: InvitationId, createdAt :: UTCTimeMillis, createdBy :: Maybe UserId, + -- | The invitee's email address email :: EmailAddress, name :: Maybe Name, code :: InvitationCode diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs index 46445645c9d..65c209310ad 100644 --- a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -159,7 +159,6 @@ createInvitation' tid mExpectedInvId inviteeRole mbInviterUid inviterEmail invRe inviteeEmail = email, inviteeName = invRequest.inviteeName, code = code - -- mUrl = mUrl } in Store.insertInvitation insertInv timeout diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 564799f2b6b..abe7db12694 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -202,6 +202,10 @@ getLocalAccountBy includePendingInvitations uid = } ) +getUserEmail :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe EmailAddress) +getUserEmail lusr = + (>>= userEmail) <$> getLocalAccountBy WithPendingInvitations lusr + getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe User) getLocalUserAccountByUserKey q@(tUnqualified -> ek) = listToMaybe <$> getAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index e6fcd9f0d43..9cfd621c9bf 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -40,10 +40,8 @@ import Data.Id import Data.List1 qualified as List1 import Data.Qualified import Data.Range -import Data.Text.Ascii import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT -import Data.Text.Lazy qualified as Text import Imports hiding (head) import Network.Wai.Utilities hiding (Error, code, message) import Polysemy @@ -67,9 +65,9 @@ import Wire.API.Team.Invitation qualified as Public import Wire.API.Team.Member (teamMembers) import Wire.API.Team.Member qualified as Teams import Wire.API.Team.Permission (Perm (AddTeamMember)) -import Wire.API.Team.Role import Wire.API.User hiding (fromEmail) import Wire.BlockListStore +import Wire.EmailSubsystem.Interpreter (renderInvitationUrl) import Wire.EmailSubsystem.Template import Wire.Error import Wire.Events (Events) @@ -80,6 +78,7 @@ import Wire.InvitationStore (InvitationStore (..), PaginatedResult (..), StoredI import Wire.InvitationStore qualified as Store import Wire.Sem.Concurrency import Wire.TeamInvitationSubsystem +import Wire.TeamInvitationSubsystem.Interpreter (toInvitation) import Wire.UserKeyStore import Wire.UserSubsystem import Wire.UserSubsystem.Error @@ -234,54 +233,32 @@ listInvitations uid tid startingId mSize = do -- To create the correct team invitation URL, we need to detect whether the invited account already exists. -- Optimization: if url is not to be shown, do not check for existing personal user. toInvitationHack :: ShowOrHideInvitationUrl -> StoredInvitation -> Sem r Invitation - toInvitationHack HideInvitationUrl si = toInvitation False HideInvitationUrl si -- isPersonalUserMigration is always ignored here + toInvitationHack HideInvitationUrl si = + toInvitation "" HideInvitationUrl si -- isPersonalUserMigration is always ignored here toInvitationHack ShowInvitationUrl si = do isPersonalUserMigration <- isPersonalUser (mkEmailKey si.email) - toInvitation isPersonalUserMigration ShowInvitationUrl si + template <- + if isPersonalUserMigration + then invitationEmailUrl . existingUserInvitationEmail <$> input + else invitationEmailUrl . invitationEmail <$> input + let url = renderInvitationUrl template tid si.code id + toInvitation url ShowInvitationUrl si --- | brig used to not store the role, so for migration we allow this to be empty and fill in the --- default here. -toInvitation :: +mkInviteUrl :: + forall r. ( Member TinyLog r, Member (Input TeamTemplates) r ) => - Bool -> ShowOrHideInvitationUrl -> - StoredInvitation -> - Sem r Invitation -toInvitation isPersonalUserMigration showUrl storedInv = do - url <- - if isPersonalUserMigration - then mkInviteUrlPersonalUser showUrl storedInv.teamId storedInv.code - else mkInviteUrl showUrl storedInv.teamId storedInv.code - pure $ - Invitation - { team = storedInv.teamId, - role = fromMaybe defaultRole storedInv.role, - invitationId = storedInv.invitationId, - createdAt = storedInv.createdAt, - createdBy = storedInv.createdBy, - inviteeEmail = storedInv.email, - inviteeName = storedInv.name, - inviteeUrl = url - } - -getInviteUrl :: - forall r. - (Member TinyLog r) => - InvitationEmailTemplate -> TeamId -> - AsciiText Base64Url -> + InvitationCode -> Sem r (Maybe (URIRef Absolute)) -getInviteUrl (invitationEmailUrl -> template) team code = do - let branding = id -- url is not branded - let url = Text.toStrict $ renderTextWithBranding template replace branding +mkInviteUrl HideInvitationUrl _ _ = pure Nothing +mkInviteUrl ShowInvitationUrl team c = do + template <- invitationEmailUrl . invitationEmail <$> input + let url = renderInvitationUrl template team c id parseHttpsUrl url where - replace "team" = idToText team - replace "code" = toText code - replace x = x - parseHttpsUrl :: Text -> Sem r (Maybe (URIRef Absolute)) parseHttpsUrl url = either (\e -> Nothing <$ logError url e) (pure . Just) $ @@ -293,32 +270,6 @@ getInviteUrl (invitationEmailUrl -> template) team code = do . Log.field "url" url . Log.field "error" (show e) -mkInviteUrl :: - ( Member TinyLog r, - Member (Input TeamTemplates) r - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationCode -> - Sem r (Maybe (URIRef Absolute)) -mkInviteUrl HideInvitationUrl _ _ = pure Nothing -mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do - template <- invitationEmail <$> input - getInviteUrl template team c - -mkInviteUrlPersonalUser :: - ( Member TinyLog r, - Member (Input TeamTemplates) r - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationCode -> - Sem r (Maybe (URIRef Absolute)) -mkInviteUrlPersonalUser HideInvitationUrl _ _ = pure Nothing -mkInviteUrlPersonalUser ShowInvitationUrl team (InvitationCode c) = do - template <- existingUserInvitationEmail <$> input - getInviteUrl template team c - getInvitation :: ( Member GalleyAPIAccess r, Member InvitationStore r, @@ -332,7 +283,6 @@ getInvitation :: Sem r (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - invitationM <- Store.lookupInvitation tid iid case invitationM of Nothing -> pure Nothing @@ -350,14 +300,24 @@ isPersonalUser uke = do Just account -> account.userStatus == Active && isNothing account.userTeam getInvitationByCode :: + forall r. ( Member Store.InvitationStore r, - Member (Error UserSubsystemError) r + Member (Error UserSubsystemError) r, + Member UserSubsystem r, + Member (Input (Local ())) r ) => InvitationCode -> - Sem r Public.Invitation + Sem r Public.InvitationUserView getInvitationByCode c = do - inv <- Store.lookupInvitationByCode c - maybe (throw UserSubsystemInvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv + storedInv <- + Store.lookupInvitationByCode c + >>= note UserSubsystemInvalidInvitationCode + let inv = Store.invitationFromStored Nothing storedInv + mInviterEmail <- + isPersonalUser (mkEmailKey inv.inviteeEmail) >>= \case + False -> pure Nothing + True -> maybe (pure Nothing) (qualifyLocal' >=> getUserEmail) inv.createdBy + pure $ InvitationUserView {invitation = inv, inviterEmail = mInviterEmail} headInvitationByEmail :: (Member InvitationStore r, Member TinyLog r) =>