Files
team/server/channels/api4/channel_join_request.go

294 lines
9.4 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
// initChannelJoinRequestRoutes registers the discoverable-private-channel
// join request endpoints. The route group is split into its own file so the
// handlers stay isolated from the rest of api4/channel.go.
func (api *API) initChannelJoinRequestRoutes() {
if !api.srv.Config().FeatureFlags.DiscoverableChannels {
return
}
api.BaseRoutes.Channel.Handle("/join_request", api.APISessionRequired(requestJoinChannel)).Methods(http.MethodPost)
api.BaseRoutes.Channel.Handle("/join_request", api.APISessionRequired(getMyChannelJoinRequest)).Methods(http.MethodGet)
api.BaseRoutes.Channel.Handle("/join_request", api.APISessionRequired(withdrawMyChannelJoinRequest)).Methods(http.MethodDelete)
api.BaseRoutes.Channel.Handle("/join_requests", api.APISessionRequired(getChannelJoinRequests)).Methods(http.MethodGet)
api.BaseRoutes.Channel.Handle("/join_requests/count", api.APISessionRequired(countPendingChannelJoinRequests)).Methods(http.MethodGet)
api.BaseRoutes.Channel.Handle("/join_requests/{request_id:[A-Za-z0-9]+}", api.APISessionRequired(patchChannelJoinRequest)).Methods(http.MethodPatch)
api.BaseRoutes.User.Handle("/channel_join_requests", api.APISessionRequired(getMyChannelJoinRequests)).Methods(http.MethodGet)
}
// channelJoinRequestBody is the POST body shape for /channels/{id}/join_request.
type channelJoinRequestBody struct {
Message string `json:"message"`
}
func requireDiscoverableChannelsEnabled(c *Context, where string) bool {
if !c.App.Config().FeatureFlags.DiscoverableChannels {
c.Err = model.NewAppError(where, "api.channel.discoverable_join_request.feature_disabled.app_error", nil, "", http.StatusNotFound)
return false
}
return true
}
func requestJoinChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !requireDiscoverableChannelsEnabled(c, "requestJoinChannel") {
return
}
var body channelJoinRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
c.SetInvalidParamWithErr("body", err)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventCreateChannelJoinRequest, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.AppContext.Session().UserId)
joined, req, appErr := c.App.RequestJoinChannel(c.AppContext, c.AppContext.Session().UserId, c.Params.ChannelId, body.Message)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
if req != nil {
auditRec.AddEventResultState(req)
}
if joined {
// Mirror the membership endpoint's "no body, just status" semantics
// when the user was added directly via the ABAC fast path.
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(map[string]string{"status": model.ChannelJoinRequestStatusApproved}); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(req); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getMyChannelJoinRequest(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !requireDiscoverableChannelsEnabled(c, "getMyChannelJoinRequest") {
return
}
req, appErr := c.App.GetMyChannelJoinRequest(c.AppContext, c.AppContext.Session().UserId, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
if req == nil {
// Mirror REST conventions: not-found instead of an explicit `null`
// so clients can distinguish "no pending request" from "service down".
w.WriteHeader(http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(req); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func withdrawMyChannelJoinRequest(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !requireDiscoverableChannelsEnabled(c, "withdrawMyChannelJoinRequest") {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventWithdrawChannelJoinRequest, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
model.AddEventParameterToAuditRec(auditRec, "user_id", c.AppContext.Session().UserId)
req, appErr := c.App.GetMyChannelJoinRequest(c.AppContext, c.AppContext.Session().UserId, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
if req == nil {
c.Err = model.NewAppError("withdrawMyChannelJoinRequest", "app.channel.join_request.not_found.app_error", nil, "channel_id="+c.Params.ChannelId, http.StatusNotFound)
return
}
updated, appErr := c.App.WithdrawChannelJoinRequest(c.AppContext, req.Id, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(updated)
if err := json.NewEncoder(w).Encode(updated); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getChannelJoinRequests(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !requireDiscoverableChannelsEnabled(c, "getChannelJoinRequests") {
return
}
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelJoinRequests); !ok {
c.SetPermissionError(model.PermissionManageChannelJoinRequests)
return
}
opts := model.GetChannelJoinRequestsOpts{
Status: r.URL.Query().Get("status"),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
}
list, appErr := c.App.GetChannelJoinRequests(c.AppContext, c.Params.ChannelId, opts)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(list); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func countPendingChannelJoinRequests(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !requireDiscoverableChannelsEnabled(c, "countPendingChannelJoinRequests") {
return
}
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelJoinRequests); !ok {
c.SetPermissionError(model.PermissionManageChannelJoinRequests)
return
}
count, appErr := c.App.CountPendingChannelJoinRequests(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(map[string]int64{"count": count}); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchChannelJoinRequest(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
if !requireDiscoverableChannelsEnabled(c, "patchChannelJoinRequest") {
return
}
if !model.IsValidId(c.Params.RequestId) {
c.SetInvalidURLParam("request_id")
return
}
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelJoinRequests); !ok {
c.SetPermissionError(model.PermissionManageChannelJoinRequests)
return
}
var patch model.ChannelJoinRequestPatch
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
c.SetInvalidParamWithErr("channel_join_request_patch", err)
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUpdateChannelJoinRequest, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
model.AddEventParameterToAuditRec(auditRec, "request_id", c.Params.RequestId)
model.AddEventParameterToAuditRec(auditRec, "status", patch.Status)
// Capture only the presence of a denial reason in the audit log; the
// free-text contents are intentionally excluded.
model.AddEventParameterToAuditRec(auditRec, "has_denial_reason", strconv.FormatBool(patch.DenialReason != nil && *patch.DenialReason != ""))
updated, appErr := c.App.UpdateChannelJoinRequest(c.AppContext, c.Params.RequestId, c.Params.ChannelId, &patch, c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(updated)
if err := json.NewEncoder(w).Encode(updated); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getMyChannelJoinRequests(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !requireDiscoverableChannelsEnabled(c, "getMyChannelJoinRequests") {
return
}
// Only the calling user can list their own requests; admins should use
// the per-channel queue endpoint.
if c.Params.UserId != c.AppContext.Session().UserId {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
opts := model.GetChannelJoinRequestsOpts{
Status: r.URL.Query().Get("status"),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
}
list, appErr := c.App.GetMyChannelJoinRequests(c.AppContext, c.AppContext.Session().UserId, opts)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(list); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}