{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2022 Wire Swiss GmbH <opensource@wire.com>
--
-- 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 <https://www.gnu.org/licenses/>.

module Wire.API.Routes.Public.Brig where

import Control.Lens ((?~))
import Data.Aeson qualified as A (FromJSON, ToJSON, Value)
import Data.ByteString.Conversion
import Data.Code (Timeout)
import Data.CommaSeparatedList (CommaSeparatedList)
import Data.Domain
import Data.Handle
import Data.Id as Id
import Data.Json.Util
import Data.Misc
import Data.Nonce (Nonce)
import Data.OpenApi hiding (Contact, Header, Schema, ToSchema)
import Data.OpenApi qualified as S
import Data.Qualified (Qualified (..))
import Data.Range
import Data.SOP
import Data.Schema as Schema
import Generics.SOP qualified as GSOP
import Imports hiding (head)
import Network.Wai.Utilities
import Servant (JSON)
import Servant hiding (Handler, JSON, addHeader, respond)
import Servant.OpenApi.Internal.Orphans ()
import Wire.API.Call.Config (RTCConfiguration)
import Wire.API.Connection hiding (MissingLegalholdConsent)
import Wire.API.Deprecated
import Wire.API.Error
import Wire.API.Error.Brig as ErrorBrig
import Wire.API.Error.Empty
import Wire.API.MLS.CipherSuite
import Wire.API.MLS.KeyPackage
import Wire.API.MLS.Servant
import Wire.API.OAuth
import Wire.API.Pagination
import Wire.API.Properties (PropertyKey, PropertyKeysAndValues, RawPropertyValue)
import Wire.API.Routes.API
import Wire.API.Routes.Bearer
import Wire.API.Routes.Cookies
import Wire.API.Routes.MultiVerb
import Wire.API.Routes.Named
import Wire.API.Routes.Public
import Wire.API.Routes.Public.Brig.Bot (BotAPI)
import Wire.API.Routes.Public.Brig.DomainVerification
import Wire.API.Routes.Public.Brig.OAuth (OAuthAPI)
import Wire.API.Routes.Public.Brig.Provider (ProviderAPI)
import Wire.API.Routes.Public.Brig.Services (ServicesAPI)
import Wire.API.Routes.Public.Util
import Wire.API.Routes.QualifiedCapture
import Wire.API.Routes.Version
import Wire.API.Routes.Versioned
import Wire.API.SystemSettings
import Wire.API.Team.Invitation
import Wire.API.Team.Size
import Wire.API.User hiding (NoIdentity)
import Wire.API.User.Activation
import Wire.API.User.Auth
import Wire.API.User.Client
import Wire.API.User.Client.DPoPAccessToken
import Wire.API.User.Client.Prekey
import Wire.API.User.Handle
import Wire.API.User.Password (CompletePasswordReset, NewPasswordReset, PasswordReset, PasswordResetKey)
import Wire.API.User.RichInfo (RichInfoAssocList)
import Wire.API.User.Search
import Wire.API.UserGroup
import Wire.API.UserGroup.Pagination
import Wire.API.UserMap

type BrigAPI =
  UserAPI
    :<|> SelfAPI
    :<|> AccountAPI
    :<|> ClientAPI
    :<|> PrekeyAPI
    :<|> UserClientAPI
    :<|> ConnectionAPI
    :<|> PropertiesAPI
    :<|> MLSAPI
    :<|> UserHandleAPI
    :<|> SearchAPI
    :<|> AuthAPI
    :<|> CallingAPI
    :<|> TeamsAPI
    :<|> SystemSettingsAPI
    :<|> OAuthAPI
    :<|> BotAPI
    :<|> ServicesAPI
    :<|> ProviderAPI
    :<|> DomainVerificationAPI
    :<|> DomainVerificationTeamAPI
    :<|> DomainVerificationChallengeAPI
    :<|> UserGroupAPI
    :<|> AppsAPI

data BrigAPITag

instance ServiceAPI BrigAPITag v where
  type ServiceAPIRoutes BrigAPITag = BrigAPI

-------------------------------------------------------------------------------
-- User API

type MaxUsersForListClientsBulk = 500

type GetUserVerb =
  MultiVerb
    'GET
    '[JSON]
    '[ ErrorResponse 'UserNotFound,
       Respond 200 "User found" UserProfile
     ]
    (Maybe UserProfile)

type CaptureUserId name = Capture' '[Description "User Id"] name UserId

type QualifiedCaptureUserId name = QualifiedCapture' '[Description "User Id"] name UserId

type CaptureClientId name = Capture' '[Description "ClientId"] name ClientId

type DeleteSelfResponses =
  '[ RespondEmpty 200 "Deletion is initiated.",
     RespondWithDeletionCodeTimeout
   ]

newtype RespondWithDeletionCodeTimeout
  = RespondWithDeletionCodeTimeout
      (Respond 202 "Deletion is pending verification with a code." DeletionCodeTimeout)
  deriving (IsResponse '[JSON], IsSwaggerResponse)

type instance ResponseType RespondWithDeletionCodeTimeout = DeletionCodeTimeout

instance AsUnion DeleteSelfResponses (Maybe Timeout) where
  toUnion (Just t) = S (Z (I (DeletionCodeTimeout t)))
  toUnion Nothing = Z (I ())
  fromUnion (Z (I ())) = Nothing
  fromUnion (S (Z (I (DeletionCodeTimeout t)))) = Just t
  fromUnion (S (S x)) = case x of {}

type ConnectionUpdateResponses = UpdateResponses "Connection unchanged" "Connection updated" UserConnection

type UserAPI =
  Named
    "get-user-unqualified"
    ( Summary "Get a user by UserId"
        :> Until 'V2
        :> ZLocalUser
        :> "users"
        :> CaptureUserId "uid"
        :> GetUserVerb
    )
    :<|> Named
           "get-user-qualified"
           ( Summary "Get a user by Domain and UserId"
               :> ZLocalUser
               :> "users"
               :> QualifiedCaptureUserId "uid"
               :> GetUserVerb
           )
    :<|> Named
           "update-user-email"
           ( Summary "Resend email address validation email."
               :> Description "If the user has a pending email validation, the validation email will be resent."
               :> ZUser
               :> "users"
               :> CaptureUserId "uid"
               :> "email"
               :> ReqBody '[JSON] EmailUpdate
               :> Put '[JSON] ()
           )
    :<|> Named
           "get-handle-info-unqualified"
           ( Summary "(deprecated, use /search/contacts) Get information on a user handle"
               :> Until 'V2
               :> ZUser
               :> "users"
               :> "handles"
               :> Capture' '[Description "The user handle"] "handle" Handle
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ ErrorResponse 'HandleNotFound,
                       Respond 200 "User found" UserHandleInfo
                     ]
                    (Maybe UserHandleInfo)
           )
    :<|> Named
           "get-user-by-handle-qualified"
           ( Summary "(deprecated, use /search/contacts) Get information on a user handle"
               :> Until 'V2
               :> ZUser
               :> "users"
               :> "by-handle"
               :> QualifiedCapture' '[Description "The user handle"] "handle" Handle
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ ErrorResponse 'HandleNotFound,
                       Respond 200 "User found" UserProfile
                     ]
                    (Maybe UserProfile)
           )
    :<|> Named
           "list-users-by-unqualified-ids-or-handles"
           ( Summary "List users (deprecated)"
               :> Until 'V2
               :> Description "The 'ids' and 'handles' parameters are mutually exclusive."
               :> ZUser
               :> "users"
               :> QueryParam' [Optional, Strict, Description "User IDs of users to fetch"] "ids" (CommaSeparatedList UserId)
               :> QueryParam' [Optional, Strict, Description "Handles of users to fetch, min 1 and max 4 (the check for handles is rather expensive)"] "handles" (Range 1 4 (CommaSeparatedList Handle))
               :> Get '[JSON] [UserProfile]
           )
    :<|> Named
           "list-users-by-ids-or-handles"
           ( Summary "List users"
               :> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive."
               :> ZUser
               :> From 'V4
               :> "list-users"
               :> QueryParam' [Optional, Strict, Description "Include whether each local user can currently be contacted"] "include-contact-status" Bool
               :> ReqBody '[JSON] ListUsersQuery
               :> Post '[JSON] ListUsersById
           )
    :<|> Named
           "list-users-by-ids-or-handles@V3"
           ( Summary "List users"
               :> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive."
               :> ZUser
               :> Until 'V4
               :> "list-users"
               :> ReqBody '[JSON] ListUsersQuery
               :> Post '[JSON] [UserProfile]
           )
    :<|> Named
           "send-verification-code"
           ( Summary "Send a verification code to a given email address."
               :> "verification-code"
               :> "send"
               :> ReqBody '[JSON] SendVerificationCode
               :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Verification code sent."] ()
           )
    :<|> Named
           "get-rich-info"
           ( Summary "Get a user's rich info"
               :> CanThrow 'InsufficientTeamPermissions
               :> ZLocalUser
               :> "users"
               :> CaptureUserId "uid"
               :> "rich-info"
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[Respond 200 "Rich info about the user" RichInfoAssocList]
                    RichInfoAssocList
           )
    :<|> Named
           "get-supported-protocols"
           ( Summary "Get a user's supported protocols"
               :> From 'V5
               :> ZLocalUser
               :> "users"
               :> QualifiedCaptureUserId "uid"
               :> "supported-protocols"
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (Respond 200 "Protocols supported by the user" (Set BaseProtocolTag))
           )
    :<|> Named
           "set-user-searchable"
           ( Summary "Set user's visibility in search"
               :> From 'V12
               :> ZLocalUser
               :> "users"
               :> CaptureUserId "uid"
               :> "searchable"
               :> ReqBody '[JSON] SetSearchable
               :> Post '[JSON] ()
           )

type LastSeenNameDesc = Description "`name` of the last seen user group, used to get the next page when sorting by name."

type LastSeenCreatedAtDesc = Description "`created_at` field of the last seen user group, used to get the next page when sorting by created_at."

type LastSeenIdDesc = Description "`id` of the last seen group, used to get the next page. **Must** be sent to get the next page."

type UserGroupAPI =
  Named
    "create-user-group"
    ( From 'V10
        :> CanThrow 'UserGroupNotATeamAdmin
        :> CanThrow 'UserGroupMemberIsNotInTheSameTeam
        :> ZLocalUser
        :> "user-groups"
        :> ReqBody '[JSON] NewUserGroup
        :> Post '[JSON] UserGroup
    )
    :<|> Named
           "get-user-group"
           ( Summary "Fetch a group accessible to the logged-in user"
               :> From 'V10
               :> ZLocalUser
               :> CanThrow 'UserGroupNotFound
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> QueryFlag "include_channels"
               :> MultiVerb
                    'GET
                    '[JSON]
                    [ ErrorResponse 'UserGroupNotFound,
                      Respond 200 "User Group Found" UserGroup
                    ]
                    (Maybe UserGroup)
           )
    :<|> Named
           "get-user-groups"
           ( Summary "Fetch groups accessible to the logged-in user"
               :> From 'V10
               :> ZLocalUser
               :> "user-groups"
               :> QueryParam' '[Optional, Strict, Description "Search string"] "q" Text
               :> QueryParam' '[Optional, Strict] "sort_by" SortBy
               :> QueryParam' '[Optional, Strict] "sort_order" SortOrder
               :> QueryParam' '[Optional, Strict] "page_size" PageSize
               :> QueryParam' '[Optional, Strict, LastSeenNameDesc] "last_seen_name" UserGroupName
               :> QueryParam' '[Optional, Strict, LastSeenCreatedAtDesc] "last_seen_created_at" UTCTimeMillis
               :> QueryParam' '[Optional, Strict, LastSeenIdDesc] "last_seen_id" UserGroupId
               :> QueryFlag "include_channels"
               :> QueryFlag "include_member_count"
               :> Get '[JSON] UserGroupPage
           )
    :<|> Named
           "update-user-group"
           ( From 'V10
               :> ZLocalUser
               :> CanThrow 'UserGroupNotFound
               :> CanThrow 'UserGroupNotATeamAdmin
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> ReqBody '[JSON] UserGroupUpdate
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "User added updated")
           )
    :<|> Named
           "delete-user-group"
           ( From 'V10
               :> ZLocalUser
               :> CanThrow 'UserGroupNotFound
               :> CanThrow 'UserGroupNotATeamAdmin
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 204 "User group deleted")
           )
    :<|> Named
           "add-user-to-group"
           ( From 'V10
               :> ZLocalUser
               :> CanThrow 'UserGroupNotFound
               :> CanThrow 'UserGroupNotATeamAdmin
               :> CanThrow 'UserGroupMemberIsNotInTheSameTeam
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> "users"
               :> Capture "uid" UserId
               :> MultiVerb1 'POST '[JSON] (RespondEmpty 204 "User added to group")
           )
    :<|> Named
           "add-users-to-group-bulk"
           ( From 'V11
               :> ZLocalUser
               :> CanThrow 'UserGroupNotFound
               :> CanThrow 'UserGroupNotATeamAdmin
               :> CanThrow 'UserGroupMemberIsNotInTheSameTeam
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> "users"
               :> ReqBody '[JSON] UserGroupAddUsers
               :> MultiVerb1 'POST '[JSON] (RespondEmpty 204 "Users added to group")
           )
    :<|> Named
           "remove-user-from-group"
           ( From 'V10
               :> ZLocalUser
               :> CanThrow 'UserGroupNotFound
               :> CanThrow 'UserGroupNotATeamAdmin
               :> CanThrow 'UserGroupMemberIsNotInTheSameTeam
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> "users"
               :> Capture "uid" UserId
               :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 204 "User removed from group")
           )
    :<|> Named
           "update-user-group-members"
           ( Summary "[STUB] Update user group members. Replaces the users with the given list."
               :> From 'V13
               :> ZLocalUser
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> "users"
               :> ReqBody '[JSON] UpdateUserGroupMembers
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "User group members updated")
           )
    :<|> Named
           "update-user-group-channels"
           ( Summary "Replaces the channels with the given list."
               :> From 'V13
               :> CanThrow 'UserGroupNotFound
               :> CanThrow 'UserGroupNotATeamAdmin
               :> ZLocalUser
               :> "user-groups"
               :> Capture "gid" UserGroupId
               :> "channels"
               :> QueryFlag "append_only"
               :> ReqBody '[JSON] UpdateUserGroupChannels
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "User group channels updated")
           )
    :<|> Named
           "check-user-group-name-available"
           ( Summary "[STUB] Check if a user group name is available"
               :> From 'V13
               :> ZLocalUser
               :> "user-groups"
               :> "check-name"
               :> ReqBody '[JSON] CheckUserGroupName
               :> MultiVerb 'POST '[JSON] '[Respond 200 "OK" UserGroupNameAvailability] UserGroupNameAvailability
           )

type SelfAPI =
  Named
    "get-self"
    ( Summary "Get your own profile"
        :> DescriptionOAuthScope 'ReadSelf
        :> ZLocalUser
        :> "self"
        :> Get '[JSON] SelfProfile
    )
    :<|>
    -- This endpoint can lead to the following events being sent:
    -- - UserDeleted event to contacts of self
    -- - MemberLeave event to members for all conversations the user was in (via galley)
    Named
      "delete-self"
      ( Summary "Initiate account deletion."
          :> Description
               "if the account has a verified identity, a verification \
               \code is sent and needs to be confirmed to authorise the \
               \deletion. if the account has no verified identity but a \
               \password, it must be provided. if password is correct, or if neither \
               \a verified identity nor a password exists, account deletion \
               \is scheduled immediately."
          :> CanThrow 'InvalidUser
          :> CanThrow 'InvalidCode
          :> CanThrow 'BadCredentials
          :> CanThrow 'MissingAuth
          :> CanThrow 'DeleteCodePending
          :> CanThrow 'OwnerDeletingSelf
          :> ZLocalUser
          :> "self"
          :> ReqBody '[JSON] DeleteUser
          :> MultiVerb 'DELETE '[JSON] DeleteSelfResponses (Maybe Timeout)
      )
    :<|>
    -- This endpoint can lead to the following events being sent:
    -- - UserUpdated event to contacts of self
    Named
      "put-self"
      ( Summary "Update your profile."
          :> ZLocalUser
          :> ZConn
          :> "self"
          :> ReqBody '[JSON] UserUpdate
          :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "User updated")
      )
    :<|> Named
           "change-phone"
           ( Summary "Change your phone number."
               :> Until 'V6
               :> ZUser
               :> ZConn
               :> "self"
               :> "phone"
               :> ReqBody '[JSON] PhoneUpdate
               :> MultiVerb 'PUT '[JSON] ChangePhoneResponses (Maybe ChangePhoneError)
           )
    :<|>
    -- This endpoint can lead to the following events being sent:
    -- - UserIdentityRemoved event to self
    Named
      "remove-phone"
      ( Summary "Remove your phone number."
          :> Until 'V6
          :> Description
               "Your phone number can only be removed if you also have an \
               \email address and a password."
          :> ZUser
          :> "self"
          :> "phone"
          :> MultiVerb 'DELETE '[JSON] RemoveIdentityResponses (Maybe RemoveIdentityError)
      )
    :<|>
    -- This endpoint can lead to the following events being sent:
    -- - UserIdentityRemoved event to self
    Named
      "remove-email"
      ( Summary "Remove your email address."
          :> Description
               "Your email address can only be removed if you also have a \
               \phone number."
          :> ZLocalUser
          :> "self"
          :> "email"
          :> MultiVerb 'DELETE '[JSON] RemoveIdentityResponses (Maybe RemoveIdentityError)
      )
    :<|> Named
           "check-password-exists"
           ( Summary "Check that your password is set."
               :> ZUser
               :> "self"
               :> "password"
               :> MultiVerb
                    'HEAD
                    '()
                    '[ RespondEmpty 404 "Password is not set",
                       RespondEmpty 200 "Password is set"
                     ]
                    Bool
           )
    :<|> Named
           "change-password"
           ( Summary "Change your password."
               :> ZUser
               :> "self"
               :> "password"
               :> ReqBody '[JSON] PasswordChange
               :> MultiVerb 'PUT '[JSON] ChangePasswordResponses (Maybe ChangePasswordError)
           )
    :<|> Named
           "change-locale"
           ( Summary "Change your locale."
               :> ZLocalUser
               :> ZConn
               :> "self"
               :> "locale"
               :> ReqBody '[JSON] LocaleUpdate
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Local Changed")
           )
    :<|> Named
           "change-handle"
           ( Summary "Change your handle."
               :> ZLocalUser
               :> ZConn
               :> "self"
               :> "handle"
               :> ReqBody '[JSON] HandleUpdate
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Handle Changed")
           )
    :<|> Named
           "change-supported-protocols"
           ( Summary "Change your supported protocols"
               :> From 'V5
               :> CanThrow 'MlsRemovalNotAllowed
               :> ZLocalUser
               :> ZConn
               :> "self"
               :> "supported-protocols"
               :> ReqBody '[JSON] SupportedProtocolUpdate
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Supported protocols changed")
           )

type UserHandleAPI =
  Named
    "check-user-handles@v6"
    ( Summary "Check availability of user handles"
        :> Until V7
        :> ZUser
        :> "users"
        :> "handles"
        :> ReqBody '[JSON] CheckHandles
        :> MultiVerb
             'POST
             '[JSON]
             '[Respond 200 "List of free handles" [Handle]]
             [Handle]
    )
    :<|> Named
           "check-user-handles"
           ( Summary "Check availability of user handles"
               :> From V7
               :> ZUser
               :> "handles"
               :> ReqBody '[JSON] CheckHandles
               :> MultiVerb
                    'POST
                    '[JSON]
                    '[Respond 200 "List of free handles" [Handle]]
                    [Handle]
           )
    :<|> Named
           "check-user-handle@v6"
           ( Summary "Check whether a user handle can be taken"
               :> Until V7
               :> CanThrow 'InvalidHandle
               :> CanThrow 'HandleNotFound
               :> ZUser
               :> "users"
               :> "handles"
               :> Capture "handle" Text
               :> MultiVerb
                    'HEAD
                    '[JSON]
                    '[Respond 200 "Handle is taken" ()]
                    ()
           )
    :<|> Named
           "check-user-handle"
           ( Summary "Check whether a user handle can be taken"
               :> From V7
               :> CanThrow 'InvalidHandle
               :> CanThrow 'HandleNotFound
               :> ZUser
               :> "handles"
               :> Capture "handle" Text
               :> MultiVerb
                    'HEAD
                    '[JSON]
                    '[Respond 200 "Handle is taken" ()]
                    ()
           )

type AccountAPI =
  Named
    "upgrade-personal-to-team"
    ( Summary "Upgrade personal user to team owner"
        :> "upgrade-personal-to-team"
        :> ZLocalUser
        :> ReqBody '[JSON] BindingNewTeamUser
        :> MultiVerb
             'POST
             '[JSON]
             UpgradePersonalToTeamResponses
             (Either UpgradePersonalToTeamError CreateUserTeam)
    )
    :<|>
    -- docs/reference/user/registration.md {#RefRegistration}
    --
    -- This endpoint can lead to the following events being sent:
    -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID
    -- - UserIdentityUpdated event to created user, if email code or phone code is provided
    Named
      "register@v16"
      ( Summary "Register a new user."
          :> Until 'V17
          :> Description
               "If the environment where the registration takes \
               \place is private and a registered email address \
               \is not whitelisted, a 403 error is returned."
          :> "register"
          :> Header' '[Required, Strict] "X-Forwarded-For" IpAddr
          :> ReqBody '[JSON] NewUserPublic
          :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess)
      )
    :<|> Named
           "register"
           ( Summary "Register a new user."
               :> From 'V17
               :> Description
                    "If the environment where the registration takes \
                    \place is private and a registered email address \
                    \is not whitelisted, a 403 error is returned."
               :> "register"
               :> Header' '[Required, Strict] "X-Forwarded-For" IpAddr
               :> ReqBody '[JSON] NewUserPublic
               :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess)
           )
    -- This endpoint can lead to the following events being sent:
    -- UserDeleted event to contacts of deleted user
    -- MemberLeave event to members for all conversations the user was in (via galley)
    :<|> Named
           "verify-delete"
           ( Summary "Verify account deletion with a code."
               :> CanThrow 'InvalidCode
               :> "delete"
               :> ReqBody '[JSON] VerifyDeleteUser
               :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Deletion is initiated."] ()
           )
    -- This endpoint can lead to the following events being sent:
    -- - UserActivated event to the user, if account gets activated
    -- - UserIdentityUpdated event to the user, if email or phone get activated
    :<|> Named
           "get-activate"
           ( Summary "Activate (i.e. confirm) an email address."
               :> Description "See also 'POST /activate' which has a larger feature set."
               :> CanThrow 'UserKeyExists
               :> CanThrow 'InvalidActivationCodeWrongUser
               :> CanThrow 'InvalidActivationCodeWrongCode
               :> CanThrow 'InvalidEmail
               :> CanThrow 'InvalidPhone
               :> "activate"
               :> QueryParam' '[Required, Strict, Description "Activation key"] "key" ActivationKey
               :> QueryParam' '[Required, Strict, Description "Activation code"] "code" ActivationCode
               :> MultiVerb
                    'GET
                    '[JSON]
                    GetActivateResponse
                    ActivationRespWithStatus
           )
    -- docs/reference/user/activation.md {#RefActivationSubmit}
    --
    -- This endpoint can lead to the following events being sent:
    -- - UserActivated event to the user, if account gets activated
    -- - UserIdentityUpdated event to the user, if email or phone get activated
    :<|> Named
           "post-activate"
           ( Summary "Activate (i.e. confirm) an email address."
               :> Description
                    "Activation only succeeds once and the number of \
                    \failed attempts for a valid key is limited."
               :> CanThrow 'UserKeyExists
               :> CanThrow 'InvalidActivationCodeWrongUser
               :> CanThrow 'InvalidActivationCodeWrongCode
               :> CanThrow 'InvalidEmail
               :> CanThrow 'InvalidPhone
               :> "activate"
               :> ReqBody '[JSON] Activate
               :> MultiVerb
                    'POST
                    '[JSON]
                    GetActivateResponse
                    ActivationRespWithStatus
           )
    -- docs/reference/user/activation.md {#RefActivationRequest}
    :<|> Named
           "post-activate-send"
           ( Summary "Send (or resend) an email activation code."
               :> CanThrow 'UserKeyExists
               :> CanThrow 'InvalidEmail
               :> CanThrow 'BlacklistedEmail
               :> CanThrow 'CustomerExtensionBlockedDomain
               :> "activate"
               :> "send"
               :> ReqBody '[JSON] SendActivationCode
               :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Activation code sent."] ()
           )
    :<|> Named
           "post-password-reset"
           ( Summary "Initiate a password reset."
               :> "password-reset"
               :> ReqBody '[JSON] NewPasswordReset
               :> MultiVerb 'POST '[JSON] '[RespondEmpty 201 "Password reset code created and sent by email."] ()
           )
    :<|> Named
           "post-password-reset-complete"
           ( Summary "Complete a password reset."
               :> CanThrow 'InvalidPasswordResetCode
               :> "password-reset"
               :> "complete"
               :> ReqBody '[JSON] CompletePasswordReset
               :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Password reset successful."] ()
           )
    :<|> Named
           "post-password-reset-key-deprecated"
           ( Summary "Complete a password reset."
               :> Deprecated
               :> Until 'V8
               :> CanThrow 'PasswordResetInProgress
               :> CanThrow 'InvalidPasswordResetKey
               :> CanThrow 'InvalidPasswordResetCode
               :> CanThrow 'ResetPasswordMustDiffer
               :> Description "DEPRECATED: Use 'POST /password-reset/complete'."
               :> "password-reset"
               :> Capture' '[Description "An opaque key for a pending password reset."] "key" PasswordResetKey
               :> ReqBody '[JSON] PasswordReset
               :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Password reset successful."] ()
           )
    :<|> Named
           "onboarding"
           ( Summary "Upload contacts and invoke matching."
               :> Deprecated
               :> Until 'V8
               :> Description
                    "DEPRECATED: the feature has been turned off, the end-point does \
                    \nothing and always returns '{\"results\":[],\"auto-connects\":[]}'."
               :> ZUser
               :> "onboarding"
               :> "v3"
               :> ReqBody '[JSON] JsonValue
               :> Post '[JSON] DeprecatedMatchingResult
           )

newtype JsonValue = JsonValue {fromJsonValue :: A.Value}
  deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema JsonValue)

instance ToSchema JsonValue where
  schema = fromJsonValue .= (JsonValue <$> named "Body" jsonValue)

data DeprecatedMatchingResult = DeprecatedMatchingResult
  deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema DeprecatedMatchingResult)

instance ToSchema DeprecatedMatchingResult where
  schema =
    objectWithDocModifier
      (S.deprecated ?~ True)
      $ DeprecatedMatchingResult
        <$ const []
          .= field "results" (array (null_ @SwaggerDoc))
        <* const []
          .= field "auto-connects" (array (null_ @SwaggerDoc))

data ActivationRespWithStatus
  = ActivationResp ActivationResponse
  | ActivationRespDryRun
  | ActivationRespPass
  | ActivationRespSuccessNoIdent
  deriving (Generic)
  deriving (AsUnion GetActivateResponse) via GenericAsUnion GetActivateResponse ActivationRespWithStatus

instance GSOP.Generic ActivationRespWithStatus

type GetActivateResponse =
  '[ Respond 200 "Activation successful." ActivationResponse,
     RespondEmpty 200 "Activation successful. (Dry run)",
     RespondEmpty 204 "A recent activation was already successful.",
     RespondEmpty 200 "Activation successful."
   ]

type PrekeyAPI =
  Named
    "get-users-prekeys-client-unqualified"
    ( Summary "(deprecated) Get a prekey for a specific client of a user."
        :> Until 'V2
        :> ZUser
        :> "users"
        :> CaptureUserId "uid"
        :> "prekeys"
        :> CaptureClientId "client"
        :> Get '[JSON] ClientPrekey
    )
    :<|> Named
           "get-users-prekeys-client-qualified"
           ( Summary "Get a prekey for a specific client of a user."
               :> ZUser
               :> "users"
               :> QualifiedCaptureUserId "uid"
               :> "prekeys"
               :> CaptureClientId "client"
               :> Get '[JSON] ClientPrekey
           )
    :<|> Named
           "get-users-prekey-bundle-unqualified"
           ( Summary "(deprecated) Get a prekey for each client of a user."
               :> Until 'V2
               :> ZUser
               :> "users"
               :> CaptureUserId "uid"
               :> "prekeys"
               :> Get '[JSON] PrekeyBundle
           )
    :<|> Named
           "get-users-prekey-bundle-qualified"
           ( Summary "Get a prekey for each client of a user."
               :> ZUser
               :> "users"
               :> QualifiedCaptureUserId "uid"
               :> "prekeys"
               :> Get '[JSON] PrekeyBundle
           )
    :<|> Named
           "get-multi-user-prekey-bundle-unqualified"
           ( Summary
               "(deprecated)  Given a map of user IDs to client IDs return a prekey for each one."
               :> Description "You can't request information for more users than maximum conversation size."
               :> Until 'V2
               :> ZUser
               :> "users"
               :> "prekeys"
               :> ReqBody '[JSON] UserClients
               :> Post '[JSON] UserClientPrekeyMap
           )
    :<|> Named
           "get-multi-user-prekey-bundle-qualified@v3"
           ( Summary
               "(deprecated)  Given a map of user IDs to client IDs return a prekey for each one."
               :> Description "You can't request information for more users than maximum conversation size."
               :> ZUser
               :> Until 'V4
               :> "users"
               :> "list-prekeys"
               :> ReqBody '[JSON] QualifiedUserClients
               :> Post '[JSON] QualifiedUserClientPrekeyMap
           )
    :<|> Named
           "get-multi-user-prekey-bundle-qualified"
           ( Summary
               "(deprecated)  Given a map of user IDs to client IDs return a prekey for each one."
               :> Description "You can't request information for more users than maximum conversation size."
               :> ZUser
               :> From 'V4
               :> "users"
               :> "list-prekeys"
               :> ReqBody '[JSON] QualifiedUserClients
               :> Post '[JSON] QualifiedUserClientPrekeyMapV4
           )

-- User Client API ----------------------------------------------------

type ClientHeaders = '[DescHeader "Location" "Client ID" ClientId]

type UserClientAPI =
  -- This endpoint can lead to the following events being sent:
  -- - ClientAdded event to self
  -- - ClientRemoved event to self, if removing old clients due to max number
  Named
    "add-client@v6"
    ( Summary "Register a new client"
        :> Until 'V7
        :> CanThrow 'TooManyClients
        :> CanThrow 'MissingAuth
        :> CanThrow 'MalformedPrekeys
        :> CanThrow 'CodeAuthenticationFailed
        :> CanThrow 'CodeAuthenticationRequired
        :> ZLocalUser
        :> ZConn
        :> "clients"
        :> VersionedReqBody 'V6 '[JSON] NewClient
        :> MultiVerb1
             'POST
             '[JSON]
             ( WithHeaders
                 ClientHeaders
                 Client
                 (VersionedRespond 'V6 201 "Client registered" Client)
             )
    )
    :<|> Named
           "add-client@v7"
           ( Summary "Register a new client"
               :> From 'V7
               :> Until 'V8
               :> CanThrow 'TooManyClients
               :> CanThrow 'MissingAuth
               :> CanThrow 'MalformedPrekeys
               :> CanThrow 'CodeAuthenticationFailed
               :> CanThrow 'CodeAuthenticationRequired
               :> ZLocalUser
               :> ZConn
               :> "clients"
               :> VersionedReqBody 'V7 '[JSON] NewClient
               :> MultiVerb1
                    'POST
                    '[JSON]
                    ( WithHeaders
                        ClientHeaders
                        Client
                        (VersionedRespond 'V7 201 "Client registered" Client)
                    )
           )
    :<|> Named
           "add-client"
           ( Summary "Register a new client"
               :> From 'V8
               :> CanThrow 'TooManyClients
               :> CanThrow 'MissingAuth
               :> CanThrow 'MalformedPrekeys
               :> CanThrow 'CodeAuthenticationFailed
               :> CanThrow 'CodeAuthenticationRequired
               :> ZLocalUser
               :> ZConn
               :> "clients"
               :> ReqBody '[JSON] NewClient
               :> MultiVerb1
                    'POST
                    '[JSON]
                    ( WithHeaders
                        ClientHeaders
                        Client
                        (Respond 201 "Client registered" Client)
                    )
           )
    :<|> Named
           "update-client@v6"
           ( Summary "Update a registered client"
               :> Until 'V7
               :> CanThrow 'MalformedPrekeys
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> VersionedReqBody 'V6 '[JSON] UpdateClient
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Client updated")
           )
    :<|> Named
           "update-client@v7"
           ( Summary "Update a registered client"
               :> From 'V7
               :> Until 'V8
               :> CanThrow 'MalformedPrekeys
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> VersionedReqBody 'V7 '[JSON] UpdateClient
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Client updated")
           )
    :<|> Named
           "update-client"
           ( Summary "Update a registered client"
               :> From 'V8
               :> CanThrow 'MalformedPrekeys
               :> CanThrow 'MLSDuplicatePublicKey
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> ReqBody '[JSON] UpdateClient
               :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Client updated")
           )
    :<|>
    -- This endpoint can lead to the following events being sent:
    -- - ClientRemoved event to self
    Named
      "delete-client"
      ( Summary "Delete an existing client"
          :> ZUser
          :> ZConn
          :> "clients"
          :> CaptureClientId "client"
          :> ReqBody '[JSON] RmClient
          :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Client deleted"] ()
      )
    :<|> Named
           "list-clients@v6"
           ( Summary "List the registered clients"
               :> Until 'V7
               :> ZUser
               :> "clients"
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (VersionedRespond 'V6 200 "List of clients" [Client])
           )
    :<|> Named
           "list-clients@v7"
           ( Summary "List the registered clients"
               :> From 'V7
               :> Until 'V8
               :> ZUser
               :> "clients"
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (VersionedRespond 'V7 200 "List of clients" [Client])
           )
    :<|> Named
           "list-clients"
           ( Summary "List the registered clients"
               :> From 'V8
               :> ZUser
               :> "clients"
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (Respond 200 "List of clients" [Client])
           )
    :<|> Named
           "get-client@v6"
           ( Summary "Get a registered client by ID"
               :> Until 'V7
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ EmptyErrorForLegacyReasons 404 "Client not found",
                       VersionedRespond 'V6 200 "Client found" Client
                     ]
                    (Maybe Client)
           )
    :<|> Named
           "get-client@v7"
           ( Summary "Get a registered client by ID"
               :> From 'V7
               :> Until 'V8
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ EmptyErrorForLegacyReasons 404 "Client not found",
                       VersionedRespond 'V7 200 "Client found" Client
                     ]
                    (Maybe Client)
           )
    :<|> Named
           "get-client"
           ( Summary "Get a registered client by ID"
               :> From 'V8
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ EmptyErrorForLegacyReasons 404 "Client not found",
                       Respond 200 "Client found" Client
                     ]
                    (Maybe Client)
           )
    :<|> Named
           "get-client-capabilities@v6"
           ( Summary "Read back what the client has been posting about itself"
               :> Until 'V7
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> "capabilities"
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (VersionedRespond 'V6 200 "capabilities" ClientCapabilityList)
           )
    :<|> Named
           "get-client-capabilities@v7"
           ( Summary "Read back what the client has been posting about itself"
               :> From 'V7
               :> Until 'V8
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> "capabilities"
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (VersionedRespond 'V7 200 "capabilities" ClientCapabilityList)
           )
    :<|> Named
           "get-client-capabilities"
           ( Summary "Read back what the client has been posting about itself"
               :> From 'V8
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> "capabilities"
               :> Get '[JSON] ClientCapabilityList
           )
    :<|> Named
           "get-client-prekeys"
           ( Summary "List the remaining prekey IDs of a client"
               :> ZUser
               :> "clients"
               :> CaptureClientId "client"
               :> "prekeys"
               :> Get '[JSON] [PrekeyId]
           )
    -- be aware that the order of the head-nonce and get-nonce matters, if get was first, then head requests would be routed to the get handler
    :<|> NewNonce "head-nonce" 'HEAD 200
    :<|> NewNonce "get-nonce" 'GET 204
    :<|> CreateAccessToken

type CreateAccessToken =
  Named
    "create-access-token"
    ( Summary "Create a JWT DPoP access token"
        :> Description
             ( "Create an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. \
               \The access token will be returned in the JSON response body as a JWT DPoP token."
             )
        :> ZLocalUser
        :> "clients"
        :> CaptureClientId "cid"
        :> "access-token"
        :> Header' '[Required, Strict] "DPoP" Proof
        :> MultiVerb1
             'POST
             '[JSON]
             ( WithHeaders
                 '[Header "Cache-Control" CacheControl]
                 (DPoPAccessTokenResponse, CacheControl)
                 (Respond 200 "Access token created" DPoPAccessTokenResponse)
             )
    )

type NewNonce name method statusCode =
  Named
    name
    ( Summary "Get a new nonce for a client CSR"
        :> Description "Get a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding."
        :> ZUser
        :> "clients"
        :> CaptureClientId "client"
        :> "nonce"
        :> MultiVerb1
             method
             '[JSON]
             ( WithHeaders
                 '[Header "Replay-Nonce" NonceHeader, Header "Cache-Control" CacheControl]
                 (Nonce, CacheControl)
                 (RespondEmpty statusCode "No Content")
             )
    )

newtype NonceHeader = NonceHeader Nonce
  deriving (Eq, Show)
  deriving newtype (FromByteString, ToByteString, ToParamSchema, ToHttpApiData, FromHttpApiData)

instance AsHeaders '[NonceHeader, CacheControl] () (Nonce, CacheControl) where
  fromHeaders (I (NonceHeader n) :* (I cc :* Nil), ()) = (n, cc)
  toHeaders (n, cc) = (I (NonceHeader n) :* (I cc :* Nil), ())

type ClientAPI =
  Named
    "get-user-clients-unqualified"
    ( Summary "Get all of a user's clients"
        :> Until 'V2
        :> "users"
        :> CaptureUserId "uid"
        :> "clients"
        :> Get '[JSON] [PubClient]
    )
    :<|> Named
           "get-user-clients-qualified"
           ( Summary "Get all of a user's clients"
               :> Description "This will go away in V16, please use <code>POST /users/list-clients</code> instead."
               :> Until 'V16
               :> "users"
               :> QualifiedCaptureUserId "uid"
               :> "clients"
               :> Get '[JSON] [PubClient]
           )
    :<|> Named
           "get-user-client-unqualified"
           ( Summary "Get a specific client of a user"
               :> Until 'V2
               :> "users"
               :> CaptureUserId "uid"
               :> "clients"
               :> CaptureClientId "client"
               :> Get '[JSON] PubClient
           )
    :<|> Named
           "get-user-client-qualified"
           ( Summary "Get a specific client of a user"
               :> Description "Hint: to list all clients of one or more users, use <code>POST /users/list-clients</code>."
               :> "users"
               :> QualifiedCaptureUserId "uid"
               :> "clients"
               :> CaptureClientId "client"
               :> Get '[JSON] PubClient
           )
    :<|> Named
           "list-clients-bulk"
           ( Summary "List all clients for a set of user ids"
               :> Until 'V2
               :> ZUser
               :> "users"
               :> "list-clients"
               :> ReqBody '[JSON] (Range 1 MaxUsersForListClientsBulk [Qualified UserId])
               :> Post '[JSON] (QualifiedUserMap (Set PubClient))
           )
    :<|> Named
           "list-clients-bulk-v2"
           ( Summary "List all clients for a set of user ids"
               :> Until 'V2
               :> ZUser
               :> "users"
               :> "list-clients"
               :> "v2"
               :> ReqBody '[JSON] (LimitedQualifiedUserIdList MaxUsersForListClientsBulk)
               :> Post '[JSON] (WrappedQualifiedUserMap (Set PubClient))
           )
    :<|> Named
           "list-clients-bulk@v2"
           ( Summary "List all clients for a set of user ids"
               :> Description "If a backend is unreachable, the clients from that backend will be omitted from the response"
               :> From 'V2
               :> ZUser
               :> "users"
               :> "list-clients"
               :> ReqBody '[JSON] (LimitedQualifiedUserIdList MaxUsersForListClientsBulk)
               :> Post '[JSON] (WrappedQualifiedUserMap (Set PubClient))
           )

-- Connection API -----------------------------------------------------
--
-- This endpoint can lead to the following events being sent:
-- - ConnectionUpdated event to self and other, if any side's connection state changes
-- - MemberJoin event to self and other, if joining an existing connect conversation (via galley)
-- - ConvCreate event to self, if creating a connect conversation (via galley)
-- - ConvConnect event to self, in some cases (via galley),
--   for details see 'Wire.ConversationSubsystem.Interpreter.createConnectConversationLogic'
type ConnectionAPI =
  Named
    "create-connection-unqualified"
    ( Summary "Create a connection to another user"
        :> Until 'V2
        :> CanThrow 'MissingLegalholdConsentOldClients
        :> CanThrow 'MissingLegalholdConsent
        :> CanThrow 'InvalidUser
        :> CanThrow 'ConnectionLimitReached
        :> CanThrow 'NoIdentity
        -- Config value 'setUserMaxConnections' value in production/by default
        -- is currently 1000 and has not changed in the last few years.
        -- While it would be more correct to use the config value here, that
        -- might not be time well spent.
        :> Description "You can have no more than 1000 connections in accepted or sent state"
        :> ZUser
        :> ZConn
        :> "connections"
        :> ReqBody '[JSON] ConnectionRequest
        :> MultiVerb
             'POST
             '[JSON]
             (ResponsesForExistedCreated "Connection existed" "Connection was created" UserConnection)
             (ResponseForExistedCreated UserConnection)
    )
    :<|> Named
           "create-connection"
           ( Summary "Create a connection to another user"
               :> CanThrow 'MissingLegalholdConsentOldClients
               :> CanThrow 'MissingLegalholdConsent
               :> CanThrow 'InvalidUser
               :> CanThrow 'ConnectionLimitReached
               :> CanThrow 'NoIdentity
               -- Config value 'setUserMaxConnections' value in production/by default
               -- is currently 1000 and has not changed in the last few years.
               -- While it would be more correct to use the config value here, that
               -- might not be time well spent.
               :> Description "You can have no more than 1000 connections in accepted or sent state"
               :> ZUser
               :> ZConn
               :> "connections"
               :> QualifiedCaptureUserId "uid"
               :> MultiVerb
                    'POST
                    '[JSON]
                    (ResponsesForExistedCreated "Connection existed" "Connection was created" UserConnection)
                    (ResponseForExistedCreated UserConnection)
           )
    :<|> Named
           "list-local-connections"
           ( Summary "List the local connections to other users"
               :> Until 'V2
               :> ZUser
               :> "connections"
               :> QueryParam' '[Optional, Strict, Description "User ID to start from when paginating"] "start" UserId
               :> QueryParam' '[Optional, Strict, Description "Number of results to return (default 100, max 500)"] "size" (Range 1 500 Int32)
               :> Get '[JSON] UserConnectionList
           )
    :<|> Named
           "list-connections"
           ( Summary "List the connections to other users, including remote users"
               :> Description PaginationDocs
               :> ZUser
               :> "list-connections"
               :> ReqBody '[JSON] ListConnectionsRequestPaginated
               :> Post '[JSON] ConnectionsPage
           )
    :<|> Named
           "get-connection-unqualified"
           ( Summary "Get an existing connection to another user"
               :> Until 'V2
               :> ZUser
               :> "connections"
               :> CaptureUserId "uid"
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ EmptyErrorForLegacyReasons 404 "Connection not found",
                       Respond 200 "Connection found" UserConnection
                     ]
                    (Maybe UserConnection)
           )
    :<|> Named
           "get-connection"
           ( Summary "Get an existing connection to another user (local or remote)"
               :> ZUser
               :> "connections"
               :> QualifiedCaptureUserId "uid"
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ EmptyErrorForLegacyReasons 404 "Connection not found",
                       Respond 200 "Connection found" UserConnection
                     ]
                    (Maybe UserConnection)
           )
    :<|>
    -- This endpoint can lead to the following events being sent:
    -- - ConnectionUpdated event to self and other, if their connection states change
    --
    -- When changing the connection state to Sent or Accepted, this can cause events to be sent
    -- when joining the connect conversation:
    -- - MemberJoin event to self and other (via galley)
    Named
      "update-connection-unqualified"
      ( Summary "Update a connection to another user"
          :> Until 'V2
          :> CanThrow 'MissingLegalholdConsentOldClients
          :> CanThrow 'MissingLegalholdConsent
          :> CanThrow 'InvalidUser
          :> CanThrow 'ConnectionLimitReached
          :> CanThrow 'NotConnected
          :> CanThrow 'InvalidTransition
          :> CanThrow 'NoIdentity
          :> ZUser
          :> ZConn
          :> "connections"
          :> CaptureUserId "uid"
          :> ReqBody '[JSON] ConnectionUpdate
          :> MultiVerb
               'PUT
               '[JSON]
               ConnectionUpdateResponses
               (UpdateResult UserConnection)
      )
    :<|>
    -- This endpoint can lead to the following events being sent:
    -- - ConnectionUpdated event to self and other, if their connection states change
    --
    -- When changing the connection state to Sent or Accepted, this can cause events to be sent
    -- when joining the connect conversation:
    -- - MemberJoin event to self and other (via galley)
    Named
      "update-connection"
      ( Summary "Update a connection to another user"
          :> CanThrow 'MissingLegalholdConsentOldClients
          :> CanThrow 'MissingLegalholdConsent
          :> CanThrow 'InvalidUser
          :> CanThrow 'ConnectionLimitReached
          :> CanThrow 'NotConnected
          :> CanThrow 'InvalidTransition
          :> CanThrow 'NoIdentity
          :> ZUser
          :> ZConn
          :> "connections"
          :> QualifiedCaptureUserId "uid"
          :> ReqBody '[JSON] ConnectionUpdate
          :> MultiVerb
               'PUT
               '[JSON]
               ConnectionUpdateResponses
               (UpdateResult UserConnection)
      )
    :<|> Named
           "search-contacts"
           ( Summary "Search for users"
               :> ZLocalUser
               :> CanThrow 'InsufficientPermissions
               :> "search"
               :> "contacts"
               :> QueryParam'
                    '[ Required,
                       Strict,
                       Description
                         "Search query\
                         \ <p>The search query is normalized: lower-cased and diacritics are removed ('Björn' becomes 'bjorn').\
                         \ The normalized search query matches accounts that, in this order of priority:</p>\
                         \ <ul>\
                         \ <li>are equal to the normalized full handle;\
                         \ <li>are equal to the normalized full user display name;\
                         \ <li>prefix-match the normalized handle;\
                         \ <li>prefix-match the normalized user display name.\
                         \ </ul>\
                         \ <p>NB: '@' Does NOT do anything special, ignoring user display names.</p>\
                         \ <p>See also: [authoritative ElasticSearch query](https://github.com/wireapp/wire-server/blob/83c25cca6a5e9d2205c102410b452eb78fc50a00/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs#L251-L288)</p>\
                         \"
                     ]
                    "q"
                    Text
               :> QueryParam' '[Optional, Strict, Description "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this."] "domain" Domain
               :> QueryParam' '[Optional, Strict, Description "Number of results to return (min: 1, max: 500, default 15)"] "size" (Range 1 500 Int32)
               :> QueryParam' '[Optional, Strict, Description "Only user types. Omitted or empty (type=) means no filtering."] "type" (CommaSeparatedList UserTypeFilter)
               :> Get '[Servant.JSON] (SearchResult Contact)
           )

-- Properties API -----------------------------------------------------

type PropertiesAPI =
  LiftNamed
    ( ZUser
        :> "properties"
        :> ( Named
               "set-property"
               -- This endpoint can lead to the following events being sent:
               -- - PropertySet event to self
               ( Summary "Set a user property"
                   :> ZConn
                   :> Capture "key" PropertyKey
                   :> ReqBody '[JSON] RawPropertyValue
                   :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Property set")
               )
               :<|>
               -- This endpoint can lead to the following events being sent:
               -- - PropertyDeleted event to self
               Named
                 "delete-property"
                 ( Summary "Delete a property"
                     :> ZConn
                     :> Capture "key" PropertyKey
                     :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "Property deleted")
                 )
               :<|>
               -- This endpoint can lead to the following events being sent:
               -- - PropertiesCleared event to self
               Named
                 "clear-properties"
                 ( Summary "Clear all properties"
                     :> ZConn
                     :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "Properties cleared")
                 )
               :<|> Named
                      "get-property"
                      ( Summary "Get a property value"
                          :> Capture "key" PropertyKey
                          :> MultiVerb
                               'GET
                               '[JSON]
                               '[ EmptyErrorForLegacyReasons 404 "Property not found",
                                  Respond 200 "The property value" RawPropertyValue
                                ]
                               (Maybe RawPropertyValue)
                      )
               :<|> Named
                      "list-property-keys"
                      ( Summary "List all property keys"
                          :> MultiVerb1 'GET '[JSON] (Respond 200 "List of property keys" [PropertyKey])
                      )
           )
    )
    :<|> Named
           "list-properties"
           ( Summary "List all properties with key and value"
               :> ZUser
               :> "properties-values"
               :> Get '[JSON] PropertyKeysAndValues
           )

-- MLS API ---------------------------------------------------------------------

type CipherSuiteParam =
  QueryParam'
    [ Required,
      Strict,
      Description "Ciphersuite in hex format (e.g. 0x0002)"
    ]
    "ciphersuite"
    CipherSuite

type CipherSuiteParamV7 =
  QueryParam'
    [ Optional,
      Strict,
      Description "Ciphersuite in hex format (e.g. 0x0002) - default is 0x0001"
    ]
    "ciphersuite"
    CipherSuite

type MultipleCipherSuitesParam =
  QueryParam'
    [ Required,
      Strict,
      Description "Comma-separated list of ciphersuites in hex format (e.g. 0x0002)"
    ]
    "ciphersuites"
    (CommaSeparatedList CipherSuite)

type MultipleCipherSuitesParamV7 =
  QueryParam'
    [ Optional,
      Strict,
      Description "Comma-separated list of ciphersuites in hex format (e.g. 0x0002) - default is 0x0001"
    ]
    "ciphersuites"
    (CommaSeparatedList CipherSuite)

type MLSKeyPackageAPI =
  "key-packages"
    :> ( Named
           "mls-key-packages-upload"
           ( "self"
               :> Summary "Upload a fresh batch of key packages"
               :> From 'V5
               :> Description "The request body should be a json object containing a list of base64-encoded key packages."
               :> ZLocalUser
               :> CanThrow 'MLSProtocolError
               :> CanThrow 'MLSIdentityMismatch
               :> CaptureClientId "client"
               :> ReqBody '[JSON] KeyPackageUpload
               :> MultiVerb 'POST '[JSON, MLS] '[RespondEmpty 201 "Key packages uploaded"] ()
           )
           :<|> Named
                  "mls-key-packages-replace@v7"
                  ( "self"
                      :> Summary "Upload a fresh batch of key packages and replace the old ones"
                      :> From 'V5
                      :> Until 'V8
                      :> Description "The request body should be a json object containing a list of base64-encoded key packages. Use this sparingly."
                      :> ZLocalUser
                      :> CanThrow 'MLSProtocolError
                      :> CanThrow 'MLSIdentityMismatch
                      :> CaptureClientId "client"
                      :> MultipleCipherSuitesParamV7
                      :> ReqBody '[JSON] KeyPackageUpload
                      :> MultiVerb 'PUT '[JSON, MLS] '[RespondEmpty 201 "Key packages replaced"] ()
                  )
           :<|> Named
                  "mls-key-packages-replace"
                  ( "self"
                      :> Summary "Upload a fresh batch of key packages and replace the old ones"
                      :> From 'V8
                      :> Description "The request body should be a json object containing a list of base64-encoded key packages. Use this sparingly."
                      :> ZLocalUser
                      :> CanThrow 'MLSProtocolError
                      :> CanThrow 'MLSIdentityMismatch
                      :> CaptureClientId "client"
                      :> MultipleCipherSuitesParam
                      :> ReqBody '[JSON] KeyPackageUpload
                      :> MultiVerb 'PUT '[JSON, MLS] '[RespondEmpty 201 "Key packages replaced"] ()
                  )
           :<|> Named
                  "mls-key-packages-claim@v7"
                  ( "claim"
                      :> Summary "Claim one key package for each client of the given user"
                      :> From 'V5
                      :> Until 'V8
                      :> Description "Only key packages for the specified ciphersuite are claimed. For backwards compatibility, the `ciphersuite` parameter is optional, defaulting to ciphersuite 0x0001 when omitted."
                      :> ZLocalUser
                      :> ZOptClient
                      :> QualifiedCaptureUserId "user"
                      :> CipherSuiteParamV7
                      :> MultiVerb1 'POST '[JSON] (Respond 200 "Claimed key packages" KeyPackageBundle)
                  )
           :<|> Named
                  "mls-key-packages-claim"
                  ( "claim"
                      :> Summary "Claim one key package for each client of the given user"
                      :> From 'V8
                      :> Description "Only key packages for the specified ciphersuite are claimed."
                      :> ZLocalUser
                      :> ZOptClient
                      :> QualifiedCaptureUserId "user"
                      :> CipherSuiteParam
                      :> MultiVerb1 'POST '[JSON] (Respond 200 "Claimed key packages" KeyPackageBundle)
                  )
           :<|> Named
                  "mls-key-packages-count@v7"
                  ( "self"
                      :> Summary "Return the number of unclaimed key packages for a given ciphersuite and client"
                      :> From 'V5
                      :> Until 'V8
                      :> ZLocalUser
                      :> CaptureClientId "client"
                      :> "count"
                      :> CipherSuiteParamV7
                      :> MultiVerb1 'GET '[JSON] (Respond 200 "Number of key packages" KeyPackageCount)
                  )
           :<|> Named
                  "mls-key-packages-count"
                  ( "self"
                      :> Summary "Return the number of unclaimed key packages for a given ciphersuite and client"
                      :> From 'V8
                      :> ZLocalUser
                      :> CaptureClientId "client"
                      :> "count"
                      :> CipherSuiteParam
                      :> MultiVerb1 'GET '[JSON] (Respond 200 "Number of key packages" KeyPackageCount)
                  )
           :<|> Named
                  "mls-key-packages-delete@v7"
                  ( "self"
                      :> From 'V5
                      :> Until 'V8
                      :> ZLocalUser
                      :> CaptureClientId "client"
                      :> Summary "Delete all key packages for a given ciphersuite and client"
                      :> CipherSuiteParamV7
                      :> ReqBody '[JSON] DeleteKeyPackages
                      :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 201 "OK")
                  )
           :<|> Named
                  "mls-key-packages-delete"
                  ( "self"
                      :> From 'V8
                      :> ZLocalUser
                      :> CaptureClientId "client"
                      :> Summary "Delete all key packages for a given ciphersuite and client"
                      :> CipherSuiteParam
                      :> ReqBody '[JSON] DeleteKeyPackages
                      :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 201 "OK")
                  )
       )

type MLSAPI = LiftNamed ("mls" :> MLSKeyPackageAPI)

-- Search API -----------------------------------------------------

type SearchAPI =
  Named
    "browse-team"
    ( Summary "Browse team for members (requires add-user permission)"
        :> ZUser
        :> "teams"
        :> Capture "tid" TeamId
        :> "search"
        :> QueryParam'
             [ Optional,
               Strict,
               Description "Search expression"
             ]
             "q"
             Text
        :> QueryParam'
             [ Optional,
               Strict,
               Description "Role filter, eg. `member,partner`.  Empty list means do not filter."
             ]
             "frole"
             RoleFilter
        :> QueryParam'
             [ Optional,
               Strict,
               Description "Can be one of name, handle, email, saml_idp, managed_by, role, created_at."
             ]
             "sortby"
             TeamUserSearchSortBy
        :> QueryParam'
             [ Optional,
               Strict,
               Description "Can be one of asc, desc."
             ]
             "sortorder"
             TeamUserSearchSortOrder
        :> QueryParam'
             [ Optional,
               Strict,
               Description "Number of results to return (min: 1, max: 500, default: 15)"
             ]
             "size"
             (Range 1 500 Int)
        :> QueryParam'
             [ Optional,
               Strict,
               Description
                 "Optional, when not specified, the first page will be returned. \
                 \Every returned page contains a `paging_state`, this should be supplied to retrieve the next page."
             ]
             "pagingState"
             PagingState
        :> QueryParam'
             [ Optional,
               Strict,
               Description
                 "Filter for (un-)verified email"
             ]
             "email"
             EmailVerificationFilter
        :> QueryParam'
             [ Optional,
               Strict,
               Description "Optional, return only non-searchable members when false."
             ]
             "searchable"
             Bool
        :> MultiVerb
             'GET
             '[JSON]
             '[Respond 200 "Search results" (SearchResult TeamContact)]
             (SearchResult TeamContact)
    )

type AuthAPI =
  Named
    "access"
    ( "access"
        :> Summary "Obtain an access tokens for a cookie"
        :> Description
             "You can provide only a cookie or a cookie and token.\
             \ Every other combination is invalid.\
             \ Access tokens can be given as query parameter or authorisation\
             \ header, with the latter being preferred."
        :> QueryParam "client_id" ClientId
        :> Cookies '["zuid" ::: SomeUserToken]
        :> Bearer SomeAccessToken
        :> CanThrow 'BadCredentials
        :> MultiVerb1 'POST '[JSON] TokenResponse
    )
    :<|> Named
           "send-login-code"
           ( "login"
               :> "send"
               :> Until 'V6
               :> Summary "Send a login code to a verified phone number"
               :> Description
                    "This operation generates and sends a login code via sms for phone login.\
                    \ A login code can be used only once and times out after\
                    \ 10 minutes. Only one login code may be pending at a time.\
                    \ For 2nd factor authentication login with email and password, use the\
                    \ `/verification-code/send` endpoint."
               :> ReqBody '[JSON] SendLoginCode
               :> CanThrow 'InvalidPhone
               :> CanThrow 'PasswordExists
               :> MultiVerb1
                    'POST
                    '[JSON]
                    (Respond 200 "OK" LoginCodeTimeout)
           )
    :<|> Named
           "login"
           ( "login"
               :> Summary "Authenticate a user to obtain a cookie and first access token"
               :> Description "Logins are throttled at the server's discretion"
               :> ReqBody '[JSON] Login
               :> QueryParam'
                    [ Optional,
                      Strict,
                      Description "Request a persistent cookie instead of a session cookie"
                    ]
                    "persist"
                    Bool
               :> CanThrow 'BadCredentials
               :> CanThrow 'AccountSuspended
               :> CanThrow 'AccountPending
               :> CanThrow 'CodeAuthenticationFailed
               :> CanThrow 'CodeAuthenticationRequired
               :> MultiVerb1 'POST '[JSON] TokenResponse
           )
    :<|> Named
           "logout"
           ( "access"
               :> "logout"
               :> Summary "Log out in order to remove a cookie from the server"
               :> Description
                    "Calling this endpoint will effectively revoke the given cookie\
                    \ and subsequent calls to /access with the same cookie will\
                    \ result in a 403."
               :> Cookies '["zuid" ::: SomeUserToken]
               :> Bearer SomeAccessToken
               :> CanThrow 'BadCredentials
               :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "Logout")
           )
    :<|> Named
           "change-self-email"
           ( "access"
               :> "self"
               :> "email"
               :> Summary "Change your email address"
               :> Cookies '["zuid" ::: SomeUserToken]
               :> Bearer SomeAccessToken
               :> ReqBody '[JSON] EmailUpdate
               :> CanThrow 'InvalidEmail
               :> CanThrow 'UserKeyExists
               :> CanThrow 'BlacklistedEmail
               :> CanThrow 'BadCredentials
               :> MultiVerb
                    'PUT
                    '[JSON]
                    '[ Respond 202 "Update accepted and pending activation of the new email" (),
                       Respond 204 "No update, current and new email address are the same" (),
                       Respond 204 "Email address activated" ()
                     ]
                    ChangeEmailResponse
           )
    :<|> Named
           "list-cookies"
           ( "cookies"
               :> Summary "Retrieve the list of cookies currently stored for the user"
               :> ZLocalUser
               :> QueryParam'
                    [Optional, Strict, Description "Filter by label (comma-separated list)"]
                    "labels"
                    (CommaSeparatedList CookieLabel)
               :> MultiVerb1 'GET '[JSON] (Respond 200 "List of cookies" CookieList)
           )
    :<|> Named
           "remove-cookies"
           ( "cookies"
               :> "remove"
               :> Summary "Revoke stored cookies"
               :> ZLocalUser
               :> CanThrow 'BadCredentials
               :> ReqBody '[JSON] RemoveCookies
               :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "Cookies revoked")
           )

-------------------------------------------------------------------------------
-- Calling API

type CallingAPI =
  -- Deprecated endpoint, but still used by old clients.
  -- See https://github.com/zinfra/backend-issues/issues/1616 for context
  Named
    "get-calls-config"
    ( Summary
        "Retrieve TURN server addresses and credentials for \
        \ IP addresses, scheme `turn` and transport `udp` only (deprecated)"
        :> Deprecated
        :> Until 'V8
        :> ZUser
        :> ZConn
        :> "calls"
        :> "config"
        :> Get '[JSON] RTCConfiguration
    )
    :<|> Named
           "get-calls-config-v2"
           ( Summary
               "Retrieve all TURN server addresses and credentials. \
               \Clients are expected to do a DNS lookup to resolve \
               \the IP addresses of the given hostnames "
               :> ZUser
               :> ZConn
               :> "calls"
               :> "config"
               :> "v2"
               :> QueryParam' '[Optional, Strict, Description "Limit resulting list. Allowed values [1..10]"] "limit" (Range 1 10 Int)
               :> Get '[JSON] RTCConfiguration
           )

-- Teams API -----------------------------------------------------

type TeamsAPI =
  Named
    "send-team-invitation@v6"
    ( Summary "Create and send a new team invitation."
        :> Until V7
        :> Description
             "Invitations are sent by email. The maximum allowed number of \
             \pending team invitations is equal to the team size."
        :> CanThrow 'NoEmail
        :> CanThrow 'NoIdentity
        :> CanThrow 'InvalidEmail
        :> CanThrow 'BlacklistedEmail
        :> CanThrow 'TooManyTeamInvitations
        :> CanThrow 'InsufficientTeamPermissions
        :> CanThrow 'InvalidInvitationCode
        :> ZLocalUser
        :> "teams"
        :> Capture "tid" TeamId
        :> "invitations"
        :> VersionedReqBody V6 '[JSON] InvitationRequest
        :> MultiVerb1
             'POST
             '[JSON]
             ( WithHeaders
                 '[Header "Location" InvitationLocation]
                 (Invitation, InvitationLocation)
                 (Respond 201 "Invitation was created and sent." Invitation)
             )
    )
    :<|> Named
           "send-team-invitation"
           ( Summary "Create and send a new team invitation."
               :> From V7
               :> Description
                    "Invitations are sent by email. The maximum allowed number of \
                    \pending team invitations is equal to the team size."
               :> CanThrow 'NoEmail
               :> CanThrow 'NoIdentity
               :> CanThrow 'InvalidEmail
               :> CanThrow 'BlacklistedEmail
               :> CanThrow 'TooManyTeamInvitations
               :> CanThrow 'InsufficientTeamPermissions
               :> CanThrow 'InvalidInvitationCode
               :> ZLocalUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "invitations"
               :> ReqBody '[JSON] InvitationRequest
               :> MultiVerb1
                    'POST
                    '[JSON]
                    ( WithHeaders
                        '[Header "Location" InvitationLocation]
                        (Invitation, InvitationLocation)
                        (Respond 201 "Invitation was created and sent." Invitation)
                    )
           )
    :<|> Named
           "get-team-invitations"
           ( Summary "List the sent team invitations"
               :> CanThrow 'InsufficientTeamPermissions
               :> ZUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "invitations"
               :> QueryParam' '[Optional, Strict, Description "Invitation id to start from (ascending)."] "start" InvitationId
               :> QueryParam' '[Optional, Strict, Description "Number of results to return (default 100, max 500)."] "size" (Range 1 500 Int32)
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (Respond 200 "List of sent invitations" InvitationList)
           )
    :<|> Named
           "get-team-invitation"
           ( Summary "Get a pending team invitation by ID."
               :> CanThrow 'InsufficientTeamPermissions
               :> CanThrow 'DuplicateEntry
               :> ZUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "invitations"
               :> Capture "iid" InvitationId
               :> MultiVerb
                    'GET
                    '[JSON]
                    '[ ErrorResponse 'NotificationNotFound,
                       Respond 200 "Invitation" Invitation
                     ]
                    (Maybe Invitation)
           )
    :<|> Named
           "delete-team-invitation"
           ( Summary "Delete a pending team invitation by ID."
               :> CanThrow 'InsufficientTeamPermissions
               :> ZUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "invitations"
               :> Capture "iid" InvitationId
               :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "Invitation deleted")
           )
    :<|> Named
           "get-team-invitation-info"
           ( Summary "Get invitation info given a code."
               :> CanThrow 'InvalidInvitationCode
               :> "teams"
               :> "invitations"
               :> "info"
               :> QueryParam' '[Required, Strict, Description "Invitation code"] "code" InvitationCode
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (Respond 200 "Invitation info" InvitationUserView)
           )
    -- FUTUREWORK: Add another endpoint to allow resending of invitation codes
    :<|> Named
           "head-team-invitations"
           ( Summary "Check if there is an invitation pending given an email address."
               :> "teams"
               :> "invitations"
               :> "by-email"
               :> QueryParam' '[Required, Strict, Description "Email address"] "email" EmailAddress
               :> MultiVerb
                    'HEAD
                    '[JSON]
                    HeadInvitationsResponses
                    HeadInvitationByEmailResult
           )
    :<|> Named
           "get-team-size"
           ( Summary "Get the number of team members as an integer"
               :> Description
                    "Can be out of sync by roughly the `refresh_interval` \
                    \of the ES index."
               :> CanThrow 'InvalidInvitationCode
               :> ZUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "size"
               :> MultiVerb1
                    'GET
                    '[JSON]
                    (Respond 200 "Number of team members" TeamSize)
           )
    :<|> Named
           "accept-team-invitation"
           ( Summary "Accept a team invitation, changing a personal account into a team member account."
               :> CanThrow 'PendingInvitationNotFound
               :> CanThrow 'TooManyTeamMembers
               :> CanThrow 'MissingIdentity
               :> CanThrow 'InvalidActivationCodeWrongUser
               :> CanThrow 'InvalidActivationCodeWrongCode
               :> CanThrow 'BadCredentials
               :> CanThrow 'MissingAuth
               :> ZLocalUser
               :> "teams"
               :> "invitations"
               :> "accept"
               :> ReqBody '[JSON] AcceptTeamInvitation
               :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Team invitation accepted."] ()
           )

type SystemSettingsAPI =
  Named
    "get-system-settings-unauthorized"
    ( Summary "Returns a curated set of system configuration settings."
        :> From 'V3
        :> "system"
        :> "settings"
        :> "unauthorized"
        :> Get '[JSON] SystemSettingsPublic
    )
    :<|> Named
           "get-system-settings"
           ( Summary "Returns a curated set of system configuration settings for authorized users."
               :> From 'V4
               :> ZUser
               :> "system"
               :> "settings"
               :> Get '[JSON] SystemSettings
           )

type AppsAPI =
  Named
    "create-app"
    ( Summary "Create a new app"
        :> From 'V12
        :> ZLocalUser
        :> "teams"
        :> Capture "tid" TeamId
        :> "apps"
        :> ReqBody '[JSON] NewApp
        :> Post '[JSON] CreatedApp
    )
    :<|> Named
           "get-app"
           ( Summary "Get app"
               :> From 'V14
               :> Until 'V16
               :> ZLocalUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "apps"
               :> Capture "uid" UserId
               :> Get '[JSON] UserProfile
           )
    :<|> Named
           "get-apps"
           ( Summary "Get all apps owned by the given team (not including collaborators)"
               :> From 'V15
               :> ZLocalUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "apps"
               :> Get '[JSON] [UserProfile]
           )
    :<|> Named
           "put-app"
           ( Summary "Update metadata of an existing app"
               :> From 'V15
               :> ZLocalUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "apps"
               :> Capture "app" UserId
               :> ReqBody '[JSON] PutApp
               :> Put '[JSON] ()
           )
    :<|> Named
           "refresh-app-cookie"
           ( Summary "Get a new app authentication token"
               :> From 'V12
               :> CanThrow 'ErrorBrig.MissingAuth
               :> ZLocalUser
               :> "teams"
               :> Capture "tid" TeamId
               :> "apps"
               :> Capture "app" UserId
               :> "cookies"
               :> ReqBody '[JSON] RefreshAppCookieRequest
               :> Post '[JSON] RefreshAppCookieResponse
           )
