Serve the HTTP API from /v1/team instead of /api/v4

The product namespace is team, and /api/ is not used as a path prefix
here — API hosts carry the api.* name and paths carry the version. This
moves the one constant every route mounts on, so the public router, the
local router and the websocket endpoint all follow it.

The two 404 catch-alls had the old prefix written out by hand, which
meant the constant was not actually the single source of truth; they now
derive from it, as do the emoji, image-proxy, remote-cluster and
self-ping URLs that were each carrying their own copy of the literal.

The remote-cluster path constants gain a leading slash from the shared
constant, so the two call sites that joined them with an explicit
separator would have produced a double slash and are adjusted.

APIURLSuffixV4 was a bare alias of the value and APIURLSuffixV5 mounted
a subrouter that no route ever registered on, so both are gone rather
than carried forward under names that no longer describe anything.
APIURLSuffixV1 is not ours at all — it addresses the external push
notification service — so it keeps its value and is renamed
PushURLSuffix to stop it reading as a version of this API.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-14 10:58:01 -07:00
co-authored by Hanzo Dev
parent 0eb27d51a9
commit c4c2c619ab
50 changed files with 336 additions and 333 deletions
+2 -2
View File
@@ -834,7 +834,7 @@ func searchAccessControlPolicies(c *Context, w http.ResponseWriter, r *http.Requ
// updateActiveStatus updates the active status of a single access control policy.
//
// Deprecated: This endpoint is deprecated and will be removed in a future release.
// Use PUT /api/v4/access_control/policies/activate instead, which supports batch updates.
// Use PUT /v1/team/access_control/policies/activate instead, which supports batch updates.
func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
if c.Err != nil {
@@ -896,7 +896,7 @@ func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) {
// Set deprecation header to inform clients
w.Header().Set("Deprecation", "true")
w.Header().Set("Link", "</api/v4/access_control/policies/activate>; rel=\"successor-version\"")
w.Header().Set("Link", "<"+model.APIURLSuffix+"/access_control/policies/activate>; rel=\"successor-version\"")
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
+3 -3
View File
@@ -12,11 +12,11 @@ import (
)
func (api *API) InitAgents() {
// GET /api/v4/agents
// GET /v1/team/agents
api.BaseRoutes.Agents.Handle("", api.APISessionRequired(getAgents)).Methods(http.MethodGet)
// GET /api/v4/agents/status
// GET /v1/team/agents/status
api.BaseRoutes.Agents.Handle("/status", api.APISessionRequired(getAgentsStatus)).Methods(http.MethodGet)
// GET /api/v4/llmservices
// GET /v1/team/llmservices
api.BaseRoutes.LLMServices.Handle("", api.APISessionRequired(getLLMServices)).Methods(http.MethodGet)
}
+114 -116
View File
@@ -16,166 +16,165 @@ import (
)
type Routes struct {
Root *mux.Router // ''
APIRoot *mux.Router // 'api/v4'
APIRoot5 *mux.Router // 'api/v5'
Root *mux.Router // ''
APIRoot *mux.Router // 'v1/team'
Users *mux.Router // 'api/v4/users'
User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
UserByUsername *mux.Router // 'api/v4/users/username/{username:[A-Za-z0-9\\_\\-\\.]+}'
UserByEmail *mux.Router // 'api/v4/users/email/{email:.+}'
Users *mux.Router // 'v1/team/users'
User *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}'
UserByUsername *mux.Router // 'v1/team/users/username/{username:[A-Za-z0-9\\_\\-\\.]+}'
UserByEmail *mux.Router // 'v1/team/users/email/{email:.+}'
Bots *mux.Router // 'api/v4/bots'
Bot *mux.Router // 'api/v4/bots/{bot_user_id:[A-Za-z0-9]+}'
Bots *mux.Router // 'v1/team/bots'
Bot *mux.Router // 'v1/team/bots/{bot_user_id:[A-Za-z0-9]+}'
Teams *mux.Router // 'api/v4/teams'
TeamsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams'
Team *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}'
TeamForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}'
UserThreads *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads'
UserThread *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads/{thread_id:[A-Za-z0-9]+}'
TeamByName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}'
TeamMembers *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members'
TeamMember *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
TeamMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/members'
Teams *mux.Router // 'v1/team/teams'
TeamsForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/teams'
Team *mux.Router // 'v1/team/teams/{team_id:[A-Za-z0-9]+}'
TeamForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}'
UserThreads *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads'
UserThread *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads/{thread_id:[A-Za-z0-9]+}'
TeamByName *mux.Router // 'v1/team/teams/name/{team_name:[A-Za-z0-9_-]+}'
TeamMembers *mux.Router // 'v1/team/teams/{team_id:[A-Za-z0-9]+}/members'
TeamMember *mux.Router // 'v1/team/teams/{team_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
TeamMembersForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/teams/members'
Channels *mux.Router // 'api/v4/channels'
Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}'
ChannelForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelByNameForTeamName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels'
ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members'
ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members'
ChannelModerations *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/moderations'
ChannelCategories *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/categories'
ChannelBookmarks *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/bookmarks'
ChannelBookmark *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/bookmarks/{bookmark_id:[A-Za-z0-9]+}'
ChannelViews *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/views'
ChannelView *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/views/{view_id:[A-Za-z0-9]+}'
ChannelViewPosts *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/views/{view_id:[A-Za-z0-9]+}/posts'
Channels *mux.Router // 'v1/team/channels'
Channel *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}'
ChannelForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
ChannelByName *mux.Router // 'v1/team/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelByNameForTeamName *mux.Router // 'v1/team/teams/name/{team_name:[A-Za-z0-9_-]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelsForTeam *mux.Router // 'v1/team/teams/{team_id:[A-Za-z0-9]+}/channels'
ChannelMembers *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/members'
ChannelMember *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
ChannelMembersForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members'
ChannelModerations *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/moderations'
ChannelCategories *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/categories'
ChannelBookmarks *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/bookmarks'
ChannelBookmark *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/bookmarks/{bookmark_id:[A-Za-z0-9]+}'
ChannelViews *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/views'
ChannelView *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/views/{view_id:[A-Za-z0-9]+}'
ChannelViewPosts *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/views/{view_id:[A-Za-z0-9]+}/posts'
Posts *mux.Router // 'api/v4/posts'
Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts'
PostsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts'
PostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}'
Posts *mux.Router // 'v1/team/posts'
Post *mux.Router // 'v1/team/posts/{post_id:[A-Za-z0-9]+}'
PostsForChannel *mux.Router // 'v1/team/channels/{channel_id:[A-Za-z0-9]+}/posts'
PostsForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/posts'
PostForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}'
Files *mux.Router // 'api/v4/files'
File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
Files *mux.Router // 'v1/team/files'
File *mux.Router // 'v1/team/files/{file_id:[A-Za-z0-9]+}'
Uploads *mux.Router // 'api/v4/uploads'
Upload *mux.Router // 'api/v4/uploads/{upload_id:[A-Za-z0-9]+}'
Uploads *mux.Router // 'v1/team/uploads'
Upload *mux.Router // 'v1/team/uploads/{upload_id:[A-Za-z0-9]+}'
Plugins *mux.Router // 'api/v4/plugins'
Plugin *mux.Router // 'api/v4/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}'
Plugins *mux.Router // 'v1/team/plugins'
Plugin *mux.Router // 'v1/team/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}'
PublicFile *mux.Router // '/files/{file_id:[A-Za-z0-9]+}/public'
Commands *mux.Router // 'api/v4/commands'
Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}'
Commands *mux.Router // 'v1/team/commands'
Command *mux.Router // 'v1/team/commands/{command_id:[A-Za-z0-9]+}'
Hooks *mux.Router // 'api/v4/hooks'
IncomingHooks *mux.Router // 'api/v4/hooks/incoming'
IncomingHook *mux.Router // 'api/v4/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
OutgoingHooks *mux.Router // 'api/v4/hooks/outgoing'
OutgoingHook *mux.Router // 'api/v4/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
Hooks *mux.Router // 'v1/team/hooks'
IncomingHooks *mux.Router // 'v1/team/hooks/incoming'
IncomingHook *mux.Router // 'v1/team/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
OutgoingHooks *mux.Router // 'v1/team/hooks/outgoing'
OutgoingHook *mux.Router // 'v1/team/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
OAuth *mux.Router // 'api/v4/oauth'
OAuthApps *mux.Router // 'api/v4/oauth/apps'
OAuthApp *mux.Router // 'api/v4/oauth/apps/{app_id:[A-Za-z0-9]+}'
OAuth *mux.Router // 'v1/team/oauth'
OAuthApps *mux.Router // 'v1/team/oauth/apps'
OAuthApp *mux.Router // 'v1/team/oauth/apps/{app_id:[A-Za-z0-9]+}'
SAML *mux.Router // 'api/v4/saml'
Compliance *mux.Router // 'api/v4/compliance'
Cluster *mux.Router // 'api/v4/cluster'
SAML *mux.Router // 'v1/team/saml'
Compliance *mux.Router // 'v1/team/compliance'
Cluster *mux.Router // 'v1/team/cluster'
Image *mux.Router // 'api/v4/image'
Image *mux.Router // 'v1/team/image'
LDAP *mux.Router // 'api/v4/ldap'
LDAP *mux.Router // 'v1/team/ldap'
Elasticsearch *mux.Router // 'api/v4/elasticsearch'
Elasticsearch *mux.Router // 'v1/team/elasticsearch'
DataRetention *mux.Router // 'api/v4/data_retention'
DataRetention *mux.Router // 'v1/team/data_retention'
Brand *mux.Router // 'api/v4/brand'
Brand *mux.Router // 'v1/team/brand'
System *mux.Router // 'api/v4/system'
System *mux.Router // 'v1/team/system'
Jobs *mux.Router // 'api/v4/jobs'
Jobs *mux.Router // 'v1/team/jobs'
Recaps *mux.Router // 'api/v4/recaps'
Recaps *mux.Router // 'v1/team/recaps'
ScheduledRecaps *mux.Router // 'api/v4/scheduled_recaps'
ScheduledRecap *mux.Router // 'api/v4/scheduled_recaps/{scheduled_recap_id:[A-Za-z0-9]+}'
ScheduledRecaps *mux.Router // 'v1/team/scheduled_recaps'
ScheduledRecap *mux.Router // 'v1/team/scheduled_recaps/{scheduled_recap_id:[A-Za-z0-9]+}'
Preferences *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/preferences'
Preferences *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/preferences'
License *mux.Router // 'api/v4/license'
License *mux.Router // 'v1/team/license'
Public *mux.Router // 'api/v4/public'
Public *mux.Router // 'v1/team/public'
Reactions *mux.Router // 'api/v4/reactions'
Reactions *mux.Router // 'v1/team/reactions'
Roles *mux.Router // 'api/v4/roles'
Schemes *mux.Router // 'api/v4/schemes'
Roles *mux.Router // 'v1/team/roles'
Schemes *mux.Router // 'v1/team/schemes'
Emojis *mux.Router // 'api/v4/emoji'
Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
EmojiByName *mux.Router // 'api/v4/emoji/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
Emojis *mux.Router // 'v1/team/emoji'
Emoji *mux.Router // 'v1/team/emoji/{emoji_id:[A-Za-z0-9]+}'
EmojiByName *mux.Router // 'v1/team/emoji/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
ReactionByNameForPostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
ReactionByNameForPostForUser *mux.Router // 'v1/team/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
TermsOfService *mux.Router // 'api/v4/terms_of_service'
Groups *mux.Router // 'api/v4/groups'
TermsOfService *mux.Router // 'v1/team/terms_of_service'
Groups *mux.Router // 'v1/team/groups'
Imports *mux.Router // 'api/v4/imports'
Import *mux.Router // 'api/v4/imports/{import_name:.+\\.zip}'
Imports *mux.Router // 'v1/team/imports'
Import *mux.Router // 'v1/team/imports/{import_name:.+\\.zip}'
Exports *mux.Router // 'api/v4/exports'
Export *mux.Router // 'api/v4/exports/{export_name:.+\\.zip}'
Exports *mux.Router // 'v1/team/exports'
Export *mux.Router // 'v1/team/exports/{export_name:.+\\.zip}'
RemoteCluster *mux.Router // 'api/v4/remotecluster'
SharedChannels *mux.Router // 'api/v4/sharedchannels'
ChannelForRemote *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
SharedChannelRemotes *mux.Router // 'api/v4/remotecluster/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes'
RemoteCluster *mux.Router // 'v1/team/remotecluster'
SharedChannels *mux.Router // 'v1/team/sharedchannels'
ChannelForRemote *mux.Router // 'v1/team/remotecluster/{remote_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
SharedChannelRemotes *mux.Router // 'v1/team/remotecluster/{remote_id:[A-Za-z0-9]+}/sharedchannelremotes'
Permissions *mux.Router // 'api/v4/permissions'
Permissions *mux.Router // 'v1/team/permissions'
Usage *mux.Router // 'api/v4/usage'
Usage *mux.Router // 'v1/team/usage'
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
HostedCustomer *mux.Router // 'v1/team/hosted_customer'
Drafts *mux.Router // 'api/v4/drafts'
Drafts *mux.Router // 'v1/team/drafts'
Reports *mux.Router // 'api/v4/reports'
Reports *mux.Router // 'v1/team/reports'
OutgoingOAuthConnections *mux.Router // 'api/v4/oauth/outgoing_connections'
OutgoingOAuthConnection *mux.Router // 'api/v4/oauth/outgoing_connections/{outgoing_oauth_connection_id:[A-Za-z0-9]+}'
OutgoingOAuthConnections *mux.Router // 'v1/team/oauth/outgoing_connections'
OutgoingOAuthConnection *mux.Router // 'v1/team/oauth/outgoing_connections/{outgoing_oauth_connection_id:[A-Za-z0-9]+}'
CustomProfileAttributes *mux.Router // 'api/v4/custom_profile_attributes'
CustomProfileAttributesFields *mux.Router // 'api/v4/custom_profile_attributes/fields'
CustomProfileAttributesField *mux.Router // 'api/v4/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
CustomProfileAttributes *mux.Router // 'v1/team/custom_profile_attributes'
CustomProfileAttributesFields *mux.Router // 'v1/team/custom_profile_attributes/fields'
CustomProfileAttributesField *mux.Router // 'v1/team/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
CustomProfileAttributesValues *mux.Router // 'v1/team/custom_profile_attributes/values'
AuditLogs *mux.Router // 'api/v4/audit_logs'
AuditLogs *mux.Router // 'v1/team/audit_logs'
AccessControlPolicies *mux.Router // 'api/v4/access_control_policies'
AccessControlPolicy *mux.Router // 'api/v4/access_control_policies/{policy_id:[A-Za-z0-9]+}'
AccessControlPolicies *mux.Router // 'v1/team/access_control_policies'
AccessControlPolicy *mux.Router // 'v1/team/access_control_policies/{policy_id:[A-Za-z0-9]+}'
ContentFlagging *mux.Router // 'api/v4/content_flagging'
ContentFlagging *mux.Router // 'v1/team/content_flagging'
Agents *mux.Router // 'api/v4/agents'
LLMServices *mux.Router // 'api/v4/llmservices'
Agents *mux.Router // 'v1/team/agents'
LLMServices *mux.Router // 'v1/team/llmservices'
Boards *mux.Router // 'api/v4/boards'
Boards *mux.Router // 'v1/team/boards'
Properties *mux.Router // 'api/v4/properties'
PropertyFields *mux.Router // 'api/v4/properties/groups/{group_name:[a-z][a-z0-9_]*}/{object_type:[a-z]+}/fields'
PropertyField *mux.Router // 'api/v4/properties/groups/{group_name:[a-z][a-z0-9_]*}/{object_type:[a-z]+}/fields/{field_id:[A-Za-z0-9]+}'
PropertyFieldsSearch *mux.Router // 'api/v4/properties/groups/{group_name:[a-z][a-z0-9_]*}/fields/search'
PropertyValues *mux.Router // 'api/v4/properties/groups/{group_name:[a-z][a-z0-9_]*}/{object_type:[a-z]+}/values/{target_id:[A-Za-z0-9]+}'
PropertySystemValues *mux.Router // 'api/v4/properties/groups/{group_name:[a-z][a-z0-9_]*}/system/values'
Properties *mux.Router // 'v1/team/properties'
PropertyFields *mux.Router // 'v1/team/properties/groups/{group_name:[a-z][a-z0-9_]*}/{object_type:[a-z]+}/fields'
PropertyField *mux.Router // 'v1/team/properties/groups/{group_name:[a-z][a-z0-9_]*}/{object_type:[a-z]+}/fields/{field_id:[A-Za-z0-9]+}'
PropertyFieldsSearch *mux.Router // 'v1/team/properties/groups/{group_name:[a-z][a-z0-9_]*}/fields/search'
PropertyValues *mux.Router // 'v1/team/properties/groups/{group_name:[a-z][a-z0-9_]*}/{object_type:[a-z]+}/values/{target_id:[A-Za-z0-9]+}'
PropertySystemValues *mux.Router // 'v1/team/properties/groups/{group_name:[a-z][a-z0-9_]*}/system/values'
}
type API struct {
@@ -191,7 +190,6 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.Root = srv.Router
api.BaseRoutes.APIRoot = srv.Router.PathPrefix(model.APIURLSuffix).Subrouter()
api.BaseRoutes.APIRoot5 = srv.Router.PathPrefix(model.APIURLSuffixV5).Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
api.BaseRoutes.User = api.BaseRoutes.APIRoot.PathPrefix("/users/{user_id:[A-Za-z0-9]+}").Subrouter()
@@ -400,7 +398,7 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.Root.Handle("/manualtest", api.APIHandler(manualtesting.ManualTest)).Methods(http.MethodGet)
}
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
srv.Router.Handle(model.APIURLSuffix+"/{anything:.*}", http.HandlerFunc(api.Handle404))
InitLocal(srv)
@@ -509,7 +507,7 @@ func InitLocal(srv *app.Server) *API {
api.InitAccessControlPolicyLocal()
api.InitStatusLocal()
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
srv.LocalRouter.Handle(model.APIURLSuffix+"/{anything:.*}", http.HandlerFunc(api.Handle404))
return api
}
+1 -1
View File
@@ -8192,7 +8192,7 @@ func TestChannelEndpointsExcludeSpaces(t *testing.T) {
}
// TestLocalChannelEndpoints404Spaces verifies the local (admin socket) channel endpoints 404 a
// space, the same as /api/v4: space channels are excluded from the generic GetChannel these
// space, the same as /v1/team: space channels are excluded from the generic GetChannel these
// handlers resolve through, so they never reach the mutation.
func TestLocalChannelEndpoints404Spaces(t *testing.T) {
mainHelper.Parallel(t)
+1 -1
View File
@@ -133,7 +133,7 @@ func TestCORSRequestHandling(t *testing.T) {
port := th.App.Srv().ListenAddr.Port
host := fmt.Sprintf("http://localhost:%v", port)
url := fmt.Sprintf("%v/api/v4/system/ping", host)
url := fmt.Sprintf("%v/v1/team/system/ping", host)
req, err := http.NewRequest("GET", url, nil)
require.NoError(t, err)
+1 -1
View File
@@ -11,7 +11,7 @@ channels, posts, and other Hanzo Team resources.
# Architecture
The API is structured around resource-based endpoints under the /api/v4/ path.
The API is structured around resource-based endpoints under the /v1/team/ path.
Each endpoint is handled by specific handler functions that provide different
levels of authentication and authorization:
+24 -24
View File
@@ -20,58 +20,58 @@ import (
)
func (api *API) InitGroup() {
// GET /api/v4/groups
// GET /v1/team/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods(http.MethodGet)
// POST /api/v4/groups
// POST /v1/team/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(createGroup)).Methods(http.MethodPost)
// GET /api/v4/groups/:group_id
// GET /v1/team/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(getGroup)).Methods(http.MethodGet)
// PUT /api/v4/groups/:group_id/patch
// PUT /v1/team/groups/:group_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(patchGroup)).Methods(http.MethodPut)
// POST /api/v4/groups/:group_id/teams/:team_id/link
// POST /api/v4/groups/:group_id/channels/:channel_id/link
// POST /v1/team/groups/:group_id/teams/:team_id/link
// POST /v1/team/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(linkGroupSyncable)).Methods(http.MethodPost)
// DELETE /api/v4/groups/:group_id/teams/:team_id/link
// DELETE /api/v4/groups/:group_id/channels/:channel_id/link
// DELETE /v1/team/groups/:group_id/teams/:team_id/link
// DELETE /v1/team/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(unlinkGroupSyncable)).Methods(http.MethodDelete)
// GET /api/v4/groups/:group_id/teams/:team_id
// GET /api/v4/groups/:group_id/channels/:channel_id
// GET /v1/team/groups/:group_id/teams/:team_id
// GET /v1/team/groups/:group_id/channels/:channel_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}",
api.APISessionRequired(getGroupSyncable)).Methods(http.MethodGet)
// GET /api/v4/groups/:group_id/teams
// GET /api/v4/groups/:group_id/channels
// GET /v1/team/groups/:group_id/teams
// GET /v1/team/groups/:group_id/channels
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}",
api.APISessionRequired(getGroupSyncables)).Methods(http.MethodGet)
// PUT /api/v4/groups/:group_id/teams/:team_id/patch
// PUT /api/v4/groups/:group_id/channels/:channel_id/patch
// PUT /v1/team/groups/:group_id/teams/:team_id/patch
// PUT /v1/team/groups/:group_id/channels/:channel_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(patchGroupSyncable)).Methods(http.MethodPut)
// GET /api/v4/groups/:group_id/stats
// GET /v1/team/groups/:group_id/stats
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats",
api.APISessionRequired(getGroupStats)).Methods(http.MethodGet)
// GET /api/v4/groups/:group_id/members
// GET /v1/team/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(getGroupMembers)).Methods(http.MethodGet)
// GET /api/v4/users/:user_id/groups
// GET /v1/team/users/:user_id/groups
api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByUserId)).Methods(http.MethodGet)
// GET /api/v4/channels/:channel_id/groups
// GET /v1/team/channels/:channel_id/groups
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByChannel)).Methods(http.MethodGet)
@@ -79,27 +79,27 @@ func (api *API) InitGroup() {
api.BaseRoutes.Groups.Handle("/names",
api.APISessionRequired(getGroupsByNames)).Methods(http.MethodPost)
// GET /api/v4/teams/:team_id/groups
// GET /v1/team/teams/:team_id/groups
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(getGroupsByTeam)).Methods(http.MethodGet)
// GET /api/v4/teams/:team_id/groups_by_channels
// GET /v1/team/teams/:team_id/groups_by_channels
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels",
api.APISessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods(http.MethodGet)
// DELETE /api/v4/groups/:group_id
// DELETE /v1/team/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(deleteGroup)).Methods(http.MethodDelete)
// POST /api/v4/groups/:group_id
// POST /v1/team/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/restore",
api.APISessionRequired(restoreGroup)).Methods(http.MethodPost)
// POST /api/v4/groups/:group_id/members
// POST /v1/team/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(addGroupMembers)).Methods(http.MethodPost)
// DELETE /api/v4/groups/:group_id/members
// DELETE /v1/team/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(deleteGroupMembers)).Methods(http.MethodDelete)
}
+4 -4
View File
@@ -26,7 +26,7 @@ func handlerForGzip(t *testing.T) func(*Context, http.ResponseWriter, *http.Requ
func testAPIHandlerGzipMode(t *testing.T, name string, h http.Handler, token string) {
t.Run("Handler: "+name+" No Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
req.Header.Set(model.HeaderAuth, "Bearer "+token)
h.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
@@ -35,7 +35,7 @@ func testAPIHandlerGzipMode(t *testing.T, name string, h http.Handler, token str
t.Run("Handler: "+name+" With Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
req.Header.Set("Accept-Encoding", "gzip")
req.Header.Set(model.HeaderAuth, "Bearer "+token)
@@ -48,7 +48,7 @@ func testAPIHandlerGzipMode(t *testing.T, name string, h http.Handler, token str
func testAPIHandlerNoGzipMode(t *testing.T, name string, h http.Handler, token string) {
t.Run("Handler: "+name+" No Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
req.Header.Set(model.HeaderAuth, "Bearer "+token)
h.ServeHTTP(resp, req)
@@ -58,7 +58,7 @@ func testAPIHandlerNoGzipMode(t *testing.T, name string, h http.Handler, token s
t.Run("Handler: "+name+" With Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
req.Header.Set("Accept-Encoding", "gzip")
req.Header.Set(model.HeaderAuth, "Bearer "+token)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
// Endpoints for cloud installations should not go in this file.
func (api *API) InitHostedCustomer() {
// POST /api/v4/hosted_customer/available
// POST /v1/team/hosted_customer/available
api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods(http.MethodGet)
}
+3 -3
View File
@@ -27,13 +27,13 @@ func (api *API) InitLdap() {
api.BaseRoutes.LDAP.Handle("/migrateid", api.APISessionRequired(migrateIDLdap)).Methods(http.MethodPost)
// GET /api/v4/ldap/groups?page=0&per_page=1000
// GET /v1/team/ldap/groups?page=0&per_page=1000
api.BaseRoutes.LDAP.Handle("/groups", api.APISessionRequired(getLdapGroups)).Methods(http.MethodGet)
// POST /api/v4/ldap/groups/:remote_id/link
// POST /v1/team/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(linkLdapGroup)).Methods(http.MethodPost)
// DELETE /api/v4/ldap/groups/:remote_id/link
// DELETE /v1/team/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods(http.MethodDelete)
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate, handlerParamFileAPI)).Methods(http.MethodPost)
+8 -8
View File
@@ -1560,7 +1560,7 @@ func TestCreatePostCheckOnlineStatus(t *testing.T) {
postJSON, jsonErr := json.Marshal(post)
require.NoError(t, jsonErr)
req := httptest.NewRequest("POST", "/api/v4/posts?set_online=false", bytes.NewReader(postJSON))
req := httptest.NewRequest("POST", "/v1/team/posts?set_online=false", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1573,7 +1573,7 @@ func TestCreatePostCheckOnlineStatus(t *testing.T) {
postJSON, jsonErr = json.Marshal(post)
require.NoError(t, jsonErr)
req = httptest.NewRequest("POST", "/api/v4/posts", bytes.NewReader(postJSON))
req = httptest.NewRequest("POST", "/v1/team/posts", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1603,7 +1603,7 @@ func TestCreatePostSilentRejectedForHumanUser(t *testing.T) {
postJSON, jsonErr := json.Marshal(post)
require.NoError(t, jsonErr)
req := httptest.NewRequest("POST", "/api/v4/posts?silent=true", bytes.NewReader(postJSON))
req := httptest.NewRequest("POST", "/v1/team/posts?silent=true", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1629,7 +1629,7 @@ func TestCreatePostSilentQueryParam(t *testing.T) {
t.Run("invalid silent value returns 400", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v4/posts?silent=invalid", bytes.NewReader(postJSON))
req := httptest.NewRequest("POST", "/v1/team/posts?silent=invalid", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1639,7 +1639,7 @@ func TestCreatePostSilentQueryParam(t *testing.T) {
t.Run("silent=false does not set prop", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v4/posts?silent=false", bytes.NewReader(postJSON))
req := httptest.NewRequest("POST", "/v1/team/posts?silent=false", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1653,7 +1653,7 @@ func TestCreatePostSilentQueryParam(t *testing.T) {
t.Run("silent=0 does not set prop", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v4/posts?silent=0", bytes.NewReader(postJSON))
req := httptest.NewRequest("POST", "/v1/team/posts?silent=0", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1667,7 +1667,7 @@ func TestCreatePostSilentQueryParam(t *testing.T) {
t.Run("empty silent param is ignored", func(t *testing.T) {
session, _ := th.App.GetSession(th.Client.AuthToken)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v4/posts?silent=", bytes.NewReader(postJSON))
req := httptest.NewRequest("POST", "/v1/team/posts?silent=", bytes.NewReader(postJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1705,7 +1705,7 @@ func TestCreatePostSilentQueryParam(t *testing.T) {
}
botPostJSON, err := json.Marshal(botPost)
require.NoError(t, err)
req := httptest.NewRequest("POST", "/api/v4/posts?silent=true", bytes.NewReader(botPostJSON))
req := httptest.NewRequest("POST", "/v1/team/posts?silent=true", bytes.NewReader(botPostJSON))
req.Header.Set(model.HeaderAuth, "Bearer "+rtoken.Token)
handler.ServeHTTP(resp, req)
+3 -3
View File
@@ -36,7 +36,7 @@ func TestRateLimitingMiddleware(t *testing.T) {
th.App.Srv().Store().(*storemocks.Store).On("License").Return(&licenseStore)
port := th.App.Srv().ListenAddr.Port
url := fmt.Sprintf("http://localhost:%v/api/v4/system/ping", port)
url := fmt.Sprintf("http://localhost:%v/v1/team/system/ping", port)
client := &http.Client{}
t.Run("requests within burst succeed", func(t *testing.T) {
@@ -93,7 +93,7 @@ func TestRateLimitingVaryByHeader(t *testing.T) {
th.App.Srv().Store().(*storemocks.Store).On("License").Return(&licenseStore)
port := th.App.Srv().ListenAddr.Port
url := fmt.Sprintf("http://localhost:%v/api/v4/system/ping", port)
url := fmt.Sprintf("http://localhost:%v/v1/team/system/ping", port)
client := &http.Client{}
// 2 requests with client-A should succeed
@@ -157,7 +157,7 @@ func TestRateLimitingVaryByUser(t *testing.T) {
th.App.Srv().RateLimiter = rl
port := th.App.Srv().ListenAddr.Port
url := fmt.Sprintf("http://localhost:%v/api/v4/system/ping", port)
url := fmt.Sprintf("http://localhost:%v/v1/team/system/ping", port)
client := &http.Client{}
userAToken := th.Client.AuthToken
+1 -1
View File
@@ -160,7 +160,7 @@ func fillUserReportOptions(values url.Values) (*model.UserReportOptions, *model.
// getPostsForReporting retrieves posts for reporting purposes with cursor-based pagination.
//
// API Endpoint: POST /api/v4/reports/posts
// API Endpoint: POST /v1/team/reports/posts
//
// Cursor Behavior:
// - The cursor is opaque and self-contained (base64-encoded)
+23 -23
View File
@@ -329,7 +329,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
@@ -351,7 +351,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
@@ -364,7 +364,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.Client, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.Client, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
@@ -377,7 +377,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result model.ReportPostListResponse
@@ -395,7 +395,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
@@ -411,7 +411,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusNotFound, resp.StatusCode, "should return 404 for non-existent channel")
})
@@ -426,7 +426,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode, "empty cursor should be accepted")
var result model.ReportPostListResponse
@@ -447,7 +447,7 @@ func TestGetPostsForReporting(t *testing.T) {
// cursor omitted
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode, "omitted cursor should be accepted")
var result model.ReportPostListResponse
@@ -466,7 +466,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "invalid cursor format should be rejected")
})
@@ -480,7 +480,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 5,
}
resp1, body1 := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody1)
resp1, body1 := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody1)
require.Equal(t, http.StatusOK, resp1.StatusCode)
var result1 model.ReportPostListResponse
@@ -496,7 +496,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 5,
}
resp2, body2 := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody2)
resp2, body2 := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody2)
require.Equal(t, http.StatusOK, resp2.StatusCode)
var result2 model.ReportPostListResponse
@@ -524,7 +524,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 5,
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result model.ReportPostListResponse
@@ -567,7 +567,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 100,
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode, "cursor parameters should take precedence (self-contained cursor)")
var result model.ReportPostListResponse
@@ -609,7 +609,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result model.ReportPostListResponse
@@ -634,7 +634,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 100,
}
resp1, body1 := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody1)
resp1, body1 := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody1)
require.Equal(t, http.StatusOK, resp1.StatusCode)
var result1 model.ReportPostListResponse
@@ -659,7 +659,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 100,
}
resp2, body2 := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody2)
resp2, body2 := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody2)
require.Equal(t, http.StatusOK, resp2.StatusCode)
var result2 model.ReportPostListResponse
@@ -710,7 +710,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 100,
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result model.ReportPostListResponse
@@ -732,7 +732,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 5000, // More than max
}
resp, body := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, body := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusOK, resp.StatusCode, "should cap per_page to MaxReportingPerPage instead of rejecting")
// Verify it was capped to max (we can't directly check the query but the request succeeds)
@@ -752,7 +752,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should reject invalid ID format")
})
@@ -768,7 +768,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should reject cursor with invalid time_field")
})
@@ -784,7 +784,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should reject cursor with invalid sort_direction")
})
@@ -800,7 +800,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should reject cursor with invalid channel_id")
})
@@ -816,7 +816,7 @@ func TestGetPostsForReporting(t *testing.T) {
"per_page": 10,
}
resp, _ := doPostJSON(t, th.SystemAdminClient, "/api/v4/reports/posts", requestBody)
resp, _ := doPostJSON(t, th.SystemAdminClient, "/v1/team/reports/posts", requestBody)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should reject cursor with invalid cursor_id")
})
}
@@ -62,7 +62,7 @@ func NewSelfReferentialSyncHandler(t *testing.T, service *sharedchannel.Service,
// HandleRequest processes incoming HTTP requests for the test server.
func (h *SelfReferentialSyncHandler) HandleRequest(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v4/remotecluster/msg":
case "/v1/team/remotecluster/msg":
atomic.AddInt32(h.syncMessageCount, 1)
// Read and process the sync message
@@ -117,10 +117,10 @@ func (h *SelfReferentialSyncHandler) HandleRequest(w http.ResponseWriter, r *htt
writeOKResponse(w)
case "/api/v4/remotecluster/ping":
case "/v1/team/remotecluster/ping":
writeOKResponse(w)
case "/api/v4/remotecluster/confirm_invite":
case "/v1/team/remotecluster/confirm_invite":
writeOKResponse(w)
default:
+4 -4
View File
@@ -323,7 +323,7 @@ func TestSiteURLTest(t *testing.T) {
client := th.Client
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/valid/api/v4/system/ping") {
if strings.HasSuffix(r.URL.Path, "/valid/v1/team/system/ping") {
w.WriteHeader(200)
} else {
w.WriteHeader(400)
@@ -1051,7 +1051,7 @@ func TestPushNotificationAck(t *testing.T) {
t.Run("should return error when the ack body is not passed", func(t *testing.T) {
handler := api.APIHandler(pushNotificationAck)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v4/notifications/ack", nil)
req := httptest.NewRequest("POST", "/v1/team/notifications/ack", nil)
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
handler.ServeHTTP(resp, req)
@@ -1065,7 +1065,7 @@ func TestPushNotificationAck(t *testing.T) {
handler := api.APIHandler(pushNotificationAck)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v4/notifications/ack", nil)
req := httptest.NewRequest("POST", "/v1/team/notifications/ack", nil)
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
req.Body = io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{"id":"123", "is_id_loaded":true, "post_id":"%s", "type": "%s"}`, privatePost.Id, model.PushTypeMessage)))
@@ -1121,7 +1121,7 @@ func TestPushNotificationAck(t *testing.T) {
handler := api.APIHandler(pushNotificationAck)
resp := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v4/notifications/ack", nil)
req := httptest.NewRequest("POST", "/v1/team/notifications/ack", nil)
req.Header.Set(model.HeaderAuth, "Bearer "+session.Token)
req.Body = io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{"id":"123", "is_id_loaded":true, "platform": "%s", "post_id":"%s", "type": "%s"}`, tc.platform, th.BasicPost.Id, model.PushTypeMessage)))
+3 -3
View File
@@ -13,11 +13,11 @@ import (
)
func (api *API) InitUsage() {
// GET /api/v4/usage/posts
// GET /v1/team/usage/posts
api.BaseRoutes.Usage.Handle("/posts", api.APISessionRequired(getPostsUsage)).Methods(http.MethodGet)
// GET /api/v4/usage/storage
// GET /v1/team/usage/storage
api.BaseRoutes.Usage.Handle("/storage", api.APISessionRequired(getStorageUsage)).Methods(http.MethodGet)
// GET /api/v4/usage/teams
// GET /v1/team/usage/teams
api.BaseRoutes.Usage.Handle("/teams", api.APISessionRequired(getTeamsUsage)).Methods(http.MethodGet)
}
+1 -1
View File
@@ -157,7 +157,7 @@ func (a *App) RecycleDatabaseConnection(rctx request.CTX) {
}
func (a *App) TestSiteURL(rctx request.CTX, siteURL string) *model.AppError {
url := fmt.Sprintf("%s/api/v4/system/ping", siteURL)
url := fmt.Sprintf("%s%s/system/ping", siteURL, model.APIURLSuffix)
res, err := http.Get(url)
if err != nil || res.StatusCode != 200 {
return model.NewAppError("testSiteURL", "app.admin.test_site_url.failure", nil, "", http.StatusBadRequest)
+1 -1
View File
@@ -408,7 +408,7 @@ func (a *App) MFARequired(rctx request.CTX) *model.AppError {
// Special case to let user get themself
subpath, _ := utils.GetSubpathFromConfig(a.Config())
if rctx.Path() == path.Join(subpath, "/api/v4/users/me") {
if rctx.Path() == path.Join(subpath, model.APIURLSuffix, "users/me") {
return nil
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/store"
)
// channelJoinRequestPaginationDefaultPerPage matches the public /api/v4 default
// channelJoinRequestPaginationDefaultPerPage matches the public /v1/team default
// for paginated endpoints.
const channelJoinRequestPaginationDefaultPerPage = 60
+1 -1
View File
@@ -315,7 +315,7 @@ func (a *App) GetEmojiStaticURL(rctx request.CTX, emojiName string) (string, *mo
emoji, err := a.Srv().Store().Emoji().GetByName(rctx, emojiName, true)
if err == nil {
return path.Join(subPath, "/api/v4/emoji", emoji.Id, "image"), nil
return path.Join(subPath, model.APIURLSuffix, "emoji", emoji.Id, "image"), nil
}
var nfErr *store.ErrNotFound
switch {
@@ -1767,7 +1767,7 @@ func TestGetPostActionClient(t *testing.T) {
{
name: "same host with non-plugin path does not get auth",
siteURL: "http://localhost:8065",
requestURL: "http://localhost:8065/api/v4/posts",
requestURL: "http://localhost:8065/v1/team/posts",
expectAuth: false,
},
{
@@ -1791,8 +1791,8 @@ func TestGetPostActionClient(t *testing.T) {
{
name: "path traversal escaping plugins does not get auth",
siteURL: "http://localhost:8065",
requestURL: "http://localhost:8065/plugins/../api/v4/posts",
expectAuth: false, // path.Clean normalizes to /api/v4/posts
requestURL: "http://localhost:8065/plugins/../v1/team/posts",
expectAuth: false, // path.Clean normalizes to /v1/team/posts
},
{
name: "subpath with plugin path gets auth",
@@ -2395,7 +2395,7 @@ func TestDoPluginRequest(t *testing.T) {
},
{
name: "non-plugins path",
rawURL: "/api/v4/users",
rawURL: "/v1/team/users",
expectErr: true,
errDetail: "plugins not in path",
},
+2 -2
View File
@@ -542,7 +542,7 @@ func (a *App) rawSendToPushProxy(msg *model.PushNotification) (model.PushRespons
return nil, fmt.Errorf("failed to encode to JSON: %w", err)
}
url := strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/") + model.APIURLSuffixV1 + "/send_push"
url := strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/") + model.PushURLSuffix + "/send_push"
request, err := http.NewRequest("POST", url, bytes.NewReader(msgJSON))
if err != nil {
return nil, err
@@ -627,7 +627,7 @@ func (a *App) SendAckToPushProxy(rctx request.CTX, ack *model.PushNotificationAc
request, err := http.NewRequest(
"POST",
strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.APIURLSuffixV1+"/ack",
strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.PushURLSuffix+"/ack",
bytes.NewReader(ackJSON),
)
if err != nil {
+1 -1
View File
@@ -953,7 +953,7 @@ func TestGetAuthorizationServerMetadata_DCRConfig(t *testing.T) {
require.NotNil(t, metadata)
// Should include registration endpoint when DCR is enabled
assert.Equal(t, "https://example.com/api/v4/oauth/apps/register", metadata.RegistrationEndpoint)
assert.Equal(t, "https://example.com/v1/team/oauth/apps/register", metadata.RegistrationEndpoint)
// Should include basic OAuth endpoints
assert.Equal(t, "https://example.com", metadata.Issuer)
+2 -2
View File
@@ -984,8 +984,8 @@ func (a *App) getLinkMetadataForURL(rctx request.CTX, requestURL string) (*openg
var body io.ReadCloser
var contentType string
if (request.URL.Scheme+"://"+request.URL.Host) == a.GetSiteURL() && request.URL.Path == "/api/v4/image" {
// /api/v4/image requires authentication, so bypass the API by hitting the proxy directly
if (request.URL.Scheme+"://"+request.URL.Host) == a.GetSiteURL() && request.URL.Path == model.APIURLSuffix+"/image" {
// The image endpoint requires authentication, so bypass the API by hitting the proxy directly
body, contentType, err = a.ImageProxy().GetImageDirect(a.ImageProxy().GetUnproxiedImageURL(request.URL.String()))
} else {
request.Header.Add("Accept", "image/*")
+3 -3
View File
@@ -1061,7 +1061,7 @@ func TestPreparePostForClientWithImageProxy(t *testing.T) {
func testProxyLinkedImage(t *testing.T, th *TestHelper, shouldProxy bool) {
postTemplate := "![foo](%v)"
imageURL := "http://mydomain.com/myimage"
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
proxiedImageURL := "http://mymattermost.com/v1/team/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
post := &model.Post{
UserId: th.BasicUser.Id,
@@ -1136,7 +1136,7 @@ func testProxyOpenGraphImage(t *testing.T, th *TestHelper, shouldProxy bool) {
image := og.Images[0]
if shouldProxy {
assert.Equal(t, "", image.URL, "image URL should not be set with proxy")
assert.Equal(t, "http://mymattermost.com/api/v4/image?url="+url.QueryEscape(server.URL+"/test-image3.png"), image.SecureURL, "secure image URL should be sent through proxy")
assert.Equal(t, "http://mymattermost.com/v1/team/image?url="+url.QueryEscape(server.URL+"/test-image3.png"), image.SecureURL, "secure image URL should be sent through proxy")
} else {
assert.Equal(t, server.URL+"/test-image3.png", image.URL, "image URL should be set")
assert.Equal(t, "", image.SecureURL, "secure image URL should not be set")
@@ -2776,7 +2776,7 @@ func TestGetLinkMetadata(t *testing.T) {
assert.IsType(t, &url.Error{}, err)
assert.ErrorContains(t, err, httpservice.ErrAddressForbidden.Error())
requestURL = th.App.GetSiteURL() + "/api/v4/image?url=" + url.QueryEscape(requestURL)
requestURL = th.App.GetSiteURL() + "/v1/team/image?url=" + url.QueryEscape(requestURL)
// Note that this request still fails while testing because the request made by the image proxy is blocked
og, img, _, err = th.App.getLinkMetadata(th.Context, requestURL, timestamp, false, "")
+5 -5
View File
@@ -910,7 +910,7 @@ func TestImageProxy(t *testing.T) {
ProxyOptions: testHMACKey,
ImageURL: "http://mydomain.com/myimage",
ProxiedRemovedImageURL: "http://mydomain.com/myimage",
ProxiedImageURL: "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage",
ProxiedImageURL: "http://mymattermost.com/v1/team/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage",
},
"atmos/camo_SameSite": {
ProxyType: model.ImageProxyTypeAtmosCamo,
@@ -940,7 +940,7 @@ func TestImageProxy(t *testing.T) {
ProxyType: model.ImageProxyTypeLocal,
ImageURL: "http://mydomain.com/myimage",
ProxiedRemovedImageURL: "http://mydomain.com/myimage",
ProxiedImageURL: "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage",
ProxiedImageURL: "http://mymattermost.com/v1/team/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage",
},
"local_SameSite": {
ProxyType: model.ImageProxyTypeLocal,
@@ -1172,7 +1172,7 @@ func TestCreatePost(t *testing.T) {
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
imageURL := "http://mydomain.com/myimage"
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
proxiedImageURL := "http://mymattermost.com/v1/team/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
post := &model.Post{
ChannelId: th.BasicChannel.Id,
@@ -2037,7 +2037,7 @@ func TestPatchPost(t *testing.T) {
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
imageURL := "http://mydomain.com/myimage"
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
proxiedImageURL := "http://mymattermost.com/v1/team/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
post := &model.Post{
ChannelId: th.BasicChannel.Id,
@@ -2498,7 +2498,7 @@ func TestUpdatePost(t *testing.T) {
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
imageURL := "http://mydomain.com/myimage"
proxiedImageURL := "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
proxiedImageURL := "http://mymattermost.com/v1/team/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage"
post := &model.Post{
ChannelId: th.BasicChannel.Id,
@@ -120,7 +120,7 @@ func sessionAttributeRawValues(t *testing.T, th *TestHelper, sessionID string) m
func newSessionAttributesRequest(t *testing.T, userAgent, remoteAddr string) *http.Request {
t.Helper()
r := httptest.NewRequest(http.MethodGet, "/api/v4/test", nil)
r := httptest.NewRequest(http.MethodGet, "/v1/team/test", nil)
if userAgent != "" {
r.Header.Set("User-Agent", userAgent)
}
@@ -329,7 +329,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
idx := i // Capture index for closure
testServers[i] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
clusterName := fmt.Sprintf("cluster-%d", idx+1)
atomic.AddInt32(syncMessagesPerCluster[clusterName], 1)
@@ -455,7 +455,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Create test HTTP server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v4/remotecluster/msg":
case "/v1/team/remotecluster/msg":
syncAttempts.Add(1)
if failureMode.Load() {
@@ -470,7 +470,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
} else {
writeOKResponse(w)
}
case "/api/v4/remotecluster/ping":
case "/v1/team/remotecluster/ping":
writeOKResponse(w)
default:
w.WriteHeader(http.StatusNotFound)
@@ -577,7 +577,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Create test HTTP server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
syncMessageCount.Add(1)
}
writeOKResponse(w)
@@ -666,7 +666,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Create test HTTP server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
syncMessageCount.Add(1)
// Parse message to check if it's a user sync
@@ -855,7 +855,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
return
}
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
syncAttempts.Add(1)
// On second attempt, go offline
if syncAttempts.Load() >= 2 {
@@ -1106,7 +1106,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Create test HTTP servers for both "servers"
serverAHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
// Parse message to track what gets synced back to A
bodyBytes, _ := io.ReadAll(r.Body)
var frame model.RemoteClusterFrame
@@ -1125,7 +1125,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
})
serverBHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
// Parse message to track what gets synced to B
bodyBytes, _ := io.ReadAll(r.Body)
var frame model.RemoteClusterFrame
@@ -534,7 +534,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
var syncHandler *SelfReferentialSyncHandler
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
syncAttempts.Add(1)
if failureMode.Load() {
@@ -668,7 +668,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Create test HTTP server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
bodyBytes, _ := io.ReadAll(r.Body)
// Count add and remove operations
@@ -866,7 +866,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Create test server for this cluster
idx := i // Capture index for closure
testServers[i] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
clusterName := fmt.Sprintf("cluster-%d", idx+1)
atomic.AddInt32(syncMessagesPerCluster[clusterName], 1)
@@ -1202,7 +1202,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
return
}
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
currentAttempt := syncAttempts.Add(1)
// On second sync cycle, go offline (allow first full sync to complete)
if currentAttempt > 2 {
@@ -1387,7 +1387,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Create a wrapper handler to intercept sync messages
testServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Intercept and track channel syncs
if r.URL.Path == "/api/v4/remotecluster/msg" {
if r.URL.Path == "/v1/team/remotecluster/msg" {
bodyBytes, pErr := io.ReadAll(r.Body)
if pErr == nil {
// Restore body for actual handler
@@ -1584,7 +1584,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// // Create test HTTP server that tracks sync messages
// testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// if r.URL.Path == "/api/v4/remotecluster/msg" {
// if r.URL.Path == "/v1/team/remotecluster/msg" {
// syncMessageCount.Add(1)
// // Read body once
@@ -62,12 +62,12 @@ func NewSelfReferentialSyncHandler(t *testing.T, service *sharedchannel.Service,
// HandleRequest processes incoming HTTP requests for the test server.
// This handler includes common remote cluster endpoints to simulate a real remote cluster:
// - /api/v4/remotecluster/msg: Main sync message endpoint
// - /api/v4/remotecluster/ping: Ping endpoint to maintain online status (prevents offline after 5 minutes)
// - /api/v4/remotecluster/confirm_invite: Invitation confirmation endpoint
// - /v1/team/remotecluster/msg: Main sync message endpoint
// - /v1/team/remotecluster/ping: Ping endpoint to maintain online status (prevents offline after 5 minutes)
// - /v1/team/remotecluster/confirm_invite: Invitation confirmation endpoint
func (h *SelfReferentialSyncHandler) HandleRequest(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v4/remotecluster/msg":
case "/v1/team/remotecluster/msg":
currentCall := h.syncMessageCount.Add(1)
// Read and process the sync message
@@ -150,10 +150,10 @@ func (h *SelfReferentialSyncHandler) HandleRequest(w http.ResponseWriter, r *htt
writeOKResponse(w)
case "/api/v4/remotecluster/ping":
case "/v1/team/remotecluster/ping":
writeOKResponse(w)
case "/api/v4/remotecluster/confirm_invite":
case "/v1/team/remotecluster/confirm_invite":
writeOKResponse(w)
default:
+1 -1
View File
@@ -680,7 +680,7 @@ func TestCreateWebhookPostWithOverriddenIcon(t *testing.T) {
clientPost := th.App.PreparePostForClient(th.Context, post, &model.PreparePostForClientOpts{IsNewPost: true})
assert.Equal(t, fmt.Sprintf("/api/v4/emoji/%s/image", emoji.Id), clientPost.GetProp(model.PostPropsOverrideIconURL))
assert.Equal(t, fmt.Sprintf("/v1/team/emoji/%s/image", emoji.Id), clientPost.GetProp(model.PostPropsOverrideIconURL))
})
t.Run("should set props based on icon_emoji (with colons around emoji name)", func(t *testing.T) {
+22 -22
View File
@@ -38,9 +38,9 @@ func TestHandlerServeHTTPErrors(t *testing.T) {
redirect bool
}{
{"redirect on desktop non-api endpoint", "/login/sso/saml", false, true},
{"not redirect on desktop api endpoint", "/api/v4/test", false, false},
{"not redirect on desktop api endpoint", "/v1/team/test", false, false},
{"not redirect on mobile non-api endpoint", "/login/sso/saml", true, false},
{"not redirect on mobile api endpoint", "/api/v4/test", true, false},
{"not redirect on mobile api endpoint", "/v1/team/test", true, false},
}
for _, tt := range flagtests {
@@ -71,7 +71,7 @@ func TestHandlerServeDefaultSecurityHeaders(t *testing.T) {
handler := web.NewHandler(handlerForServeDefaultSecurityHeaders)
paths := []string{
"/api/v4/test", // API
"/v1/team/test", // API
"/static/manifest.json", // this should always exist. Static files have their own handler
// Note that the plugin handler isn't tested, also plugins may support arbitrary functionality
}
@@ -127,7 +127,7 @@ func TestHandlerServeHTTPSecureTransport(t *testing.T) {
web := New(th.Server)
handler := web.NewHandler(handlerForHTTPSecureTransport)
request := httptest.NewRequest("GET", "/api/v4/test", nil)
request := httptest.NewRequest("GET", "/v1/team/test", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
@@ -145,7 +145,7 @@ func TestHandlerServeHTTPSecureTransport(t *testing.T) {
*config.ServiceSettings.TLSStrictTransport = false
})
request = httptest.NewRequest("GET", "/api/v4/test", nil)
request = httptest.NewRequest("GET", "/v1/team/test", nil)
response = httptest.NewRecorder()
handler.ServeHTTP(response, request)
@@ -199,7 +199,7 @@ func TestHandlerServeCSRFToken(t *testing.T) {
// CSRF Token Used - Success Expected
request := httptest.NewRequest("POST", "/api/v4/test", nil)
request := httptest.NewRequest("POST", "/v1/team/test", nil)
request.AddCookie(cookie)
request.AddCookie(cookie2)
request.AddCookie(cookie3)
@@ -213,7 +213,7 @@ func TestHandlerServeCSRFToken(t *testing.T) {
// No CSRF Token Used - Failure Expected
request = httptest.NewRequest("POST", "/api/v4/test", nil)
request = httptest.NewRequest("POST", "/v1/team/test", nil)
request.AddCookie(cookie)
request.AddCookie(cookie2)
request.AddCookie(cookie3)
@@ -229,7 +229,7 @@ func TestHandlerServeCSRFToken(t *testing.T) {
th.App.UpdateConfig(func(config *model.Config) {
*config.ServiceSettings.ExperimentalStrictCSRFEnforcement = false
})
request = httptest.NewRequest("POST", "/api/v4/test", nil)
request = httptest.NewRequest("POST", "/v1/team/test", nil)
request.AddCookie(cookie)
request.AddCookie(cookie2)
request.AddCookie(cookie3)
@@ -266,7 +266,7 @@ func TestHandlerServeCSRFToken(t *testing.T) {
// CSRF Token Used - Success Expected
request = httptest.NewRequest("POST", "/api/v4/test", nil)
request = httptest.NewRequest("POST", "/v1/team/test", nil)
request.AddCookie(cookie)
request.AddCookie(cookie2)
request.AddCookie(cookie3)
@@ -280,7 +280,7 @@ func TestHandlerServeCSRFToken(t *testing.T) {
// No CSRF Token Used - Failure Expected
request = httptest.NewRequest("POST", "/api/v4/test", nil)
request = httptest.NewRequest("POST", "/v1/team/test", nil)
request.AddCookie(cookie)
request.AddCookie(cookie2)
request.AddCookie(cookie3)
@@ -310,7 +310,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
IsStatic: false,
}
request := httptest.NewRequest("POST", "/api/v4/test", nil)
request := httptest.NewRequest("POST", "/v1/team/test", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
assert.Equal(t, 200, response.Code)
@@ -609,7 +609,7 @@ func TestHandlerServeInvalidToken(t *testing.T) {
Value: "invalid",
}
request := httptest.NewRequest("POST", "/api/v4/test", nil)
request := httptest.NewRequest("POST", "/v1/team/test", nil)
request.AddCookie(cookie)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
@@ -670,7 +670,7 @@ func TestHandlerServeCSRFFailureClearsAuthCookie(t *testing.T) {
Value: session.Token,
}
request := httptest.NewRequest("POST", "/api/v4/test", nil)
request := httptest.NewRequest("POST", "/v1/team/test", nil)
request.AddCookie(cookie)
request.Header.Add(model.HeaderRequestedWith, model.HeaderRequestedWithXML)
response := httptest.NewRecorder()
@@ -1022,7 +1022,7 @@ func TestHandlerServeHTTPBasicSecurityChecks(t *testing.T) {
handler := web.NewHandler(noOpHandler)
// using the default URL length
request := httptest.NewRequest("GET", "/api/v4/test?with=not&many=query_params", nil)
request := httptest.NewRequest("GET", "/v1/team/test?with=not&many=query_params", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
assert.Equal(t, http.StatusOK, response.Code)
@@ -1053,7 +1053,7 @@ func TestHandlerServeHTTPBasicSecurityChecks(t *testing.T) {
web := New(th.Server)
handler := web.NewHandler(noOpHandler)
request := httptest.NewRequest("GET", "/api/v4/test?a_url_longer_than_10_characters_including_query_params", nil)
request := httptest.NewRequest("GET", "/v1/team/test?a_url_longer_than_10_characters_including_query_params", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
assert.Equal(t, http.StatusRequestURITooLong, response.Code)
@@ -1086,7 +1086,7 @@ func TestHandlerServeHTTPBasicSecurityChecks(t *testing.T) {
// this URL is within the 20 characters limit excluding query params.
// but this should still fail as URL length includes query params
request := httptest.NewRequest("GET", "/api/v4/test?a_url_longer_than_10_characters_including_query_params", nil)
request := httptest.NewRequest("GET", "/v1/team/test?a_url_longer_than_10_characters_including_query_params", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
assert.Equal(t, http.StatusRequestURITooLong, response.Code)
@@ -1114,7 +1114,7 @@ func TestHandlerServeHTTPRequestPayloadLimit(t *testing.T) {
handler := web.NewHandler(jsonReaderHandler)
body := strings.NewReader("\"a very small request body\"")
request := httptest.NewRequest("POST", "/api/v4/test", body)
request := httptest.NewRequest("POST", "/v1/team/test", body)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
@@ -1150,7 +1150,7 @@ func TestHandlerServeHTTPRequestPayloadLimit(t *testing.T) {
// Even though we have set the max payload size to be 1, we still need at least 513 bytes (1 byte configured + bytes.MinRead = 1 + 512 = 513) bytes.
// This is because the buffer will always be at least bytes.MinRead bytes large, so the effective payload limit is bytes.MinRead + the configured value.
body := strings.NewReader("\"weunfrwghyajuaqqkrecexurpmrmgpimjieymiwfhfrrrgiqpqrznkjtubwcuybyixyxwtwddpytukritccyugyeuvdtzjkkyiwhquzqkrqkhgyyfqnquzchjqkrkzfrxthduzizqtdxzqirxhzihbivmkdwpbeddepdanzuuzqxbdqfvgkwumervhghywexitbjdnvxcniuamwmqdecbbqbgnjjqwkdcucvnpwynuruztpdtmmvbpkevurpjdwdhpayaindzmnkmyybudfkjdkqwuiviriudtqytybuwfkkwpepwhpekfewnxgpkfctdqjmemngvntnizvfznaiqpbumgtcxidvawtgcdyqijbxzrgezvjmcwikiabbpqabrwfgncrmvqththepffatnhchhnmrhkuqvgrzfugzhuwicaemhcacrazmgzmrgrkuhwucfydhwxfhzfukzjhvdxkuhzjrxwippxadvwzigndxwdxvganxggjjxwdqtgqgnpqqygndviadvttwfntcreitijaqrpfygdehbcftyfcjvrfwvjmbtdptutjgtbyhbyddfecyyujgrxyujzmryymj\"")
request := httptest.NewRequest("POST", "/api/v4/test", body)
request := httptest.NewRequest("POST", "/v1/team/test", body)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
@@ -1172,7 +1172,7 @@ func TestHandlerConnectionIdHeader(t *testing.T) {
web := New(th.Server)
handler := web.NewHandler(handlerFunc)
request := httptest.NewRequest("GET", "/api/v4/test", nil)
request := httptest.NewRequest("GET", "/v1/team/test", nil)
request.Header.Set(model.ConnectionId, connectionId)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
@@ -1193,7 +1193,7 @@ func TestHandlerConnectionIdHeader(t *testing.T) {
web := New(th.Server)
handler := web.NewHandler(handlerFunc)
request := httptest.NewRequest("GET", "/api/v4/test", nil)
request := httptest.NewRequest("GET", "/v1/team/test", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
@@ -1221,7 +1221,7 @@ func TestHandleContextErrorZeroStatusCode(t *testing.T) {
Err: appErr,
}
request := httptest.NewRequest("POST", "/api/v4/test", nil)
request := httptest.NewRequest("POST", "/v1/team/test", nil)
response := httptest.NewRecorder()
h := Handler{
@@ -1246,7 +1246,7 @@ func TestHandleContextErrorZeroStatusCode(t *testing.T) {
Err: appErr,
}
request := httptest.NewRequest("POST", "/api/v4/test", nil)
request := httptest.NewRequest("POST", "/v1/team/test", nil)
response := httptest.NewRecorder()
h := Handler{
+1 -1
View File
@@ -51,7 +51,7 @@ func (w *Web) InitOAuth() {
w.MainRouter.Handle("/api/v3/oauth/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
w.MainRouter.Handle("/signup/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
w.MainRouter.Handle("/login/{service:[A-Za-z0-9]+}/complete", w.APIHandler(completeOAuth)).Methods(http.MethodGet)
w.MainRouter.Handle("/api/v4/oauth_test", w.APISessionRequired(testHandler)).Methods(http.MethodGet)
w.MainRouter.Handle(model.APIURLSuffix+"/oauth_test", w.APISessionRequired(testHandler)).Methods(http.MethodGet)
}
func testHandler(c *Context, w http.ResponseWriter, r *http.Request) {
+1 -1
View File
@@ -135,7 +135,7 @@ type Params struct {
RequestId string
}
var getChannelMembersForUserRegex = regexp.MustCompile("/api/v4/users/[A-Za-z0-9]{26}/channel_members")
var getChannelMembersForUserRegex = regexp.MustCompile(model.APIURLSuffix + "/users/[A-Za-z0-9]{26}/channel_members")
func ParamsFromRequest(r *http.Request) *Params {
params := &Params{}
@@ -37,7 +37,7 @@ func newResponseWithHijack(original *httptest.ResponseRecorder) *responseRecorde
func TestStatusCodeIsAccessible(t *testing.T) {
resp := newWrappedWriter(httptest.NewRecorder())
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
handler := TestHandler{func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}}
@@ -47,7 +47,7 @@ func TestStatusCodeIsAccessible(t *testing.T) {
func TestStatusCodeShouldBe200IfNotHeaderWritten(t *testing.T) {
resp := newWrappedWriter(httptest.NewRecorder())
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
handler := TestHandler{func(w http.ResponseWriter, r *http.Request) {
n, err := w.Write([]byte{})
require.NoError(t, err, "Failed to write response")
@@ -59,7 +59,7 @@ func TestStatusCodeShouldBe200IfNotHeaderWritten(t *testing.T) {
func TestForUnsupportedHijack(t *testing.T) {
resp := newWrappedWriter(httptest.NewRecorder())
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
handler := TestHandler{func(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(*responseWriterWrapper).Hijack()
assert.Error(t, err)
@@ -72,7 +72,7 @@ func TestForUnsupportedHijack(t *testing.T) {
func TestForSupportedHijack(t *testing.T) {
resp := newWrappedWriter(newResponseWithHijack(httptest.NewRecorder()))
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
handler := TestHandler{func(w http.ResponseWriter, r *http.Request) {
conn, rw, err := w.(*responseWriterWrapper).Hijack()
require.NoError(t, err, "Hijack should succeed with supporting ResponseWriter")
@@ -84,7 +84,7 @@ func TestForSupportedHijack(t *testing.T) {
func TestForSupportedFlush(t *testing.T) {
resp := newWrappedWriter(httptest.NewRecorder())
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req := httptest.NewRequest("GET", "/v1/team/test", nil)
handler := TestHandler{func(w http.ResponseWriter, r *http.Request) {
n, err := w.Write([]byte{})
require.NoError(t, err, "Failed to write response")
@@ -183,7 +183,7 @@ func (s *MmctlE2ETestSuite) TestArchiveCommandCmdF() {
err := archiveCommandCmdF(c, &cobra.Command{}, []string{nonexistentCommandID})
s.Require().NotNil(err)
s.Require().Equal(fmt.Sprintf("Unable to archive command '%s' error: Sorry, we could not find the page., There doesn't appear to be an api call for the url='/api/v4/commands/nonexistent-command-id'. Typo? are you missing a team_id or user_id as part of the url?", nonexistentCommandID), err.Error())
s.Require().Equal(fmt.Sprintf("Unable to archive command '%s' error: Sorry, we could not find the page., There doesn't appear to be an api call for the url='/v1/team/commands/nonexistent-command-id'. Typo? are you missing a team_id or user_id as part of the url?", nonexistentCommandID), err.Error())
s.Require().Len(printer.GetLines(), 0)
s.Require().Len(printer.GetErrorLines(), 0)
})
+1 -1
View File
@@ -168,7 +168,7 @@ func TestVerifyCertificates(t *testing.T) {
func TestNewAPIv4Client(t *testing.T) {
t.Run("should take http proxy into account", func(t *testing.T) {
router := mux.NewRouter()
router.Handle("/api/v4/users/me", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
router.Handle("/v1/team/users/me", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := &model.User{
Id: model.NewId(),
}
+3 -1
View File
@@ -15,6 +15,8 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/mattermost/mattermost/server/public/model"
)
const (
@@ -219,7 +221,7 @@ func Flush() error {
fallthrough
case len(printer.Lines) > 0:
entity := cmd.Parent().Name()
container := strings.TrimSuffix(printer.serverAddr, "api/v4")
container := strings.TrimSuffix(printer.serverAddr, model.APIURLSuffix)
if container != "" {
container = fmt.Sprintf(" on %s", container)
}
+3 -3
View File
@@ -132,7 +132,7 @@ services:
- './../../enterprise:/home/enterprise'
restart: on-failure
healthcheck:
test: ["CMD", "curl", "-f", "http://leader:8065/api/v4/system/ping"]
test: ["CMD", "curl", "-f", "http://leader:8065/v1/team/system/ping"]
interval: 5s
timeout: 30s
retries: 30
@@ -165,7 +165,7 @@ services:
- './../:/home/mattermost-server:Z'
- './../../enterprise:/home/enterprise:Z'
healthcheck:
test: ["CMD", "curl", "-f", "http://follower:8065/api/v4/system/ping"]
test: ["CMD", "curl", "-f", "http://follower:8065/v1/team/system/ping"]
interval: 5s
timeout: 30s
retries: 30
@@ -199,7 +199,7 @@ services:
- './../:/home/mattermost-server:Z'
- './../../enterprise:/home/enterprise:Z'
healthcheck:
test: ["CMD", "curl", "-f", "http://follower2:8065/api/v4/system/ping"]
test: ["CMD", "curl", "-f", "http://follower2:8065/v1/team/system/ping"]
interval: 5s
timeout: 30s
retries: 30
@@ -153,7 +153,7 @@ func (proxy *ImageProxy) GetProxiedImageURL(imageURL string) string {
return parsedURL.String()
}
return proxy.siteURL.String() + "/api/v4/image?url=" + url.QueryEscape(parsedURL.String())
return proxy.siteURL.String() + model.APIURLSuffix + "/image?url=" + url.QueryEscape(parsedURL.String())
}
// GetUnproxiedImageURL takes the URL of an image on the image proxy and returns the original URL of the image.
@@ -162,7 +162,7 @@ func (proxy *ImageProxy) GetUnproxiedImageURL(proxiedURL string) string {
}
func getUnproxiedImageURL(proxiedURL, siteURL string) string {
if !strings.HasPrefix(proxiedURL, siteURL+"/api/v4/image?url=") {
if !strings.HasPrefix(proxiedURL, siteURL+model.APIURLSuffix+"/image?url=") {
return proxiedURL
}
@@ -19,7 +19,7 @@ func TestGetProxiedImageURL(t *testing.T) {
require.NoError(t, err)
imageURL := "https://hanzo.ai/wp-content/uploads/2022/02/logoHorizontal.png"
proxiedURL := "https://mattermost.example.com/api/v4/image?url=https%3A%2F%2Fmattermost.com%2Fwp-content%2Fuploads%2F2022%2F02%2FlogoHorizontal.png"
proxiedURL := "https://mattermost.example.com/v1/team/image?url=https%3A%2F%2Fmattermost.com%2Fwp-content%2Fuploads%2F2022%2F02%2FlogoHorizontal.png"
proxy := ImageProxy{siteURL: parsedURL}
@@ -56,17 +56,17 @@ func TestGetProxiedImageURL(t *testing.T) {
{
Name: "should not bypass protocol relative URLs",
Input: "//mattermost.com/static/logo.png",
Expected: "https://mattermost.example.com/api/v4/image?url=https%3A%2F%2Fmattermost.com%2Fstatic%2Flogo.png",
Expected: "https://mattermost.example.com/v1/team/image?url=https%3A%2F%2Fmattermost.com%2Fstatic%2Flogo.png",
},
{
Name: "should not bypass if the host prefix is same",
Input: "https://mattermost.example.com.anothersite.com/static/logo.png",
Expected: "https://mattermost.example.com/api/v4/image?url=https%3A%2F%2Fmattermost.example.com.anothersite.com%2Fstatic%2Flogo.png",
Expected: "https://mattermost.example.com/v1/team/image?url=https%3A%2F%2Fmattermost.example.com.anothersite.com%2Fstatic%2Flogo.png",
},
{
Name: "should not bypass for user auth URLs",
Input: "https://[email protected]/static/logo.png",
Expected: "https://mattermost.example.com/api/v4/image?url=https%3A%2F%2Fmattermost.example.com%40anothersite.com%2Fstatic%2Flogo.png",
Expected: "https://mattermost.example.com/v1/team/image?url=https%3A%2F%2Fmattermost.example.com%40anothersite.com%2Fstatic%2Flogo.png",
},
{
Name: "should not proxy embedded image",
@@ -84,7 +84,7 @@ func TestGetUnproxiedImageURL(t *testing.T) {
siteURL := "https://mattermost.example.com"
imageURL := "https://hanzo.ai/wp-content/uploads/2022/02/logoHorizontal.png"
proxiedURL := "https://mattermost.example.com/api/v4/image?url=https%3A%2F%2Fmattermost.com%2Fwp-content%2Fuploads%2F2022%2F02%2FlogoHorizontal.png"
proxiedURL := "https://mattermost.example.com/v1/team/image?url=https%3A%2F%2Fmattermost.com%2Fwp-content%2Fuploads%2F2022%2F02%2FlogoHorizontal.png"
for _, test := range []struct {
Name string
@@ -43,7 +43,7 @@ func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name str
return nil, err
}
url := fmt.Sprintf("%s/%s", rcSaved.SiteURL, ConfirmInviteURL)
url := fmt.Sprintf("%s%s", rcSaved.SiteURL, ConfirmInviteURL)
// for the invite confirm message, we need to use the token that
// the originating server sent in the invite instead of the one
@@ -135,7 +135,7 @@ func (rcs *Service) pingRemote(rc *model.RemoteCluster) error {
if err != nil {
return err
}
url := fmt.Sprintf("%s/%s", rc.SiteURL, PingURL)
url := fmt.Sprintf("%s%s", rc.SiteURL, PingURL)
resp, err := rcs.sendFrameToRemote(PingTimeout, rc, frame, url)
if err != nil {
@@ -23,15 +23,15 @@ const (
ResultsChanBuffer = 50
ResultQueueDrainTimeoutMillis = 10000
MaxConcurrentSends = 10
SendMsgURL = "api/v4/remotecluster/msg"
SendMsgURL = model.APIURLSuffix + "/remotecluster/msg"
SendTimeout = time.Minute
SendFileTimeout = time.Minute * 5
PingURL = "api/v4/remotecluster/ping"
PingURL = model.APIURLSuffix + "/remotecluster/ping"
// Raising PingFreq also raises the transport's IdleConnTimeout (PingFreq / 2),
// which must stay below peer idle timeouts (60s default). See MM-69982.
PingFreq = time.Minute
PingTimeout = time.Second * 15
ConfirmInviteURL = "api/v4/remotecluster/confirm_invite"
ConfirmInviteURL = model.APIURLSuffix + "/remotecluster/confirm_invite"
InvitationTopic = "invitation"
PingTopic = "ping"
ResponseStatusOK = model.StatusOk
@@ -158,50 +158,50 @@ sequenceDiagram
participant SB as Server B
Note over UA,SB: Phase 1: Generate Invitation
UA->>SA: POST /api/v4/remotecluster<br/>{name, display_name, password}
UA->>SA: POST /v1/team/remotecluster<br/>{name, display_name, password}
SA->>SA: Generate RemoteId, Token
SA->>SA: Encrypt invitation (PBKDF2 + AES-GCM)
SA->>UA: Return encrypted invite code
Note over UA,SB: Phase 2: Accept Invitation
UA->>UB: Share invite code + password<br/>(out of band)
UB->>SB: POST /api/v4/remotecluster/accept_invite<br/>{invite, password}
UB->>SB: POST /v1/team/remotecluster/accept_invite<br/>{invite, password}
SB->>SB: Decrypt invitation
SB->>SB: Create RemoteCluster record
SB->>SA: POST /api/v4/remotecluster/confirm_invite<br/>X-MM-RemoteCluster-Token: [token]<br/>{remote_id, site_url, token}
SB->>SA: POST /v1/team/remotecluster/confirm_invite<br/>X-MM-RemoteCluster-Token: [token]<br/>{remote_id, site_url, token}
SA->>SA: Update RemoteCluster with SiteURL
SA->>SB: 200 OK
SB->>UB: Connection established
Note over UA,SB: Phase 3: Continuous Heartbeat
loop Every 60 seconds
SA->>SB: POST /api/v4/remotecluster/ping<br/>{sent_at}
SA->>SB: POST /v1/team/remotecluster/ping<br/>{sent_at}
SB->>SA: {sent_at, recv_at}
SB->>SA: POST /api/v4/remotecluster/ping<br/>{sent_at}
SB->>SA: POST /v1/team/remotecluster/ping<br/>{sent_at}
SA->>SB: {sent_at, recv_at}
end
```
**Step 1: Create Invitation (Server A)**
- API: `POST /api/v4/remotecluster`
- API: `POST /v1/team/remotecluster`
- Generates encrypted invitation with PBKDF2 encryption
- Returns base64-encoded invite code
- Creates pending RemoteCluster record
**Step 2: Accept Invitation (Server B)**
- API: `POST /api/v4/remotecluster/accept_invite`
- API: `POST /v1/team/remotecluster/accept_invite`
- Decrypts invitation using password
- Creates local RemoteCluster record
- Sends confirmation to Server A
**Step 3: Confirm Connection (Server A)**
- API: `POST /api/v4/remotecluster/confirm_invite`
- API: `POST /v1/team/remotecluster/confirm_invite`
- Receives confirmation from Server B
- Updates RemoteCluster with actual SiteURL
- Connection established
**Step 4: Continuous Heartbeat**
- API: `POST /api/v4/remotecluster/ping`
- API: `POST /v1/team/remotecluster/ping`
- Every 60 seconds (default)
- Updates `LastPingAt` timestamp
- Remote considered online if pinged within 5 minutes
@@ -219,7 +219,7 @@ sequenceDiagram
UA->>SA: Invite Remote B to Channel
SA->>SA: Create SharedChannel (Home=true)
SA->>SA: Create SharedChannelRemote
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_invite<br/>{channel_id, name, type, ...}
SA->>SB: POST /v1/team/remotecluster/msg<br/>Topic: sharedchannel_invite<br/>{channel_id, name, type, ...}
SB->>SB: Validate invitation
SB->>SB: Create local channel
SB->>SB: Create SharedChannel (Home=false)
@@ -231,7 +231,7 @@ sequenceDiagram
Note over UA,SB: Initial Sync
SA->>SA: Queue sync task for channel
SA->>SA: Collect users, posts, reactions
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_sync<br/>{users, posts, reactions, ...}
SA->>SB: POST /v1/team/remotecluster/msg<br/>Topic: sharedchannel_sync<br/>{users, posts, reactions, ...}
SB->>SB: Process sync message
SB->>SB: Create synthetic users
SB->>SB: Create posts
@@ -247,7 +247,7 @@ sequenceDiagram
- Contains channel metadata (name, type, permissions)
**Step 2: Receive Invitation (Server B)**
- API: `POST /api/v4/remotecluster/msg` (topic: `sharedchannel_invite`)
- API: `POST /v1/team/remotecluster/msg` (topic: `sharedchannel_invite`)
- Creates local channel (regular, DM, or GM)
- Creates SharedChannel record (`Home=false`)
- Creates SharedChannelRemote record
@@ -278,13 +278,13 @@ sequenceDiagram
SA->>SA: Filter & batch (100 posts max)
alt Has file attachments
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_upload<br/>{upload_session}
SA->>SB: POST /v1/team/remotecluster/msg<br/>Topic: sharedchannel_upload<br/>{upload_session}
SB->>SA: 200 OK {session_id}
SA->>SB: POST /api/v4/remotecluster/upload/{id}<br/>multipart file data
SA->>SB: POST /v1/team/remotecluster/upload/{id}<br/>multipart file data
SB->>SB: Save file to filestore
end
SA->>SB: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_sync<br/>Headers: X-MM-RemoteCluster-Id, Token<br/>{SyncMsg}
SA->>SB: POST /v1/team/remotecluster/msg<br/>Topic: sharedchannel_sync<br/>Headers: X-MM-RemoteCluster-Id, Token<br/>{SyncMsg}
Note over SB: Process Sync Message
SB->>SB: Validate auth token
@@ -302,7 +302,7 @@ sequenceDiagram
SB->>SB: Save post
SB->>UB: WebSocket: posted event
SB->>SB: Queue sync task
SB->>SA: POST /api/v4/remotecluster/msg<br/>Topic: sharedchannel_sync
SB->>SA: POST /v1/team/remotecluster/msg<br/>Topic: sharedchannel_sync
SA->>SA: Process sync message
SA->>UA: WebSocket: posted event
SA->>SB: 200 OK {timestamps}
@@ -346,7 +346,7 @@ graph LR
- Attachments: All for synced posts
**Send Sync Message (Server A → Server B)**
- API: `POST /api/v4/remotecluster/msg` (topic: `sharedchannel_sync`)
- API: `POST /v1/team/remotecluster/msg` (topic: `sharedchannel_sync`)
- Headers:
- `X-MM-RemoteCluster-Id`: Remote cluster ID
- `X-MM-RemoteCluster-Token`: Authentication token
@@ -380,14 +380,14 @@ graph LR
1. Server A creates upload session
2. Sends upload creation message (topic: `sharedchannel_upload`)
3. Server B creates matching session
4. Server A streams file data: `POST /api/v4/remotecluster/upload/{upload_id}`
4. Server A streams file data: `POST /v1/team/remotecluster/upload/{upload_id}`
5. Server B saves file and creates FileInfo record
#### 5. Profile Image Synchronization
**Upload Flow:**
1. Detect user image update (`LastPictureUpdate` changed)
2. Server A uploads image: `POST /api/v4/remotecluster/{user_id}/image`
2. Server A uploads image: `POST /v1/team/remotecluster/{user_id}/image`
3. Server B validates user belongs to remote
4. Saves image and invalidates cache
@@ -434,7 +434,7 @@ sequenceDiagram
SB->>SB: Store RemoteToken = Token_A
Note over SA,SB: Server A sends message to Server B
SA->>SB: POST /api/v4/remotecluster/msg<br/>X-MM-RemoteCluster-Id: RemoteId_B<br/>X-MM-RemoteCluster-Token: Token_B
SA->>SB: POST /v1/team/remotecluster/msg<br/>X-MM-RemoteCluster-Id: RemoteId_B<br/>X-MM-RemoteCluster-Token: Token_B
SB->>SB: Validate RemoteId_B exists
SB->>SB: Validate Token matches stored Token_B
alt Valid Token
@@ -444,7 +444,7 @@ sequenceDiagram
end
Note over SA,SB: Server B sends message to Server A
SB->>SA: POST /api/v4/remotecluster/msg<br/>X-MM-RemoteCluster-Id: RemoteId_A<br/>X-MM-RemoteCluster-Token: Token_A
SB->>SA: POST /v1/team/remotecluster/msg<br/>X-MM-RemoteCluster-Id: RemoteId_A<br/>X-MM-RemoteCluster-Token: Token_A
SA->>SA: Validate RemoteId_A exists
SA->>SA: Validate Token matches stored Token_A
alt Valid Token
@@ -473,20 +473,20 @@ sequenceDiagram
### Key API Endpoints
**Remote Cluster Management:**
- `POST /api/v4/remotecluster` - Create remote (generate invite)
- `POST /api/v4/remotecluster/accept_invite` - Accept invitation
- `POST /api/v4/remotecluster/confirm_invite` - Confirm connection
- `POST /api/v4/remotecluster/ping` - Heartbeat
- `POST /api/v4/remotecluster/msg` - Message delivery (all topics)
- `POST /v1/team/remotecluster` - Create remote (generate invite)
- `POST /v1/team/remotecluster/accept_invite` - Accept invitation
- `POST /v1/team/remotecluster/confirm_invite` - Confirm connection
- `POST /v1/team/remotecluster/ping` - Heartbeat
- `POST /v1/team/remotecluster/msg` - Message delivery (all topics)
**Shared Channel Management:**
- `GET /api/v4/sharedchannels/{team_id}` - List shared channels
- `POST /api/v4/channels/{channel_id}/remotes/{remote_id}/invite` - Share channel
- `POST /api/v4/channels/{channel_id}/remotes/{remote_id}/uninvite` - Unshare
- `GET /v1/team/sharedchannels/{team_id}` - List shared channels
- `POST /v1/team/channels/{channel_id}/remotes/{remote_id}/invite` - Share channel
- `POST /v1/team/channels/{channel_id}/remotes/{remote_id}/uninvite` - Unshare
**File Operations:**
- `POST /api/v4/remotecluster/upload/{upload_id}` - Upload file
- `POST /api/v4/remotecluster/{user_id}/image` - Upload profile image
- `POST /v1/team/remotecluster/upload/{upload_id}` - Upload file
- `POST /v1/team/remotecluster/{user_id}/image` - Upload profile image
### Error Handling & Monitoring
+11 -8
View File
@@ -52,10 +52,13 @@ const (
ClientDir = "client"
APIURLSuffixV1 = "/api/v1"
APIURLSuffixV4 = "/api/v4"
APIURLSuffixV5 = "/api/v5"
APIURLSuffix = APIURLSuffixV4
// APIURLSuffix is the base path of this server's HTTP API.
APIURLSuffix = "/v1/team"
// PushURLSuffix is the base path of the external push notification
// service addressed by EmailSettings.PushNotificationServer. It is that
// service's versioning, not ours, and moves only when it does.
PushURLSuffix = "/api/v1"
)
type Response struct {
@@ -68,7 +71,7 @@ type Response struct {
type Client4 struct {
URL string // The location of the server, for example "http://localhost:8065"
APIURL string // The api location of the server, for example "http://localhost:8065/api/v4"
APIURL string // The api location of the server, for example "http://localhost:8065/v1/team"
HTTPClient *http.Client // The http client
AuthToken string
AuthType string
@@ -5633,7 +5636,7 @@ func (c *Client4) AuthorizeOAuthApp(ctx context.Context, authRequest *AuthorizeR
if err != nil {
return "", nil, err
}
// The request doesn't go to the /api/v4 subpath, so we can't use the usual helper methods
// The request doesn't go to the /v1/team subpath, so we can't use the usual helper methods
r, err := c.doAPIRequestBytes(ctx, http.MethodPost, c.URL+"/oauth/authorize", buf, "")
if err != nil {
return "", BuildResponse(r), err
@@ -5654,7 +5657,7 @@ func (c *Client4) DeauthorizeOAuthApp(ctx context.Context, appId string) (*Respo
if err != nil {
return nil, err
}
// The request doesn't go to the /api/v4 subpath, so we can't use the usual helper methods
// The request doesn't go to the /v1/team subpath, so we can't use the usual helper methods
r, err := c.doAPIRequestBytes(ctx, http.MethodPost, c.URL+"/oauth/deauthorize", buf, "")
if err != nil {
return BuildResponse(r), err
@@ -5665,7 +5668,7 @@ func (c *Client4) DeauthorizeOAuthApp(ctx context.Context, appId string) (*Respo
// GetOAuthAccessToken is a test helper function for the OAuth access token endpoint.
func (c *Client4) GetOAuthAccessToken(ctx context.Context, data url.Values) (*AccessResponse, *Response, error) {
// The request doesn't go to the /api/v4 subpath, so we can't use the usual helper methods
// The request doesn't go to the /v1/team subpath, so we can't use the usual helper methods
r, err := c.doAPIRequestReader(ctx, http.MethodPost, c.URL+"/oauth/access_token", "application/x-www-form-urlencoded", strings.NewReader(data.Encode()), nil)
if err != nil {
return nil, BuildResponse(r), err
+1 -1
View File
@@ -170,7 +170,7 @@ func TestClient4LookupInteractiveDialog(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request method and endpoint
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/v4/actions/dialogs/lookup", r.URL.Path)
assert.Equal(t, "/v1/team/actions/dialogs/lookup", r.URL.Path)
// Decode and verify request body
var submission model.SubmitDialogRequest
+1 -1
View File
@@ -33,7 +33,7 @@ const (
OAuthAuthorizeEndpoint = "/oauth/authorize"
OAuthAccessTokenEndpoint = "/oauth/access_token"
OAuthDeauthorizeEndpoint = "/oauth/deauthorize"
OAuthAppsRegisterEndpoint = "/api/v4/oauth/apps/register"
OAuthAppsRegisterEndpoint = APIURLSuffix + "/oauth/apps/register"
OAuthMetadataEndpoint = "/.well-known/oauth-authorization-server"
)
+1 -1
View File
@@ -38,7 +38,7 @@ var (
// audit level and should be applied whenever a REST API endpoint is accessed. It provides
// a record of API usage patterns and access.
//
// Example usage: Logging when a user accesses the /api/v4/posts endpoint.
// Example usage: Logging when a user accesses the /v1/team/posts endpoint.
LvlAuditAPI = Level{ID: 100, Name: "audit-api"}
// LvlAuditContent is used for auditing content-generating operations such as creating posts,