mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 13:08:56 -04:00
* Fix nil pointer dereference in UpdateUser after store update Add nil check on userUpdate result from userService.UpdateUser to prevent panic when the store returns nil unexpectedly. This fixes a nil pointer dereference that occurs when accessing userUpdate.New after the store update call. Sentry: MATTERMOST-SERVER-VF (14 events) Co-authored-by: Claude <claude@anthropic.com> * Add unit test for nil userUpdate guard in UpdateUser Test verifies that when the store returns (nil, nil) from Update, the app layer returns an appropriate error instead of panicking with a nil pointer dereference. Co-authored-by: Claude <claude@anthropic.com> * fix: gofmt user_test.go Co-authored-by: Claude <claude@anthropic.com> * fix: split nil checks per review feedback, add parallel test execution Separate userUpdate==nil from userUpdate.New==nil with distinct error detail strings for easier debugging. Add mainHelper.Parallel(t) to test for consistency with other mock-based tests. Addresses review feedback from @JulienTant and @coderabbitai. Co-authored-by: Claude <claude@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com>
3208 lines
117 KiB
Go
3208 lines
117 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/email"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/password/hashers"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/users"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
|
"github.com/mattermost/mattermost/server/v8/platform/shared/mfa"
|
|
)
|
|
|
|
const (
|
|
ImageProfilePixelDimension = 128
|
|
)
|
|
|
|
func (a *App) CreateUserWithToken(rctx request.CTX, user *model.User, token *model.Token) (*model.User, *model.AppError) {
|
|
if err := a.IsUserSignUpAllowed(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !token.IsInvitationToken() {
|
|
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_invalid.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if token.IsExpired() {
|
|
if appErr := a.DeleteToken(token); appErr != nil {
|
|
rctx.Logger().Warn("Error while deleting expired signup-invite token", mlog.Err(appErr))
|
|
}
|
|
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.signup_link_expired.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
|
|
|
|
team, nErr := a.Srv().Store().Team().Get(tokenData["teamId"])
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("CreateUserWithToken", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
// find the sender id and grab the channels in order to validate
|
|
// the sender id still belongs to team and to private channels
|
|
senderId := tokenData["senderId"]
|
|
channelIds := strings.Split(tokenData["channels"], " ")
|
|
|
|
// filter the channels the original inviter has still permissions over
|
|
channelIds = a.ValidateUserPermissionsOnChannels(rctx, senderId, channelIds)
|
|
|
|
channels, nErr := a.Srv().Store().Channel().GetChannelsByIds(channelIds, false)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("CreateUserWithToken", "app.channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
emailFromToken := tokenData["email"]
|
|
if emailFromToken != user.Email {
|
|
return nil, model.NewAppError("CreateUserWithToken", "api.user.create_user.bad_token_email_data.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
user.Email = tokenData["email"]
|
|
user.EmailVerified = true
|
|
|
|
var ruser *model.User
|
|
var err *model.AppError
|
|
if token.Type == model.TokenTypeTeamInvitation {
|
|
ruser, err = a.CreateUser(rctx, user)
|
|
} else {
|
|
ruser, err = a.CreateGuest(rctx, user)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := a.JoinUserToTeam(rctx, team, ruser, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if appErr := a.AddDirectChannels(rctx, team.Id, ruser); appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if token.Type == model.TokenTypeGuestInvitation || token.Type == model.TokenTypeGuestMagicLinkInvitation || (token.Type == model.TokenTypeTeamInvitation && len(channels) > 0) {
|
|
for _, channel := range channels {
|
|
_, err := a.AddChannelMember(rctx, ruser.Id, channel, ChannelMemberOpts{})
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to add channel member", mlog.Err(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := a.DeleteToken(token); err != nil {
|
|
rctx.Logger().Warn("Error while deleting token", mlog.Err(err))
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
// AuthenticateUserForGuestMagicLink validates an guest magic link token and creates a guest user.
|
|
// This function handles the passwordless "guest magic link" flow where clicking an email link logs the user in.
|
|
// Follows the same pattern as SAML/OAuth SSO by creating the user then calling AddUserToTeamByToken.
|
|
func (a *App) AuthenticateUserForGuestMagicLink(rctx request.CTX, tokenString string) (*model.User, *model.AppError) {
|
|
// Atomically consume the token to prevent race conditions where concurrent
|
|
// requests could reuse the same single-use token to create multiple sessions.
|
|
// Try both valid token types for guest magic links.
|
|
token, err := a.ConsumeTokenOnce(model.TokenTypeGuestMagicLinkInvitation, tokenString)
|
|
if err != nil {
|
|
token, err = a.ConsumeTokenOnce(model.TokenTypeGuestMagicLink, tokenString)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.invalid_token.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if token.IsExpired() {
|
|
return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.expired_token.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// Extract email from token
|
|
tokenData := model.MapFromJSON(strings.NewReader(token.Extra))
|
|
email := tokenData["email"]
|
|
|
|
// Check if user already exists
|
|
existingUser, getUserErr := a.GetUserByEmail(email)
|
|
|
|
// Handle login-only tokens (TokenTypeGuestMagicLink) - for existing users only
|
|
if token.Type == model.TokenTypeGuestMagicLink {
|
|
if getUserErr != nil || existingUser == nil {
|
|
// Return generic error to prevent user enumeration
|
|
return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.invalid_token.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
return existingUser, nil
|
|
}
|
|
|
|
// Handle invitation tokens (TokenTypeGuestMagicLinkInvitation) - create new guest user
|
|
if getUserErr == nil && existingUser != nil {
|
|
// Log the specific reason internally for debugging
|
|
rctx.Logger().Warn("Guest magic link invitation token attempted for existing user", mlog.String("email", email))
|
|
|
|
// Return generic error to prevent user enumeration - don't reveal that user exists
|
|
return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.invalid_token.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// Generate username from email
|
|
username := model.CleanUsername(rctx.Logger(), strings.Split(email, "@")[0])
|
|
// Ensure username is unique by appending random suffix if needed
|
|
// Try up to 10 times to find a unique username
|
|
originalUsername := username
|
|
usernameFound := false
|
|
for range 10 {
|
|
if _, usernameErr := a.GetUserByUsername(username); usernameErr != nil {
|
|
// Username is available
|
|
usernameFound = true
|
|
break
|
|
}
|
|
// Username exists, try with a random suffix (8 characters for better uniqueness)
|
|
username = originalUsername + model.NewId()[0:8]
|
|
}
|
|
|
|
if !usernameFound {
|
|
return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.username_generation_failed.app_error", nil, "could not generate unique username after 10 attempts", http.StatusInternalServerError)
|
|
}
|
|
|
|
// Create guest user with auto-generated username
|
|
user := &model.User{
|
|
Email: email,
|
|
EmailVerified: true,
|
|
Username: username,
|
|
AuthService: model.UserAuthServiceMagicLink,
|
|
}
|
|
|
|
guestUser, createErr := a.CreateGuest(rctx, user)
|
|
if createErr != nil {
|
|
return nil, createErr
|
|
}
|
|
|
|
// Add user to team and channels using the shared SSO pattern
|
|
// This handles team joining, channel assignment, and token deletion
|
|
if _, _, addErr := a.AddUserToTeamWithToken(rctx, guestUser.Id, token); addErr != nil {
|
|
return nil, addErr
|
|
}
|
|
|
|
return guestUser, nil
|
|
}
|
|
|
|
func (a *App) CreateUserWithInviteId(rctx request.CTX, user *model.User, inviteId, redirect string) (*model.User, *model.AppError) {
|
|
if err := a.IsUserSignUpAllowed(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
team, nErr := a.Srv().Store().Team().GetByInviteId(inviteId)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.get_by_invite_id.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if team.IsGroupConstrained() {
|
|
return nil, model.NewAppError("CreateUserWithInviteId", "app.team.invite_id.group_constrained.error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if !users.CheckUserDomain(user, team.AllowedDomains) {
|
|
return nil, model.NewAppError("CreateUserWithInviteId", "api.team.invite_members.invalid_email.app_error", map[string]any{"Addresses": team.AllowedDomains}, "", http.StatusForbidden)
|
|
}
|
|
|
|
user.EmailVerified = false
|
|
|
|
ruser, err := a.CreateUser(rctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := a.JoinUserToTeam(rctx, team, ruser, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if appErr := a.AddDirectChannels(rctx, team.Id, ruser); appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
|
rctx.Logger().Warn("Failed to send welcome email on create user with inviteId", mlog.Err(err))
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
func (a *App) CreateUserAsAdmin(rctx request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
|
|
ruser, err := a.CreateUser(rctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
|
rctx.Logger().Warn("Failed to send welcome email to the new user, created by system admin", mlog.Err(err))
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
func (a *App) CreateUserFromSignup(rctx request.CTX, user *model.User, redirect string) (*model.User, *model.AppError) {
|
|
if err := a.IsUserSignUpAllowed(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !a.IsFirstUserAccount() && !*a.Config().TeamSettings.EnableOpenServer {
|
|
err := model.NewAppError("CreateUserFromSignup", "api.user.create_user.no_open_server", nil, "email="+user.Email, http.StatusForbidden)
|
|
return nil, err
|
|
}
|
|
|
|
user.EmailVerified = false
|
|
|
|
ruser, err := a.CreateUser(rctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
|
rctx.Logger().Warn("Failed to send welcome email on create user from signup", mlog.Err(err))
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
func (a *App) IsUserSignUpAllowed() *model.AppError {
|
|
if !*a.Config().EmailSettings.EnableSignUpWithEmail || !*a.Config().TeamSettings.EnableUserCreation {
|
|
err := model.NewAppError("IsUserSignUpAllowed", "api.user.create_user.signup_email_disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) IsFirstUserAccount() bool {
|
|
return a.ch.srv.platform.IsFirstUserAccount()
|
|
}
|
|
|
|
// CreateUser creates a user and sets several fields of the returned User struct to
|
|
// their zero values.
|
|
func (a *App) CreateUser(rctx request.CTX, user *model.User) (*model.User, *model.AppError) {
|
|
return a.createUserOrGuest(rctx, user, false)
|
|
}
|
|
|
|
// CreateGuest creates a guest and sets several fields of the returned User struct to
|
|
// their zero values.
|
|
func (a *App) CreateGuest(rctx request.CTX, user *model.User) (*model.User, *model.AppError) {
|
|
return a.createUserOrGuest(rctx, user, true)
|
|
}
|
|
|
|
func (a *App) createUserOrGuest(rctx request.CTX, user *model.User, guest bool) (*model.User, *model.AppError) {
|
|
atUserLimit, limitErr := a.isAtUserLimit()
|
|
if limitErr != nil {
|
|
return nil, limitErr
|
|
}
|
|
|
|
if atUserLimit {
|
|
// Use different error messages based on whether server is licensed
|
|
if a.License() != nil {
|
|
return nil, model.NewAppError("createUserOrGuest", "api.user.create_user.license_user_limits.exceeded", nil, "", http.StatusBadRequest)
|
|
}
|
|
return nil, model.NewAppError("createUserOrGuest", "api.user.create_user.user_limits.exceeded", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if err := a.isUniqueToGroupNames(user.Username); err != nil {
|
|
err.Where = "createUserOrGuest"
|
|
return nil, err
|
|
}
|
|
|
|
ruser, nErr := a.ch.srv.userService.CreateUser(rctx, user, users.UserCreateOptions{Guest: guest})
|
|
if nErr != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
var nfErr *users.ErrInvalidPassword
|
|
switch {
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
case errors.Is(nErr, users.AcceptedDomainError):
|
|
return nil, model.NewAppError("createUserOrGuest", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("createUserOrGuest", nfErr.Id(), map[string]any{"Min": *a.Config().PasswordSettings.MinimumLength}, "", http.StatusBadRequest)
|
|
case errors.Is(nErr, users.UserStoreIsEmptyError):
|
|
return nil, model.NewAppError("createUserOrGuest", "app.user.store_is_empty.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
case errors.As(nErr, &invErr):
|
|
switch invErr.Field {
|
|
case "email":
|
|
return nil, model.NewAppError("createUserOrGuest", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case "username":
|
|
return nil, model.NewAppError("createUserOrGuest", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("createUserOrGuest", "app.user.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
default:
|
|
return nil, model.NewAppError("createUserOrGuest", "app.user.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
// We always invalidate the user because we actually need to invalidate
|
|
// in case the user's EmailVerified is true, but we also always need to invalidate
|
|
// the GetAllProfiles cache.
|
|
// To have a proper fix would mean duplicating the invalidation of GetAllProfiles
|
|
// everywhere else. Therefore, to keep things simple we always invalidate both caches here.
|
|
// The performance penalty for invalidating the UserById cache is nil because the user was just created.
|
|
a.InvalidateCacheForUser(ruser.Id)
|
|
|
|
if user.EmailVerified {
|
|
nUser, err := a.ch.srv.userService.GetUser(ruser.Id)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("createUserOrGuest", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("createUserOrGuest", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
a.sendUpdatedUserEvent(nUser)
|
|
}
|
|
|
|
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryRecommendedNextSteps, Name: model.PreferenceNameRecommendedNextStepsHide, Value: "false"}
|
|
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
|
|
gmASdmPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategorySystemNotice, Name: "GMasDM", Value: "true"}
|
|
|
|
preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref, gmASdmPref}
|
|
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
|
|
rctx.Logger().Warn("Encountered error saving user preferences", mlog.Err(err))
|
|
}
|
|
|
|
go a.UpdateViewedProductNoticesForNewUser(ruser.Id)
|
|
|
|
// This message goes to everyone, so the teamID, channelID and userID are irrelevant
|
|
message := model.NewWebSocketEvent(model.WebsocketEventNewUser, "", "", "", nil, "")
|
|
message.Add("user_id", ruser.Id)
|
|
a.Publish(message)
|
|
|
|
pluginContext := pluginContext(rctx)
|
|
a.Srv().Go(func() {
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.UserHasBeenCreated(pluginContext, ruser)
|
|
return true
|
|
}, plugin.UserHasBeenCreatedID)
|
|
})
|
|
|
|
userLimits, limitErr := a.GetServerLimits()
|
|
if limitErr != nil {
|
|
// we don't want to break the create user flow just because of this.
|
|
// So, we log the error, not return
|
|
rctx.Logger().Error("Error fetching user limits in createUserOrGuest", mlog.Err(limitErr))
|
|
} else {
|
|
if userLimits.MaxUsersLimit > 0 && userLimits.ActiveUserCount > userLimits.MaxUsersLimit {
|
|
// Use different warning messages based on whether server is licensed
|
|
if a.License() != nil {
|
|
rctx.Logger().Warn("ERROR_LICENSED_USERS_LIMIT_EXCEEDED: Created user exceeds the maximum licensed users.", mlog.Int("user_limit", userLimits.MaxUsersLimit))
|
|
} else {
|
|
rctx.Logger().Warn("ERROR_SAFETY_LIMITS_EXCEEDED: Created user exceeds the total activated users limit.", mlog.Int("user_limit", userLimits.MaxUsersLimit))
|
|
}
|
|
}
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
func (a *App) CreateOAuthUser(rctx request.CTX, service string, userData io.Reader, inviteToken string, inviteId string, tokenUser *model.User) (*model.User, *model.AppError) {
|
|
if !*a.Config().TeamSettings.EnableUserCreation {
|
|
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_user.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
provider, e := a.getSSOProvider(service)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
|
|
settings, err := provider.GetSSOSettings(rctx, a.Config(), service)
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateOAuthUser", "api.user.oauth.get_settings.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
user, err := provider.GetUserFromJSON(rctx, userData, tokenUser, settings)
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if user.AuthService == "" {
|
|
user.AuthService = service
|
|
}
|
|
|
|
found := true
|
|
count := 0
|
|
for found {
|
|
if found = a.ch.srv.userService.IsUsernameTaken(user.Username); found {
|
|
user.Username = user.Username + strconv.Itoa(count)
|
|
count++
|
|
}
|
|
}
|
|
|
|
userByAuth, _ := a.ch.srv.userService.GetUserByAuth(user.AuthData, service)
|
|
if userByAuth != nil {
|
|
return userByAuth, nil
|
|
}
|
|
|
|
userByEmail, _ := a.ch.srv.userService.GetUserByEmail(user.Email)
|
|
if userByEmail != nil {
|
|
if userByEmail.AuthService == "" {
|
|
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]any{"Service": service, "Auth": model.UserAuthServiceEmail}, "email="+user.Email, http.StatusBadRequest)
|
|
}
|
|
if provider.IsSameUser(rctx, userByEmail, user) {
|
|
if _, err = a.Srv().Store().User().UpdateAuthData(userByEmail.Id, user.AuthService, user.AuthData, "", false); err != nil {
|
|
// if the user is not updated, write a warning to the log, but don't prevent user login
|
|
rctx.Logger().Warn("Error attempting to update user AuthData", mlog.Err(err))
|
|
}
|
|
return userByEmail, nil
|
|
}
|
|
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]any{"Service": service, "Auth": userByEmail.AuthService}, "email="+user.Email+" authData="+*user.AuthData, http.StatusBadRequest)
|
|
}
|
|
|
|
user.EmailVerified = true
|
|
|
|
ruser, appErr := a.CreateUser(rctx, user)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if appErr = a.AddUserToTeamByInviteIfNeeded(rctx, ruser, inviteToken, inviteId); appErr != nil {
|
|
rctx.Logger().Warn("Failed to add user to team", mlog.Err(appErr))
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
func (a *App) AddUserToTeamByInviteIfNeeded(rctx request.CTX, user *model.User, inviteToken string, inviteId string) *model.AppError {
|
|
var team *model.Team
|
|
var err *model.AppError
|
|
|
|
if inviteToken != "" {
|
|
team, _, err = a.AddUserToTeamByToken(rctx, user.Id, inviteToken)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to add user to team using invite token", mlog.Err(err))
|
|
return err
|
|
}
|
|
} else if inviteId != "" {
|
|
team, _, err = a.AddUserToTeamByInviteId(rctx, inviteId, user.Id)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to add user to team using invite ID", mlog.Err(err))
|
|
return err
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
if team != nil {
|
|
if err = a.AddDirectChannels(rctx, team.Id, user); err != nil {
|
|
rctx.Logger().Warn("Failed to add direct channels", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetUser(userID string) (*model.User, *model.AppError) {
|
|
user, err := a.ch.srv.userService.GetUser(userID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetUser", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (a *App) GetUsers(rctx request.CTX, userIDs []string) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsers(rctx, userIDs)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUserByUsername(username string) (*model.User, *model.AppError) {
|
|
result, err := a.ch.srv.userService.GetUserByUsername(username)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetUserByUsername", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *App) GetUserByEmail(email string) (*model.User, *model.AppError) {
|
|
user, err := a.ch.srv.userService.GetUserByEmail(email)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetUserByEmail", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (a *App) GetUserByRemoteID(remoteID string) (*model.User, *model.AppError) {
|
|
user, err := a.ch.srv.userService.GetUserByRemoteID(remoteID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetUserByRemoteID", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetUserByRemoteID", MissingAccountError, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (a *App) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
|
|
user, err := a.ch.srv.userService.GetUserByAuth(authData, authService)
|
|
if err != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetUserByAuth", MissingAuthAccountError, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetUserByAuth", "app.user.get_by_auth.other.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (a *App) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersFromProfiles(options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersPage(options, asAdmin)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersEtag(restrictionsHash string) string {
|
|
return a.ch.srv.userService.GetUsersEtag(restrictionsHash)
|
|
}
|
|
|
|
func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersInTeam(options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersInTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersNotInTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersInTeamPage(options, asAdmin)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersInTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersNotInTeamPage(teamID, groupConstrained, page*perPage, perPage, asAdmin, viewRestrictions)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersNotInTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
|
|
return a.ch.srv.userService.GetUsersInTeamEtag(teamID, restrictionsHash)
|
|
}
|
|
|
|
func (a *App) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
|
|
return a.ch.srv.userService.GetUsersNotInTeamEtag(teamID, restrictionsHash)
|
|
}
|
|
|
|
func (a *App) GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetProfilesInChannel(options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersInChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersInChannelByStatus(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetProfilesInChannelByStatus(options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersInChannelByStatus", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetProfilesInChannelByAdmin(options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersInChannelByAdmin", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersInChannelMap(options *model.UserGetOptions, asAdmin bool) (map[string]*model.User, *model.AppError) {
|
|
users, err := a.GetUsersInChannel(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userMap := make(map[string]*model.User, len(users))
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, asAdmin)
|
|
userMap[user.Id] = user
|
|
}
|
|
|
|
return userMap, nil
|
|
}
|
|
|
|
func (a *App) GetUsersInChannelPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
|
|
users, err := a.GetUsersInChannel(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersInChannelPageByStatus(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
|
|
users, err := a.GetUsersInChannelByStatus(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersInChannelPageByAdmin(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
|
|
users, err := a.GetUsersInChannelByAdmin(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersNotInChannel(teamID string, channelID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetProfilesNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersNotInChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersNotInChannelMap(teamID string, channelID string, groupConstrained bool, offset int, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) (map[string]*model.User, *model.AppError) {
|
|
users, err := a.GetUsersNotInChannel(teamID, channelID, groupConstrained, offset, limit, viewRestrictions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userMap := make(map[string]*model.User, len(users))
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, asAdmin)
|
|
userMap[user.Id] = user
|
|
}
|
|
|
|
return userMap, nil
|
|
}
|
|
|
|
func (a *App) GetUsersNotInChannelPage(teamID string, channelID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
|
|
users, err := a.GetUsersNotInChannel(teamID, channelID, groupConstrained, page*perPage, perPage, viewRestrictions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersNotInAbacChannel(rctx request.CTX, teamID string, channelID string, groupConstrained bool, cursorID string, limit int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
|
|
// Get the AccessControl service
|
|
acs := a.Srv().Channels().AccessControl
|
|
if acs == nil {
|
|
return nil, model.NewAppError("GetUsersNotInAbacChannel", "api.user.get_users_not_in_abac_channel.access_control_unavailable.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
// Use cursor-based pagination for ABAC channels
|
|
users, _, appErr := acs.QueryUsersForResource(rctx, channelID, "*", model.SubjectSearchOptions{
|
|
TeamID: teamID,
|
|
Limit: limit,
|
|
Cursor: model.SubjectCursor{
|
|
TargetID: cursorID, // Empty string means start from beginning
|
|
},
|
|
})
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersWithoutTeamPage(options, asAdmin)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersWithoutTeamPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersWithoutTeam(options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersWithoutTeam", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// GetTeamGroupUsers returns the users who are associated to the team via GroupTeams and GroupMembers.
|
|
func (a *App) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetTeamGroupUsers(teamID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetTeamGroupUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// GetChannelGroupUsers returns the users who are associated to the channel via GroupChannels and GroupMembers.
|
|
func (a *App) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetChannelGroupUsers(channelID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelGroupUsers", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersByIds(rctx request.CTX, userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersByIds(rctx, userIDs, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersByIds", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) GetUsersByGroupChannelIds(rctx request.CTX, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) {
|
|
usersByChannelId, err := a.Srv().Store().User().GetProfileByGroupChannelIdsForUser(rctx.Session().UserId, channelIDs)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersByGroupChannelIds", "app.user.get_profile_by_group_channel_ids_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
for channelID, userList := range usersByChannelId {
|
|
usersByChannelId[channelID] = a.sanitizeProfiles(userList, asAdmin)
|
|
}
|
|
|
|
return usersByChannelId, nil
|
|
}
|
|
|
|
func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
|
|
users, err := a.ch.srv.userService.GetUsersByUsernames(usernames, &model.UserGetOptions{ViewRestrictions: viewRestrictions})
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersByUsernames", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return a.sanitizeProfiles(users, asAdmin), nil
|
|
}
|
|
|
|
func (a *App) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User {
|
|
for _, u := range users {
|
|
a.SanitizeProfile(u, asAdmin)
|
|
}
|
|
|
|
return users
|
|
}
|
|
|
|
func (a *App) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
|
|
user, appErr := a.GetUser(userID)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
|
|
return nil, model.NewAppError("GenerateMfaSecret", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
mfaSecret, err := a.ch.srv.userService.GenerateMfaSecret(user)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GenerateMfaSecret", "mfa.generate_qr_code.create_code.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return mfaSecret, nil
|
|
}
|
|
|
|
func (a *App) ActivateMfa(userID, token string) *model.AppError {
|
|
user, appErr := a.GetUser(userID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if user.AuthService != "" && user.AuthService != model.UserAuthServiceLdap {
|
|
return model.NewAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
|
|
return model.NewAppError("ActivateMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if err := a.ch.srv.userService.ActivateMfa(user, token); err != nil {
|
|
switch {
|
|
case errors.Is(err, mfa.InvalidToken):
|
|
return model.NewAppError("ActivateMfa", "mfa.activate.bad_token.app_error", nil, "", http.StatusUnauthorized)
|
|
default:
|
|
return model.NewAppError("ActivateMfa", "mfa.activate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
// Make sure old MFA status is not cached locally or in cluster nodes.
|
|
a.InvalidateCacheForUser(userID)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) DeactivateMfa(userID string) *model.AppError {
|
|
user, appErr := a.GetUser(userID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if err := a.ch.srv.userService.DeactivateMfa(user); err != nil {
|
|
return model.NewAppError("DeactivateMfa", "mfa.deactivate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// Make sure old MFA status is not cached locally or in cluster nodes.
|
|
a.InvalidateCacheForUser(userID)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetProfileImagePaths returns the paths to the profile images for the given user IDs if such a profile image exists.
|
|
func (a *App) GetProfileImagePath(user *model.User) (string, *model.AppError) {
|
|
path := getProfileImagePath(user.Id)
|
|
exist, err := a.ch.srv.FileBackend().FileExists(path)
|
|
if err != nil {
|
|
return "", model.NewAppError(
|
|
"GetProfileImagePath",
|
|
"api.user.get_profile_image_path.app_error",
|
|
nil,
|
|
"",
|
|
http.StatusInternalServerError,
|
|
).Wrap(err)
|
|
}
|
|
if !exist {
|
|
return "", nil
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
|
|
return a.ch.srv.GetProfileImage(user)
|
|
}
|
|
|
|
func (a *App) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
|
|
return a.ch.srv.GetDefaultProfileImage(user)
|
|
}
|
|
|
|
func (a *App) UpdateDefaultProfileImage(rctx request.CTX, user *model.User) *model.AppError {
|
|
img, appErr := a.GetDefaultProfileImage(user)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
path := getProfileImagePath(user.Id)
|
|
if _, err := a.WriteFile(bytes.NewReader(img), path); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.Srv().Store().User().ResetLastPictureUpdate(user.Id); err != nil {
|
|
rctx.Logger().Warn("Failed to reset last picture update", mlog.Err(err))
|
|
}
|
|
|
|
a.InvalidateCacheForUser(user.Id)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SetDefaultProfileImage(rctx request.CTX, user *model.User) *model.AppError {
|
|
if err := a.UpdateDefaultProfileImage(rctx, user); err != nil {
|
|
rctx.Logger().Error("Failed to update default profile image for user", mlog.String("user_id", user.Id), mlog.Err(err))
|
|
return err
|
|
}
|
|
|
|
updatedUser, appErr := a.GetUser(user.Id)
|
|
if appErr != nil {
|
|
rctx.Logger().Warn("Error in getting users profile forcing logout", mlog.String("user_id", user.Id), mlog.Err(appErr))
|
|
return nil
|
|
}
|
|
|
|
options := a.Config().GetSanitizeOptions()
|
|
updatedUser.SanitizeProfile(options, false)
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
|
|
message.Add("user", updatedUser)
|
|
a.Publish(message)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SetProfileImage(rctx request.CTX, userID string, imageData *multipart.FileHeader) *model.AppError {
|
|
file, err := imageData.Open()
|
|
if err != nil {
|
|
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
defer file.Close()
|
|
return a.SetProfileImageFromMultiPartFile(rctx, userID, file)
|
|
}
|
|
|
|
func (a *App) SetProfileImageFromMultiPartFile(rctx request.CTX, userID string, file multipart.File) *model.AppError {
|
|
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
|
|
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest).Wrap(limitErr)
|
|
}
|
|
|
|
return a.SetProfileImageFromFile(rctx, userID, file)
|
|
}
|
|
|
|
func (a *App) AdjustImage(rctx request.CTX, file io.ReadSeeker) (*bytes.Buffer, *model.AppError) {
|
|
// Decode image into Image object
|
|
img, format, err := a.ch.imgDecoder.Decode(file)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
// Decode() reads the file to EOF; seek back to beginning so GetImageOrientation
|
|
// can read the EXIF data to determine the correct orientation.
|
|
if _, seekErr := file.Seek(0, io.SeekStart); seekErr != nil {
|
|
rctx.Logger().Warn("Failed to seek image file for orientation check", mlog.Err(seekErr))
|
|
}
|
|
|
|
orientation, err := imaging.GetImageOrientation(file, format)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to get image orientation", mlog.Err(err))
|
|
}
|
|
|
|
img = imaging.MakeImageUpright(img, orientation)
|
|
|
|
// Scale profile image
|
|
profileWidthAndHeight := 128
|
|
img = imaging.FillCenter(img, profileWidthAndHeight, profileWidthAndHeight)
|
|
|
|
buf := new(bytes.Buffer)
|
|
err = a.ch.imgEncoder.EncodePNG(buf, img)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func (a *App) SetProfileImageFromFile(rctx request.CTX, userID string, file io.ReadSeeker) *model.AppError {
|
|
buf, err := a.AdjustImage(rctx, file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := getProfileImagePath(userID)
|
|
if storedData, err := a.ReadFile(path); err == nil && bytes.Equal(storedData, buf.Bytes()) {
|
|
return nil
|
|
}
|
|
|
|
if _, err := a.WriteFile(buf, path); err != nil {
|
|
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().User().UpdateLastPictureUpdate(userID); err != nil {
|
|
rctx.Logger().Warn("Error with updating last picture update", mlog.Err(err))
|
|
}
|
|
a.invalidateUserCacheAndPublish(rctx, userID)
|
|
a.onUserProfileChange(userID)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdatePasswordAsUser(rctx request.CTX, userID, currentPassword, newPassword string) *model.AppError {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if user == nil {
|
|
return model.NewAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if user.AuthData != nil && *user.AuthData != "" {
|
|
return model.NewAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService, http.StatusBadRequest)
|
|
}
|
|
|
|
if user.IsMagicLinkEnabled() {
|
|
return model.NewAppError("updatePassword", "api.user.update_password.magic_link.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if err := a.DoubleCheckPassword(rctx, user, currentPassword); err != nil {
|
|
if err.Id == "api.user.check_user_password.invalid.app_error" {
|
|
err = model.NewAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
return err
|
|
}
|
|
|
|
T := i18n.GetUserTranslations(user.Locale)
|
|
|
|
return a.UpdatePasswordSendEmail(rctx, user, newPassword, T("api.user.update_password.menu"))
|
|
}
|
|
|
|
func (a *App) userDeactivated(rctx request.CTX, userID string) *model.AppError {
|
|
a.SetStatusOffline(userID, false, true)
|
|
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// when disable a user, userDeactivated is called for the user and the
|
|
// bots the user owns. Only notify once, when the user is the owner, not the
|
|
// owners bots
|
|
if !user.IsBot {
|
|
if appErr := a.notifySysadminsBotOwnerDeactivated(rctx, userID); appErr != nil {
|
|
rctx.Logger().Warn("Error while notifying the system admin that the owner of bot accounts got disabled", mlog.Err(appErr))
|
|
}
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated {
|
|
if appErr := a.disableUserBots(rctx, userID); appErr != nil {
|
|
rctx.Logger().Warn("Error while disabling all bots owned by the deactivated user", mlog.Err(appErr))
|
|
}
|
|
}
|
|
|
|
if nErr := a.Srv().Store().OAuth().RemoveAuthDataByUserId(userID); nErr != nil {
|
|
rctx.Logger().Warn("unable to remove auth data by user id", mlog.Err(nErr))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) invalidateUserChannelMembersCaches(rctx request.CTX, userID string) *model.AppError {
|
|
teamsForUser, err := a.GetTeamsForUser(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, team := range teamsForUser {
|
|
channelsForUser, err := a.GetChannelsForTeamForUser(rctx, team.Id, userID, &model.ChannelSearchOpts{
|
|
IncludeDeleted: false,
|
|
LastDeleteAt: 0,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, channel := range channelsForUser {
|
|
a.invalidateCacheForChannelMembers(channel.Id)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateActive(rctx request.CTX, user *model.User, active bool) (*model.User, *model.AppError) {
|
|
if active {
|
|
atUserLimit, appErr := a.isAtUserLimit()
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if atUserLimit {
|
|
// Use different error messages based on whether server is licensed
|
|
if a.License() != nil {
|
|
return nil, model.NewAppError("UpdateActive", "app.user.update_active.license_user_limit.exceeded", nil, "", http.StatusBadRequest)
|
|
}
|
|
return nil, model.NewAppError("UpdateActive", "app.user.update_active.user_limit.exceeded", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
user.UpdateAt = model.GetMillis()
|
|
if active {
|
|
user.DeleteAt = 0
|
|
} else {
|
|
user.DeleteAt = user.UpdateAt
|
|
}
|
|
|
|
userUpdate, err := a.ch.srv.userService.UpdateUser(rctx, user, true)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("UpdateActive", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("UpdateActive", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
ruser := userUpdate.New
|
|
a.InvalidateCacheForUser(user.Id)
|
|
|
|
if !active {
|
|
if err := a.RevokeAllSessions(rctx, ruser.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := a.userDeactivated(rctx, ruser.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if appErr := a.invalidateUserChannelMembersCaches(rctx, user.Id); appErr != nil {
|
|
rctx.Logger().Warn("Error while invalidating user channel members caches", mlog.Err(appErr))
|
|
}
|
|
a.sendUpdatedUserEvent(ruser)
|
|
|
|
if !active && user.DeleteAt != 0 {
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(rctx)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.UserHasBeenDeactivated(pluginContext, user)
|
|
return true
|
|
}, plugin.UserHasBeenDeactivatedID)
|
|
})
|
|
}
|
|
|
|
if active {
|
|
userLimits, appErr := a.GetServerLimits()
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Error fetching user limits in UpdateActive", mlog.Err(appErr))
|
|
} else {
|
|
if userLimits.MaxUsersLimit > 0 && userLimits.ActiveUserCount > userLimits.MaxUsersLimit {
|
|
// Use different warning messages based on whether server is licensed
|
|
if a.License() != nil {
|
|
rctx.Logger().Warn("ERROR_LICENSED_USERS_LIMIT_EXCEEDED: Activated user exceeds the maximum licensed users.", mlog.Int("user_limit", userLimits.MaxUsersLimit))
|
|
} else {
|
|
rctx.Logger().Warn("ERROR_SAFETY_LIMITS_EXCEEDED: Activated user exceeds the total active user limit.", mlog.Int("user_limit", userLimits.MaxUsersLimit))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
func (a *App) DeactivateGuests(rctx request.CTX) *model.AppError {
|
|
userIDs, err := a.ch.srv.userService.DeactivateAllGuests()
|
|
if err != nil {
|
|
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, userID := range userIDs {
|
|
if err := a.Srv().Platform().RevokeAllSessions(rctx, userID); err != nil {
|
|
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
for _, userID := range userIDs {
|
|
if err := a.userDeactivated(rctx, userID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
a.Srv().Store().Channel().ClearCaches()
|
|
a.Srv().Store().User().ClearCaches()
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventGuestsDeactivated, "", "", "", nil, "")
|
|
a.Publish(message)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) DeactivateMagicLinkGuests(rctx request.CTX) *model.AppError {
|
|
userIDs, err := a.ch.srv.userService.DeactivateMagicLinkGuests()
|
|
if err != nil {
|
|
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, userID := range userIDs {
|
|
if err := a.Srv().Platform().RevokeAllSessions(rctx, userID); err != nil {
|
|
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
for _, userID := range userIDs {
|
|
if err := a.userDeactivated(rctx, userID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().Token().RemoveAllTokensByType(model.TokenTypeGuestMagicLinkInvitation); err != nil {
|
|
rctx.Logger().Warn("Error while removing guest magic link invitation tokens", mlog.Err(err))
|
|
}
|
|
if err := a.Srv().Store().Token().RemoveAllTokensByType(model.TokenTypeGuestMagicLink); err != nil {
|
|
rctx.Logger().Warn("Error while removing guest magic link tokens", mlog.Err(err))
|
|
}
|
|
a.Srv().Store().Channel().ClearCaches()
|
|
a.Srv().Store().User().ClearCaches()
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventGuestsDeactivated, "", "", "", nil, "")
|
|
a.Publish(message)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
|
|
return a.ch.srv.userService.GetSanitizeOptions(asAdmin)
|
|
}
|
|
|
|
func (a *App) SanitizeProfile(user *model.User, asAdmin bool) {
|
|
options := a.ch.srv.userService.GetSanitizeOptions(asAdmin)
|
|
|
|
user.SanitizeProfile(options, asAdmin)
|
|
}
|
|
|
|
func (a *App) UpdateUserAsUser(rctx request.CTX, user *model.User, asAdmin bool) (*model.User, *model.AppError) {
|
|
updatedUser, err := a.UpdateUser(rctx, user, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return updatedUser, nil
|
|
}
|
|
|
|
// CheckProviderAttributes returns the empty string if the patch can be applied without
|
|
// overriding attributes set by the user's login provider; otherwise, the name of the offending
|
|
// field is returned.
|
|
func (a *App) CheckProviderAttributes(rctx request.CTX, user *model.User, patch *model.UserPatch) string {
|
|
tryingToChange := func(userValue *string, patchValue *string) bool {
|
|
return patchValue != nil && *patchValue != *userValue
|
|
}
|
|
|
|
// If any login provider is used, then the username may not be changed
|
|
if user.AuthService != "" && tryingToChange(&user.Username, patch.Username) {
|
|
return "username"
|
|
}
|
|
|
|
LdapSettings := &a.Config().LdapSettings
|
|
SamlSettings := &a.Config().SamlSettings
|
|
|
|
conflictField := ""
|
|
if a.Ldap() != nil &&
|
|
(user.IsLDAPUser() || (user.IsSAMLUser() && *SamlSettings.EnableSyncWithLdap)) {
|
|
conflictField = a.Ldap().CheckProviderAttributes(rctx, LdapSettings, user, patch)
|
|
} else if a.Saml() != nil && user.IsSAMLUser() {
|
|
conflictField = a.Saml().CheckProviderAttributes(rctx, SamlSettings, user, patch)
|
|
} else if user.IsOAuthUser() {
|
|
if tryingToChange(&user.FirstName, patch.FirstName) || tryingToChange(&user.LastName, patch.LastName) {
|
|
conflictField = "full name"
|
|
}
|
|
}
|
|
|
|
return conflictField
|
|
}
|
|
|
|
func (a *App) PatchUser(rctx request.CTX, userID string, patch *model.UserPatch, asAdmin bool) (*model.User, *model.AppError) {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user.Patch(patch)
|
|
|
|
updatedUser, err := a.UpdateUser(rctx, user, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return updatedUser, nil
|
|
}
|
|
|
|
func (a *App) UpdateUserAuth(rctx request.CTX, userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) {
|
|
if _, err := a.Srv().Store().User().UpdateAuthData(userID, userAuth.AuthService, userAuth.AuthData, "", false); err != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("UpdateUserAuth", "app.user.update_auth_data.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
a.InvalidateCacheForUser(userID)
|
|
|
|
return userAuth, nil
|
|
}
|
|
|
|
func (a *App) sendUpdatedUserEvent(user *model.User) {
|
|
// exclude event creator user from admin, member user broadcast
|
|
omitUsers := make(map[string]bool, 1)
|
|
omitUsers[user.Id] = true
|
|
|
|
// First, creating a base copy to avoid race conditions
|
|
// from setting the binaryParamKey in userstore.Update.
|
|
user = user.DeepCopy()
|
|
// Create copies for different sanitization levels:
|
|
// - adminCopyOfUser: moderately sanitized for admins
|
|
// - sourceUserCopyOfUser: minimally sanitized (keeps NotifyProps) for event creator
|
|
adminCopyOfUser := user.DeepCopy()
|
|
sourceUserCopyOfUser := user.DeepCopy()
|
|
|
|
a.SanitizeProfile(adminCopyOfUser, true)
|
|
adminMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", omitUsers, "")
|
|
adminMessage.Add("user", adminCopyOfUser)
|
|
adminMessage.GetBroadcast().ContainsSensitiveData = true
|
|
a.Publish(adminMessage)
|
|
|
|
a.SanitizeProfile(user, false)
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", omitUsers, "")
|
|
message.Add("user", user)
|
|
message.GetBroadcast().ContainsSanitizedData = true
|
|
a.Publish(message)
|
|
|
|
sourceUserCopyOfUser.Sanitize(nil)
|
|
sourceUserMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", sourceUserCopyOfUser.Id, nil, "")
|
|
sourceUserMessage.Add("user", sourceUserCopyOfUser)
|
|
a.Publish(sourceUserMessage)
|
|
}
|
|
|
|
func (a *App) isUniqueToGroupNames(val string) *model.AppError {
|
|
if val == "" {
|
|
return nil
|
|
}
|
|
var notFoundErr *store.ErrNotFound
|
|
group, err := a.Srv().Store().Group().GetByName(val, model.GroupSearchOpts{})
|
|
if err != nil && !errors.As(err, ¬FoundErr) {
|
|
return model.NewAppError("isUniqueToGroupNames", "app.user.save.groupname.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if group != nil {
|
|
return model.NewAppError("isUniqueToGroupNames", "app.user.save.username_exists.app_error", nil, fmt.Sprintf("group name %s exists", val), http.StatusBadRequest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateUser(rctx request.CTX, user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
|
|
prev, err := a.ch.srv.userService.GetUser(user.Id)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("UpdateUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("UpdateUser", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if prev.CreateAt != user.CreateAt {
|
|
user.CreateAt = prev.CreateAt
|
|
}
|
|
|
|
if user.Username != prev.Username {
|
|
if err := a.isUniqueToGroupNames(user.Username); err != nil {
|
|
err.Where = "UpdateUser"
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var newEmail string
|
|
if user.Email != prev.Email {
|
|
if !users.CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) {
|
|
if !prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
|
|
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_domain.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if !users.CheckUserDomain(user, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) {
|
|
if prev.IsGuest() && !prev.IsLDAPUser() && !prev.IsSAMLUser() {
|
|
return nil, model.NewAppError("UpdateUser", "api.user.update_user.accepted_guest_domain.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if *a.Config().EmailSettings.RequireEmailVerification {
|
|
newEmail = user.Email
|
|
// Don't set new eMail on user account if email verification is required, this will be done as a post-verification action
|
|
// to avoid users being able to set non-controlled eMails as their account email
|
|
if _, appErr := a.GetUserByEmail(newEmail); appErr == nil {
|
|
return nil, model.NewAppError("UpdateUser", "app.user.save.email_exists.app_error", nil, "user_id="+user.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
// When a bot is created, prev.Email will be an autogenerated faked email,
|
|
// which will not match a CLI email input during bot to user conversions.
|
|
// To update a bot users email, do not set the email to the faked email
|
|
// stored in prev.Email. Allow using the email defined in the CLI
|
|
if !user.IsBot {
|
|
user.Email = prev.Email
|
|
}
|
|
}
|
|
}
|
|
|
|
userUpdate, err := a.ch.srv.userService.UpdateUser(rctx, user, false)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
var conErr *store.ErrConflict
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("UpdateUser", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &conErr):
|
|
if conErr.Resource == "Username" {
|
|
return nil, model.NewAppError("UpdateUser", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
return nil, model.NewAppError("UpdateUser", "app.user.save.email_exists.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("UpdateUser", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if userUpdate == nil {
|
|
return nil, model.NewAppError("UpdateUser", "app.user.update.find.app_error", nil, "received nil update result from store for userId="+user.Id, http.StatusInternalServerError)
|
|
}
|
|
if userUpdate.New == nil {
|
|
return nil, model.NewAppError("UpdateUser", "app.user.update.find.app_error", nil, "received update result with nil New user from store for userId="+user.Id, http.StatusInternalServerError)
|
|
}
|
|
|
|
newUser := userUpdate.New
|
|
|
|
if (newUser.Username != userUpdate.Old.Username) && (newUser.LastPictureUpdate <= 0) {
|
|
// When a username is updated and the profile is still using a default profile picture, generate a new one based on their username
|
|
if err := a.UpdateDefaultProfileImage(rctx, newUser); err != nil {
|
|
rctx.Logger().Warn("Error with updating default profile image", mlog.Err(err))
|
|
}
|
|
|
|
tempUser, getUserErr := a.GetUser(user.Id)
|
|
if getUserErr != nil {
|
|
rctx.Logger().Warn("Error when retrieving user after profile picture update, avatar may fail to update automatically on client applications.", mlog.Err(getUserErr))
|
|
} else {
|
|
newUser = tempUser
|
|
}
|
|
}
|
|
|
|
if sendNotifications {
|
|
if newUser.Email != userUpdate.Old.Email || newEmail != "" {
|
|
if *a.Config().EmailSettings.RequireEmailVerification {
|
|
a.Srv().Go(func() {
|
|
if err := a.SendEmailVerification(newUser, newEmail, ""); err != nil {
|
|
rctx.Logger().Error("Failed to send email verification", mlog.Err(err))
|
|
}
|
|
})
|
|
} else {
|
|
a.Srv().Go(func() {
|
|
if err := a.Srv().EmailService.SendEmailChangeEmail(userUpdate.Old.Email, newUser.Email, newUser.Locale, a.GetSiteURL()); err != nil {
|
|
rctx.Logger().Error("Failed to send email change email", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if newUser.Username != userUpdate.Old.Username {
|
|
a.Srv().Go(func() {
|
|
if err := a.Srv().EmailService.SendChangeUsernameEmail(newUser.Username, newUser.Email, newUser.Locale, a.GetSiteURL()); err != nil {
|
|
rctx.Logger().Error("Failed to send change username email", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
a.sendUpdatedUserEvent(newUser)
|
|
}
|
|
|
|
a.InvalidateCacheForUser(user.Id)
|
|
a.onUserProfileChange(user.Id)
|
|
|
|
// If user locale changed, invalidate auto-translation language caches
|
|
if newUser.Locale != userUpdate.Old.Locale {
|
|
a.Srv().Store().AutoTranslation().InvalidateUserLocaleCache(user.Id)
|
|
}
|
|
|
|
newUser.Sanitize(map[string]bool{})
|
|
|
|
return newUser, nil
|
|
}
|
|
|
|
func (a *App) UpdateUserActive(rctx request.CTX, userID string, active bool) *model.AppError {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = a.UpdateActive(rctx, user, active); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) updateUserNotifyProps(userID string, props map[string]string) *model.AppError {
|
|
err := a.ch.srv.userService.UpdateUserNotifyProps(userID, props)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return appErr
|
|
default:
|
|
return model.NewAppError("UpdateUser", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
a.InvalidateCacheForUser(userID)
|
|
a.onUserProfileChange(userID)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateMfa(rctx request.CTX, activate bool, userID, token string) *model.AppError {
|
|
if activate {
|
|
if err := a.ActivateMfa(userID, token); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := a.DeactivateMfa(userID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
rctx.Logger().Error("Failed to get user", mlog.Err(err))
|
|
return
|
|
}
|
|
|
|
if err := a.Srv().EmailService.SendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
|
|
rctx.Logger().Error("Failed to send mfa change email", mlog.Err(err))
|
|
}
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdatePasswordByUserIdSendEmail(rctx request.CTX, userID, newPassword, method string) *model.AppError {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.UpdatePasswordSendEmail(rctx, user, newPassword, method)
|
|
}
|
|
|
|
func (a *App) UpdatePassword(rctx request.CTX, user *model.User, newPassword string) *model.AppError {
|
|
if err := a.IsPasswordValid(rctx, newPassword); err != nil {
|
|
return err
|
|
}
|
|
|
|
// remote/synthetic users cannot update password via any mechanism
|
|
if user.IsRemote() {
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
if user.IsMagicLinkEnabled() {
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.magic_link.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
hashedPassword, err := hashers.Hash(newPassword)
|
|
if err != nil {
|
|
// can't be password length (checked in IsPasswordValid)
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.password_hash.app_error", nil, "user_id="+user.Id, http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().User().UpdatePassword(user.Id, hashedPassword); err != nil {
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.InvalidateCacheForUser(user.Id)
|
|
|
|
if *a.Config().ServiceSettings.TerminateSessionsOnPasswordChange {
|
|
// Get currently active sessions if request is user-initiated to retain it
|
|
currentSession := ""
|
|
if rctx.Session() != nil && rctx.Session().UserId == user.Id {
|
|
currentSession = rctx.Session().Id
|
|
}
|
|
|
|
sessions, err := a.GetSessions(rctx, user.Id)
|
|
if err != nil {
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// Revoke all but current session
|
|
for _, session := range sessions {
|
|
if session.Id == currentSession {
|
|
continue
|
|
}
|
|
|
|
err := a.RevokeSessionById(rctx, session.Id)
|
|
if err != nil {
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdatePasswordSendEmail(rctx request.CTX, user *model.User, newPassword, method string) *model.AppError {
|
|
if err := a.UpdatePassword(rctx, user, newPassword); err != nil {
|
|
return err
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
if err := a.Srv().EmailService.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
|
|
rctx.Logger().Error("Failed to send password change email", mlog.Err(err))
|
|
}
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateHashedPasswordByUserId(userID, newHashedPassword string) *model.AppError {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.UpdateHashedPassword(user, newHashedPassword)
|
|
}
|
|
|
|
func (a *App) UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError {
|
|
// remote/synthetic users cannot update password via any mechanism
|
|
if user.IsRemote() {
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
if err := a.Srv().Store().User().UpdatePassword(user.Id, newHashedPassword); err != nil {
|
|
return model.NewAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.InvalidateCacheForUser(user.Id)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) ResetPasswordFromToken(rctx request.CTX, userSuppliedTokenString, newPassword string) *model.AppError {
|
|
return a.resetPasswordFromToken(rctx, userSuppliedTokenString, newPassword, model.GetMillis())
|
|
}
|
|
|
|
func (a *App) resetPasswordFromToken(rctx request.CTX, userSuppliedTokenString, newPassword string, nowMilli int64) *model.AppError {
|
|
token, err := a.GetPasswordRecoveryToken(userSuppliedTokenString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We cannot use IsExpired() here because we need to check
|
|
// with the argument passed in.
|
|
if nowMilli > token.CreateAt+model.PasswordRecoverExpiryTime {
|
|
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
tokenData := struct {
|
|
UserId string
|
|
Email string
|
|
}{}
|
|
|
|
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
|
|
if err2 != nil {
|
|
return model.NewAppError("resetPassword", "api.user.reset_password.token_parse.error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
user, err := a.GetUser(tokenData.UserId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if user.Email != tokenData.Email {
|
|
return model.NewAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if user.IsSSOUser() {
|
|
return model.NewAppError("ResetPasswordFromCode", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if user.IsMagicLinkEnabled() {
|
|
return model.NewAppError("ResetPasswordFromCode", "api.user.send_password_reset.guest_magic_link.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
// don't allow password reset for remote/synthetic users
|
|
if user.IsRemote() {
|
|
return model.NewAppError("resetPassword", "api.user.reset_password.broken_token.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
T := i18n.GetUserTranslations(user.Locale)
|
|
|
|
if err := a.UpdatePasswordSendEmail(rctx, user, newPassword, T("api.user.reset_password.method")); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.DeleteToken(token); err != nil {
|
|
rctx.Logger().Warn("Failed to delete token", mlog.Err(err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SendPasswordReset(rctx request.CTX, email string, siteURL string) (bool, *model.AppError) {
|
|
user, err := a.GetUserByEmail(email)
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
|
|
// don't allow password reset for remote/synthetic users
|
|
if user.IsRemote() {
|
|
return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if user.AuthData != nil && *user.AuthData != "" {
|
|
return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if user.IsMagicLinkEnabled() {
|
|
return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.guest_magic_link.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
token, err := a.CreatePasswordRecoveryToken(rctx, user.Id, user.Email)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
result, eErr := a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
|
|
if eErr != nil {
|
|
return result, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "", http.StatusInternalServerError).Wrap(eErr)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (a *App) CreatePasswordRecoveryToken(rctx request.CTX, userID, email string) (*model.Token, *model.AppError) {
|
|
tokenExtra := struct {
|
|
UserId string
|
|
Email string
|
|
}{
|
|
userID,
|
|
email,
|
|
}
|
|
jsonData, err := json.Marshal(tokenExtra)
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreatePasswordRecoveryToken", "api.user.create_password_token.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// remove any previously created tokens for user
|
|
appErr := a.InvalidatePasswordRecoveryTokensForUser(userID)
|
|
if appErr != nil {
|
|
rctx.Logger().Warn("Error while deleting additional user tokens.", mlog.Err(err))
|
|
}
|
|
|
|
token := model.NewToken(model.TokenTypePasswordRecovery, string(jsonData))
|
|
if err := a.Srv().Store().Token().Save(token); err != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("CreatePasswordRecoveryToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func (a *App) InvalidatePasswordRecoveryTokensForUser(userID string) *model.AppError {
|
|
tokens, err := a.Srv().Store().Token().GetAllTokensByType(model.TokenTypePasswordRecovery)
|
|
if err != nil {
|
|
return model.NewAppError("InvalidatePasswordRecoveryTokensForUser", "api.user.invalidate_password_recovery_tokens.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
var appErr *model.AppError
|
|
for _, token := range tokens {
|
|
tokenExtra := struct {
|
|
UserId string
|
|
Email string
|
|
}{}
|
|
if err := json.Unmarshal([]byte(token.Extra), &tokenExtra); err != nil {
|
|
appErr = model.NewAppError("InvalidatePasswordRecoveryTokensForUser", "api.user.invalidate_password_recovery_tokens_parse.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
continue
|
|
}
|
|
|
|
if tokenExtra.UserId != userID {
|
|
continue
|
|
}
|
|
|
|
if err := a.Srv().Store().Token().Delete(token.Token); err != nil {
|
|
appErr = model.NewAppError("InvalidatePasswordRecoveryTokensForUser", "api.user.invalidate_password_recovery_tokens_delete.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return appErr
|
|
}
|
|
|
|
func (a *App) GetPasswordRecoveryToken(token string) (*model.Token, *model.AppError) {
|
|
rtoken, err := a.Srv().Store().Token().GetByToken(token)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.invalid_link.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
if rtoken.Type != model.TokenTypePasswordRecovery {
|
|
return nil, model.NewAppError("GetPasswordRecoveryToken", "api.user.reset_password.broken_token.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
return rtoken, nil
|
|
}
|
|
|
|
func (a *App) GetTokenById(token string) (*model.Token, *model.AppError) {
|
|
rtoken, err := a.Srv().Store().Token().GetByToken(token)
|
|
if err != nil {
|
|
var status int
|
|
|
|
switch err.(type) {
|
|
case *store.ErrNotFound:
|
|
status = http.StatusNotFound
|
|
default:
|
|
status = http.StatusInternalServerError
|
|
}
|
|
|
|
return nil, model.NewAppError("GetTokenById", "api.user.create_user.signup_link_invalid.app_error", nil, "", status).Wrap(err)
|
|
}
|
|
|
|
return rtoken, nil
|
|
}
|
|
|
|
func (a *App) ConsumeTokenOnce(tokenType, tokenStr string) (*model.Token, *model.AppError) {
|
|
token, err := a.Srv().Store().Token().ConsumeOnce(tokenType, tokenStr)
|
|
if err != nil {
|
|
var status int
|
|
switch err.(type) {
|
|
case *store.ErrNotFound:
|
|
status = http.StatusNotFound
|
|
default:
|
|
status = http.StatusInternalServerError
|
|
}
|
|
return nil, model.NewAppError("ConsumeTokenOnce", "api.user.create_user.signup_link_invalid.app_error", nil, "", status).Wrap(err)
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
func (a *App) DeleteToken(token *model.Token) *model.AppError {
|
|
err := a.Srv().Store().Token().Delete(token.Token)
|
|
if err != nil {
|
|
return model.NewAppError("DeleteToken", "app.recover.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateUserRoles(rctx request.CTX, userID string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
err.StatusCode = http.StatusBadRequest
|
|
return nil, err
|
|
}
|
|
|
|
return a.UpdateUserRolesWithUser(rctx, user, newRoles, sendWebSocketEvent)
|
|
}
|
|
|
|
func (a *App) UpdateUserRolesWithUser(rctx request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError) {
|
|
if err := a.CheckRolesExist(strings.Fields(newRoles)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user.IsSystemAdmin() && !strings.Contains(newRoles, model.SystemAdminRoleId) {
|
|
// if user being updated is SysAdmin, make sure its not the last one.
|
|
options := model.UserCountOptions{
|
|
IncludeBotAccounts: false,
|
|
Roles: []string{model.SystemAdminRoleId},
|
|
}
|
|
count, err := a.Srv().Store().User().Count(options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.countAdmins.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
if count <= 1 {
|
|
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.lastAdmin.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
user.Roles = newRoles
|
|
uchan := make(chan store.StoreResult[*model.UserUpdate], 1)
|
|
go func() {
|
|
userUpdate, err := a.Srv().Store().User().Update(rctx, user, true)
|
|
uchan <- store.StoreResult[*model.UserUpdate]{Data: userUpdate, NErr: err}
|
|
close(uchan)
|
|
}()
|
|
|
|
schan := make(chan store.StoreResult[string], 1)
|
|
go func() {
|
|
id, err := a.Srv().Store().Session().UpdateRoles(user.Id, newRoles)
|
|
schan <- store.StoreResult[string]{Data: id, NErr: err}
|
|
close(schan)
|
|
}()
|
|
|
|
result := <-uchan
|
|
if result.NErr != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(result.NErr, &appErr):
|
|
return nil, appErr
|
|
case errors.As(result.NErr, &invErr):
|
|
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(result.NErr)
|
|
default:
|
|
return nil, model.NewAppError("UpdateUserRoles", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(result.NErr)
|
|
}
|
|
}
|
|
ruser := result.Data.New
|
|
|
|
if result := <-schan; result.NErr != nil {
|
|
// soft error since the user roles were still updated
|
|
rctx.Logger().Warn("Failed during updating user roles", mlog.Err(result.NErr))
|
|
}
|
|
|
|
a.InvalidateCacheForUser(user.Id)
|
|
a.ClearSessionCacheForUser(user.Id)
|
|
|
|
if sendWebSocketEvent {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserRoleUpdated, "", "", user.Id, nil, "")
|
|
message.Add("user_id", user.Id)
|
|
message.Add("roles", newRoles)
|
|
a.Publish(message)
|
|
}
|
|
|
|
return ruser, nil
|
|
}
|
|
|
|
func (a *App) PermanentDeleteUser(rctx request.CTX, user *model.User) *model.AppError {
|
|
rctx.Logger().Warn("Attempting to permanently delete account", mlog.String("user_id", user.Id), mlog.String("user_email", user.Email))
|
|
if user.IsInRole(model.SystemAdminRoleId) {
|
|
rctx.Logger().Warn("You are deleting a user that is a system administrator. You may need to set another account as the system administrator using the command line tools.", mlog.String("user_email", user.Email))
|
|
}
|
|
|
|
if _, err := a.UpdateActive(rctx, user, false); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.Srv().Store().Session().PermanentDeleteSessionsByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.session.permanent_delete_sessions_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().UserAccessToken().DeleteAllForUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.user_access_token.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().OAuth().PermanentDeleteAuthDataByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.oauth.permanent_delete_auth_data_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Webhook().PermanentDeleteIncomingByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_incoming_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Webhook().PermanentDeleteOutgoingByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.webhooks.permanent_delete_outgoing_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Command().PermanentDeleteByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.user.permanentdeleteuser.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Preference().PermanentDeleteByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.preference.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Channel().PermanentDeleteMembersByUser(rctx, user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.channel.permanent_delete_members_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Group().PermanentDeleteMembersByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.group.permanent_delete_members_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Post().PermanentDeleteByUser(rctx, user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Reaction().PermanentDeleteByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.reaction.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().ScheduledPost().PermanentDeleteByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.scheduled_post.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Draft().PermanentDeleteByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.drafts.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Bot().PermanentDelete(user.Id); err != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return model.NewAppError("PermanentDeleteUser", "app.bot.permenent_delete.bad_id", map[string]any{"user_id": invErr.Value}, "", http.StatusBadRequest).Wrap(err)
|
|
default: // last fallback in case it doesn't map to an existing app error.
|
|
return model.NewAppError("PermanentDeleteUser", "app.bot.permanent_delete.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
infos, err := a.Srv().Store().FileInfo().GetForUser(user.Id)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Error getting file list for user from FileInfoStore", mlog.Err(err))
|
|
}
|
|
|
|
a.RemoveFilesFromFileStore(rctx, infos)
|
|
|
|
// delete directory containing user's profile image
|
|
profileImageDirectory := getProfileImageDirectory(user.Id)
|
|
profileImagePath := getProfileImagePath(user.Id)
|
|
resProfileImageExists, errProfileImageExists := a.FileExists(profileImagePath)
|
|
|
|
fileHandlingErrorsFound := false
|
|
|
|
if errProfileImageExists != nil {
|
|
fileHandlingErrorsFound = true
|
|
rctx.Logger().Warn(
|
|
"Error checking existence of profile image.",
|
|
mlog.String("path", profileImagePath),
|
|
mlog.Err(errProfileImageExists),
|
|
)
|
|
}
|
|
|
|
if resProfileImageExists {
|
|
errRemoveDirectory := a.RemoveDirectory(profileImageDirectory)
|
|
|
|
if errRemoveDirectory != nil {
|
|
fileHandlingErrorsFound = true
|
|
rctx.Logger().Warn(
|
|
"Unable to remove profile image directory",
|
|
mlog.String("path", profileImageDirectory),
|
|
mlog.Err(errRemoveDirectory),
|
|
)
|
|
}
|
|
}
|
|
|
|
if _, err := a.Srv().Store().FileInfo().PermanentDeleteByUser(rctx, user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.file_info.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().User().PermanentDelete(rctx, user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.user.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Audit().PermanentDeleteByUser(user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.audit.permanent_delete_by_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Team().RemoveAllMembersByUser(rctx, user.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteUser", "app.team.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.InvalidateCacheForUser(user.Id)
|
|
|
|
if fileHandlingErrorsFound {
|
|
return model.NewAppError("PermanentDeleteUser", "app.file_info.permanent_delete_by_user.app_error", nil, "Couldn't delete profile image of the user.", http.StatusAccepted)
|
|
}
|
|
|
|
rctx.Logger().Warn("Permanently deleted account", mlog.String("user_email", user.Email), mlog.String("user_id", user.Id))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PermanentDeleteAllUsers(rctx request.CTX) *model.AppError {
|
|
users, err := a.Srv().Store().User().GetAll()
|
|
if err != nil {
|
|
return model.NewAppError("PermanentDeleteAllUsers", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
for _, user := range users {
|
|
if appErr := a.PermanentDeleteUser(rctx, user); appErr != nil {
|
|
rctx.Logger().Warn("Error while deleting user", mlog.Err(appErr))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError {
|
|
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, email.CreateEmailTokenError):
|
|
return model.NewAppError("CreateVerifyEmailToken", "api.user.create_email_token.error", nil, "", http.StatusInternalServerError)
|
|
default:
|
|
return model.NewAppError("CreateVerifyEmailToken", "app.recover.save.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if _, err := a.GetStatus(user.Id); err != nil {
|
|
if err.StatusCode != http.StatusNotFound {
|
|
return err
|
|
}
|
|
eErr := a.Srv().EmailService.SendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
|
|
if eErr != nil {
|
|
return model.NewAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, "", http.StatusInternalServerError).Wrap(eErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := a.Srv().EmailService.SendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token); err != nil {
|
|
return model.NewAppError("sendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) VerifyEmailFromToken(rctx request.CTX, userSuppliedTokenString string) *model.AppError {
|
|
token, err := a.GetVerifyEmailToken(userSuppliedTokenString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if token.IsExpired() {
|
|
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.link_expired.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
tokenData := struct {
|
|
UserId string
|
|
Email string
|
|
}{}
|
|
|
|
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
|
|
if err2 != nil {
|
|
return model.NewAppError("VerifyEmailFromToken", "api.user.verify_email.token_parse.error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
user, err := a.GetUser(tokenData.UserId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tokenData.Email = strings.ToLower(tokenData.Email)
|
|
if err := a.VerifyUserEmail(tokenData.UserId, tokenData.Email); err != nil {
|
|
return err
|
|
}
|
|
|
|
if user.Email != tokenData.Email {
|
|
a.Srv().Go(func() {
|
|
if err := a.Srv().EmailService.SendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
|
|
rctx.Logger().Error("Failed to send email change email", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
if err := a.DeleteToken(token); err != nil {
|
|
rctx.Logger().Warn("Failed to delete token", mlog.Err(err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) {
|
|
rtoken, err := a.Srv().Store().Token().GetByToken(token)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.bad_link.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
if rtoken.Type != model.TokenTypeVerifyEmail {
|
|
return nil, model.NewAppError("GetVerifyEmailToken", "api.user.verify_email.broken_token.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
return rtoken, nil
|
|
}
|
|
|
|
// GetTotalUsersStats is used for the DM list total
|
|
func (a *App) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
|
|
count, err := a.Srv().Store().User().Count(model.UserCountOptions{
|
|
IncludeBotAccounts: true,
|
|
ViewRestrictions: viewRestrictions,
|
|
})
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetTotalUsersStats", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
stats := &model.UsersStats{
|
|
TotalUsersCount: count,
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// GetFilteredUsersStats is used to get a count of users based on the set of filters supported by UserCountOptions.
|
|
func (a *App) GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError) {
|
|
count, err := a.Srv().Store().User().Count(*options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetFilteredUsersStats", "app.user.get_total_users_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
stats := &model.UsersStats{
|
|
TotalUsersCount: count,
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
func (a *App) VerifyUserEmail(userID, email string) *model.AppError {
|
|
if _, err := a.Srv().Store().User().VerifyEmail(userID, email); err != nil {
|
|
return model.NewAppError("VerifyUserEmail", "app.user.verify_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.InvalidateCacheForUser(userID)
|
|
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.sendUpdatedUserEvent(user)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) SearchUsers(rctx request.CTX, props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
if props.WithoutTeam {
|
|
return a.SearchUsersWithoutTeam(props.Term, options)
|
|
}
|
|
if props.InChannelId != "" {
|
|
return a.SearchUsersInChannel(props.InChannelId, props.Term, options)
|
|
}
|
|
if props.NotInChannelId != "" {
|
|
return a.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, options)
|
|
}
|
|
if props.NotInTeamId != "" {
|
|
return a.SearchUsersNotInTeam(props.NotInTeamId, props.Term, options)
|
|
}
|
|
if props.InGroupId != "" {
|
|
return a.SearchUsersInGroup(props.InGroupId, props.Term, options)
|
|
}
|
|
if props.NotInGroupId != "" {
|
|
return a.SearchUsersNotInGroup(props.NotInGroupId, props.Term, options)
|
|
}
|
|
return a.SearchUsersInTeam(rctx, props.TeamId, props.Term, options)
|
|
}
|
|
|
|
func (a *App) SearchUsersInChannel(channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
users, err := a.Srv().Store().User().SearchInChannel(channelID, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchUsersInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) SearchUsersNotInChannel(teamID string, channelID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
|
|
rctx := request.EmptyContext(a.Log())
|
|
if ok, err := a.ChannelAccessControlled(rctx, channelID); err != nil {
|
|
return nil, err
|
|
} else if ok {
|
|
acs := a.Srv().Channels().AccessControl
|
|
if acs != nil {
|
|
users, _, appErr := acs.QueryUsersForResource(rctx, channelID, "*", model.SubjectSearchOptions{
|
|
Term: term,
|
|
TeamID: teamID,
|
|
Limit: options.Limit,
|
|
})
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
}
|
|
|
|
users, err := a.Srv().Store().User().SearchNotInChannel(teamID, channelID, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchUsersNotInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) SearchUsersInTeam(rctx request.CTX, teamID, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
|
|
users, err := a.Srv().Store().User().Search(rctx, teamID, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchUsersInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
users, err := a.Srv().Store().User().SearchNotInTeam(notInTeamId, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchUsersNotInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
users, err := a.Srv().Store().User().SearchWithoutTeam(term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchUsersWithoutTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) SearchUsersInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
users, err := a.Srv().Store().User().SearchInGroup(groupID, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchUsersInGroup", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) SearchUsersNotInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
users, err := a.Srv().Store().User().SearchNotInGroup(groupID, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchUsersNotInGroup", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (a *App) AutocompleteUsersInChannel(rctx request.CTX, teamID string, channelID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
|
|
autocomplete, err := a.Srv().Store().User().AutocompleteUsersInChannel(rctx, teamID, channelID, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteUsersInChannel", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range autocomplete.InChannel {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
for _, user := range autocomplete.OutOfChannel {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
return autocomplete, nil
|
|
}
|
|
|
|
func (a *App) AutocompleteUsersInTeam(rctx request.CTX, teamID string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
|
|
users, err := a.Srv().Store().User().Search(rctx, teamID, term, options)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteUsersInTeam", "app.user.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
for _, user := range users {
|
|
a.SanitizeProfile(user, options.IsAdmin)
|
|
}
|
|
|
|
autocomplete := &model.UserAutocompleteInTeam{}
|
|
autocomplete.InTeam = users
|
|
return autocomplete, nil
|
|
}
|
|
|
|
func (a *App) UpdateOAuthUserAttrs(rctx request.CTX, userData io.Reader, user *model.User, provider einterfaces.OAuthProvider, service string, tokenUser *model.User) *model.AppError {
|
|
settings, err := provider.GetSSOSettings(rctx, a.Config(), service)
|
|
if err != nil {
|
|
return model.NewAppError("UpdateOAuthUserAttrs", "api.user.oauth.get_settings.app_error", map[string]any{"Service": service}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
oauthUser, err1 := provider.GetUserFromJSON(rctx, userData, tokenUser, settings)
|
|
if err1 != nil {
|
|
return model.NewAppError("UpdateOAuthUserAttrs", "api.user.update_oauth_user_attrs.get_user.app_error", map[string]any{"Service": service}, "", http.StatusBadRequest).Wrap(err1)
|
|
}
|
|
|
|
userAttrsChanged := false
|
|
|
|
if oauthUser.Username != user.Username {
|
|
if existingUser, _ := a.GetUserByUsername(oauthUser.Username); existingUser == nil {
|
|
user.Username = oauthUser.Username
|
|
userAttrsChanged = true
|
|
}
|
|
}
|
|
|
|
if oauthUser.GetFullName() != user.GetFullName() {
|
|
user.FirstName = oauthUser.FirstName
|
|
user.LastName = oauthUser.LastName
|
|
userAttrsChanged = true
|
|
}
|
|
|
|
if oauthUser.Email != user.Email {
|
|
if existingUser, _ := a.GetUserByEmail(oauthUser.Email); existingUser == nil {
|
|
user.Email = oauthUser.Email
|
|
userAttrsChanged = true
|
|
}
|
|
}
|
|
|
|
if user.DeleteAt > 0 {
|
|
// Make sure they are not disabled
|
|
user.DeleteAt = 0
|
|
userAttrsChanged = true
|
|
}
|
|
|
|
if userAttrsChanged {
|
|
users, err := a.Srv().Store().User().Update(rctx, user, true)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return appErr
|
|
case errors.As(err, &invErr):
|
|
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.find.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
default:
|
|
return model.NewAppError("UpdateOAuthUserAttrs", "app.user.update.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
user = users.New
|
|
a.InvalidateCacheForUser(user.Id)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RestrictUsersGetByPermissions(rctx request.CTX, userID string, options *model.UserGetOptions) (*model.UserGetOptions, *model.AppError) {
|
|
restrictions, err := a.GetViewUsersRestrictions(rctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
options.ViewRestrictions = restrictions
|
|
return options, nil
|
|
}
|
|
|
|
// FilterNonGroupTeamMembers returns the subset of the given user IDs of the users who are not members of groups
|
|
// associated to the team excluding bots.
|
|
func (a *App) FilterNonGroupTeamMembers(rctx request.CTX, userIDs []string, team *model.Team) ([]string, error) {
|
|
teamGroupUsers, err := a.GetTeamGroupUsers(team.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a.filterNonGroupUsers(rctx, userIDs, teamGroupUsers)
|
|
}
|
|
|
|
// FilterNonGroupChannelMembers returns the subset of the given user IDs of the users who are not members of groups
|
|
// associated to the channel excluding bots
|
|
func (a *App) FilterNonGroupChannelMembers(rctx request.CTX, userIDs []string, channel *model.Channel) ([]string, error) {
|
|
channelGroupUsers, err := a.GetChannelGroupUsers(channel.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a.filterNonGroupUsers(rctx, userIDs, channelGroupUsers)
|
|
}
|
|
|
|
// filterNonGroupUsers is a helper function that takes a list of user ids and a list of users
|
|
// and returns the list of normal users present in userIDs but not in groupUsers.
|
|
func (a *App) filterNonGroupUsers(rctx request.CTX, userIDs []string, groupUsers []*model.User) ([]string, error) {
|
|
nonMemberIds := []string{}
|
|
users, err := a.Srv().Store().User().GetProfileByIds(rctx, userIDs, nil, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, user := range users {
|
|
userIsMember := user.IsBot
|
|
|
|
for _, pu := range groupUsers {
|
|
if pu.Id == user.Id {
|
|
userIsMember = true
|
|
break
|
|
}
|
|
}
|
|
if !userIsMember {
|
|
nonMemberIds = append(nonMemberIds, user.Id)
|
|
}
|
|
}
|
|
|
|
return nonMemberIds, nil
|
|
}
|
|
|
|
func (a *App) RestrictUsersSearchByPermissions(rctx request.CTX, userID string, options *model.UserSearchOptions) (*model.UserSearchOptions, *model.AppError) {
|
|
restrictions, err := a.GetViewUsersRestrictions(rctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
options.ViewRestrictions = restrictions
|
|
return options, nil
|
|
}
|
|
|
|
func (a *App) UserCanSeeOtherUser(rctx request.CTX, userID string, otherUserId string) (bool, *model.AppError) {
|
|
if userID == otherUserId {
|
|
return true, nil
|
|
}
|
|
|
|
restrictions, err := a.GetViewUsersRestrictions(rctx, userID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if restrictions == nil {
|
|
return true, nil
|
|
}
|
|
|
|
if len(restrictions.Teams) > 0 {
|
|
result, err := a.Srv().Store().Team().UserBelongsToTeams(otherUserId, restrictions.Teams)
|
|
if err != nil {
|
|
return false, model.NewAppError("UserCanSeeOtherUser", "app.team.user_belongs_to_teams.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if result {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
if len(restrictions.Channels) > 0 {
|
|
result, err := a.userBelongsToChannels(otherUserId, restrictions.Channels)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if result {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (a *App) userBelongsToChannels(userID string, channelIDs []string) (bool, *model.AppError) {
|
|
belongs, err := a.Srv().Store().Channel().UserBelongsToChannels(userID, channelIDs)
|
|
if err != nil {
|
|
return false, model.NewAppError("userBelongsToChannels", "app.channel.user_belongs_to_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return belongs, nil
|
|
}
|
|
|
|
func (a *App) GetViewUsersRestrictions(rctx request.CTX, userID string) (*model.ViewUsersRestrictions, *model.AppError) {
|
|
if a.HasPermissionTo(userID, model.PermissionViewMembers) {
|
|
return nil, nil
|
|
}
|
|
|
|
teamIDs, nErr := a.Srv().Store().Team().GetUserTeamIds(userID, true)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("GetViewUsersRestrictions", "app.team.get_user_team_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
teamIDsWithPermission := []string{}
|
|
for _, teamID := range teamIDs {
|
|
if a.HasPermissionToTeam(rctx, userID, teamID, model.PermissionViewMembers) {
|
|
teamIDsWithPermission = append(teamIDsWithPermission, teamID)
|
|
}
|
|
}
|
|
|
|
userChannelMembers, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(rctx, userID, true, true)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetViewUsersRestrictions", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
channelIDs := []string{}
|
|
for channelID := range userChannelMembers {
|
|
channelIDs = append(channelIDs, channelID)
|
|
}
|
|
|
|
return &model.ViewUsersRestrictions{Teams: teamIDsWithPermission, Channels: channelIDs}, nil
|
|
}
|
|
|
|
// PromoteGuestToUser Convert user's roles and all his membership's roles from
|
|
// guest roles to regular user roles.
|
|
func (a *App) PromoteGuestToUser(rctx request.CTX, user *model.User, requestorId string) *model.AppError {
|
|
nErr := a.ch.srv.userService.PromoteGuestToUser(user)
|
|
a.InvalidateCacheForUser(user.Id)
|
|
if nErr != nil {
|
|
return model.NewAppError("PromoteGuestToUser", "app.user.promote_guest.user_update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
userTeams, nErr := a.Srv().Store().Team().GetTeamsByUserId(user.Id)
|
|
if nErr != nil {
|
|
return model.NewAppError("PromoteGuestToUser", "app.team.get_all.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
for _, team := range userTeams {
|
|
// Soft error if there is an issue joining the default channels
|
|
if err := a.JoinDefaultChannels(rctx, team.Id, user, false, requestorId); err != nil {
|
|
rctx.Logger().Warn("Failed to join default channels", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.String("requestor_id", requestorId), mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
promotedUser, err := a.GetUser(user.Id)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to get user on promote guest to user", mlog.Err(err))
|
|
} else {
|
|
a.sendUpdatedUserEvent(promotedUser)
|
|
if uErr := a.ch.srv.platform.UpdateSessionsIsGuest(rctx, promotedUser, promotedUser.IsGuest()); uErr != nil {
|
|
rctx.Logger().Warn("Unable to update user sessions", mlog.String("user_id", promotedUser.Id), mlog.Err(uErr))
|
|
}
|
|
}
|
|
|
|
teamMembers, err := a.GetTeamMembersForUser(rctx, user.Id, "", true)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to get team members for user on promote guest to user", mlog.Err(err))
|
|
}
|
|
|
|
for _, member := range teamMembers {
|
|
if appErr := a.sendUpdatedTeamMemberEvent(member); appErr != nil {
|
|
rctx.Logger().Warn("Error while sending updated team member event", mlog.Err(appErr))
|
|
}
|
|
|
|
channelMembers, appErr := a.GetChannelMembersForUser(rctx, member.TeamId, user.Id)
|
|
if appErr != nil {
|
|
rctx.Logger().Warn("Failed to get channel members for user on promote guest to user", mlog.Err(appErr))
|
|
}
|
|
|
|
for _, member := range channelMembers {
|
|
a.invalidateCacheForChannelMembers(member.ChannelId)
|
|
|
|
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", user.Id, nil, "")
|
|
memberJSON, jsonErr := json.Marshal(member)
|
|
if jsonErr != nil {
|
|
return model.NewAppError("PromoteGuestToUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
}
|
|
}
|
|
|
|
a.ClearSessionCacheForUser(user.Id)
|
|
return nil
|
|
}
|
|
|
|
// DemoteUserToGuest Convert user's roles and all his membership's roles from
|
|
// regular user roles to guest roles.
|
|
func (a *App) DemoteUserToGuest(rctx request.CTX, user *model.User) *model.AppError {
|
|
demotedUser, nErr := a.ch.srv.userService.DemoteUserToGuest(user)
|
|
a.InvalidateCacheForUser(user.Id)
|
|
if nErr != nil {
|
|
return model.NewAppError("DemoteUserToGuest", "app.user.demote_user_to_guest.user_update.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
a.sendUpdatedUserEvent(demotedUser)
|
|
if uErr := a.ch.srv.platform.UpdateSessionsIsGuest(rctx, demotedUser, demotedUser.IsGuest()); uErr != nil {
|
|
rctx.Logger().Warn("Unable to update user sessions", mlog.String("user_id", demotedUser.Id), mlog.Err(uErr))
|
|
}
|
|
|
|
teamMembers, err := a.GetTeamMembersForUser(rctx, user.Id, "", true)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to get team members for users on demote user to guest", mlog.Err(err))
|
|
}
|
|
|
|
for _, member := range teamMembers {
|
|
if appErr := a.sendUpdatedTeamMemberEvent(member); appErr != nil {
|
|
rctx.Logger().Warn("Error while sending updated team member event", mlog.Err(appErr))
|
|
}
|
|
|
|
channelMembers, appErr := a.GetChannelMembersForUser(rctx, member.TeamId, user.Id)
|
|
if appErr != nil {
|
|
rctx.Logger().Warn("Failed to get channel members for users on demote user to guest", mlog.Err(appErr))
|
|
continue
|
|
}
|
|
|
|
for _, member := range channelMembers {
|
|
a.invalidateCacheForChannelMembers(member.ChannelId)
|
|
|
|
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", user.Id, nil, "")
|
|
memberJSON, jsonErr := json.Marshal(member)
|
|
if jsonErr != nil {
|
|
return model.NewAppError("DemoteUserToGuest", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
}
|
|
}
|
|
|
|
a.ClearSessionCacheForUser(user.Id)
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PublishUserTyping(userID, channelID, parentId string) *model.AppError {
|
|
omitUsers := make(map[string]bool, 1)
|
|
omitUsers[userID] = true
|
|
|
|
event := model.NewWebSocketEvent(model.WebsocketEventTyping, "", channelID, "", omitUsers, "")
|
|
event.Add("parent_id", parentId)
|
|
event.Add("user_id", userID)
|
|
a.Publish(event)
|
|
|
|
return nil
|
|
}
|
|
|
|
// invalidateUserCacheAndPublish Invalidates cache for a user and publishes user updated event
|
|
func (a *App) invalidateUserCacheAndPublish(rctx request.CTX, userID string) {
|
|
a.InvalidateCacheForUser(userID)
|
|
|
|
user, userErr := a.GetUser(userID)
|
|
if userErr != nil {
|
|
rctx.Logger().Error("Error in getting users profile", mlog.String("user_id", userID), mlog.Err(userErr))
|
|
return
|
|
}
|
|
|
|
options := a.Config().GetSanitizeOptions()
|
|
user.SanitizeProfile(options, false)
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
|
|
message.Add("user", user)
|
|
a.Publish(message)
|
|
}
|
|
|
|
// GetKnownUsers returns the list of user ids of users with any direct
|
|
// relationship with a user. That means any user sharing any channel, including
|
|
// direct and group channels.
|
|
func (a *App) GetKnownUsers(userID string) ([]string, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetKnownUsers(userID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetKnownUsers", "app.user.get_known_users.get_users.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// ConvertBotToUser converts a bot to user.
|
|
func (a *App) ConvertBotToUser(rctx request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) {
|
|
user, nErr := a.Srv().Store().User().Get(rctx.Context(), bot.UserId)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("ConvertBotToUser", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("ConvertBotToUser", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if sysadmin && !user.IsInRole(model.SystemAdminRoleId) {
|
|
_, appErr := a.UpdateUserRoles(rctx,
|
|
user.Id,
|
|
fmt.Sprintf("%s %s", user.Roles, model.SystemAdminRoleId),
|
|
false)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
}
|
|
|
|
user.Patch(userPatch)
|
|
|
|
user, err := a.UpdateUser(rctx, user, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = a.UpdatePassword(rctx, user, *userPatch.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
appErr := a.Srv().Store().Bot().PermanentDelete(bot.UserId)
|
|
if appErr != nil {
|
|
return nil, model.NewAppError("ConvertBotToUser", "app.user.convert_bot_to_user.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (a *App) GetThreadsForUser(rctx request.CTX, userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
|
|
var result model.Threads
|
|
var eg errgroup.Group
|
|
postPriorityIsEnabled := a.IsPostPriorityEnabled()
|
|
if postPriorityIsEnabled {
|
|
options.IncludeIsUrgent = true
|
|
}
|
|
|
|
if !options.ThreadsOnly {
|
|
eg.Go(func() error {
|
|
totalUnreadThreads, err := a.Srv().Store().Thread().GetTotalUnreadThreads(userID, teamID, options)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to count unread threads for user id=%s", userID)
|
|
}
|
|
result.TotalUnreadThreads = totalUnreadThreads
|
|
|
|
return nil
|
|
})
|
|
|
|
// Unread is a legacy flag that caused GetTotalThreads to compute the same value as
|
|
// GetTotalUnreadThreads. If unspecified, do this work normally; otherwise, skip,
|
|
// and send back duplicate values down below.
|
|
if !options.Unread {
|
|
eg.Go(func() error {
|
|
totalCount, err := a.Srv().Store().Thread().GetTotalThreads(userID, teamID, options)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
|
|
}
|
|
result.Total = totalCount
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
eg.Go(func() error {
|
|
totalUnreadMentions, err := a.Srv().Store().Thread().GetTotalUnreadMentions(userID, teamID, options)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
|
|
}
|
|
result.TotalUnreadMentions = totalUnreadMentions
|
|
|
|
return nil
|
|
})
|
|
|
|
if postPriorityIsEnabled {
|
|
eg.Go(func() error {
|
|
totalUnreadUrgentMentions, err := a.Srv().Store().Thread().GetTotalUnreadUrgentMentions(userID, teamID, options)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to count urgent mentioned threads for user id=%s", userID)
|
|
}
|
|
result.TotalUnreadUrgentMentions = totalUnreadUrgentMentions
|
|
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
if !options.TotalsOnly {
|
|
eg.Go(func() error {
|
|
threads, err := a.Srv().Store().Thread().GetThreadsForUser(rctx, userID, teamID, options)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get threads for user id=%s", userID)
|
|
}
|
|
result.Threads = threads
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
return nil, model.NewAppError("GetThreadsForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if options.Unread {
|
|
result.Total = result.TotalUnreadThreads
|
|
}
|
|
|
|
list := &model.PostList{
|
|
Posts: make(map[string]*model.Post, len(result.Threads)),
|
|
}
|
|
for _, thread := range result.Threads {
|
|
a.sanitizeProfiles(thread.Participants, false)
|
|
thread.Post.SanitizeProps()
|
|
list.AddPost(thread.Post)
|
|
}
|
|
|
|
a.populatePostListTranslations(rctx, list)
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.ThreadMembership, *model.AppError) {
|
|
threadMembership, nErr := a.Srv().Store().Thread().GetMembershipForUser(userId, threadId)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.not_found", nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
return threadMembership, nil
|
|
}
|
|
|
|
func (a *App) GetThreadForUser(rctx request.CTX, threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
|
|
thread, nErr := a.Srv().Store().Thread().GetThreadForUser(rctx, threadMembership, extended, a.IsPostPriorityEnabled())
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.not_found", nil, "thread not found/followed", http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
a.sanitizeProfiles(thread.Participants, false)
|
|
thread.Post.SanitizeProps()
|
|
a.populatePostListTranslations(rctx, &model.PostList{Posts: map[string]*model.Post{thread.Post.Id: thread.Post}})
|
|
return thread, nil
|
|
}
|
|
|
|
func (a *App) UpdateThreadsReadForUser(userID, teamID string) *model.AppError {
|
|
nErr := a.Srv().Store().Thread().MarkAllAsReadByTeam(userID, teamID)
|
|
if nErr != nil {
|
|
return model.NewAppError("UpdateThreadsReadForUser", "app.user.update_threads_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, teamID, "", userID, nil, "")
|
|
a.Publish(message)
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError {
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: state,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: state,
|
|
UpdateParticipants: false,
|
|
}
|
|
_, err := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
|
|
if err != nil {
|
|
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
thread, err := a.Srv().Store().Thread().Get(threadID)
|
|
if err != nil {
|
|
return model.NewAppError("UpdateThreadFollowForUser", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
replyCount := int64(0)
|
|
if thread != nil {
|
|
replyCount = thread.ReplyCount
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadFollowChanged, teamID, "", userID, nil, "")
|
|
message.Add("thread_id", threadID)
|
|
message.Add("state", state)
|
|
message.Add("reply_count", replyCount)
|
|
a.Publish(message)
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateThreadFollowForUserFromChannelAdd(rctx request.CTX, userID, teamID, threadID string) *model.AppError {
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
tm, err := a.Srv().Store().Thread().MaintainMembership(userID, threadID, opts)
|
|
if err != nil {
|
|
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
post, appErr := a.GetSinglePost(rctx, threadID, false)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
user, appErr := a.GetUser(userID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
tm.UnreadMentions, appErr = a.countThreadMentions(rctx, user, post, teamID, post.CreateAt-1)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
tm.LastViewed = post.CreateAt - 1
|
|
_, err = a.Srv().Store().Thread().UpdateMembership(tm)
|
|
if err != nil {
|
|
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
|
|
userThread, err := a.Srv().Store().Thread().GetThreadForUser(rctx, tm, true, a.IsPostPriorityEnabled())
|
|
if err != nil {
|
|
var errNotFound *store.ErrNotFound
|
|
if errors.As(err, &errNotFound) {
|
|
return nil
|
|
}
|
|
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
a.sanitizeProfiles(userThread.Participants, false)
|
|
userThread.Post.SanitizeProps()
|
|
sanitizedPost, isMemberForPreviews, appErr := a.SanitizePostMetadataForUser(rctx, userThread.Post, userID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
userThread.Post = sanitizedPost
|
|
|
|
payload, jsonErr := json.Marshal(userThread)
|
|
if jsonErr != nil {
|
|
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
message.Add("thread", string(payload))
|
|
message.Add("previous_unread_replies", int64(0))
|
|
message.Add("previous_unread_mentions", int64(0))
|
|
|
|
auditRec := a.MakeAuditRecord(rctx, model.AuditEventWebsocketPost, model.AuditStatusSuccess)
|
|
defer a.LogAuditRec(rctx, auditRec, nil)
|
|
model.AddEventParameterToAuditRec(auditRec, "post_id", userThread.Post.Id)
|
|
model.AddEventParameterToAuditRec(auditRec, "user_id", userID)
|
|
model.AddEventParameterToAuditRec(auditRec, "source", "UpdateThreadFollowForUserFromChannelAdd")
|
|
if !isMemberForPreviews {
|
|
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
|
}
|
|
auditRec.Success()
|
|
|
|
a.Publish(message)
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateThreadReadForUserByPost(rctx request.CTX, currentSessionId, userID, teamID, threadID, postID string) (*model.ThreadResponse, *model.AppError) {
|
|
post, err := a.GetSinglePost(rctx, postID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if post.RootId != threadID && postID != threadID {
|
|
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user_by_post.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return a.UpdateThreadReadForUser(rctx, currentSessionId, userID, teamID, threadID, post.CreateAt-1)
|
|
}
|
|
|
|
func (a *App) UpdateThreadReadForUser(rctx request.CTX, currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the thread doesn't have a membership, we shouldn't try to mark it as unread
|
|
membership, err := a.GetThreadMembershipForUser(userID, threadID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
previousUnreadMentions := membership.UnreadMentions
|
|
previousUnreadReplies, nErr := a.Srv().Store().Thread().GetThreadUnreadReplyCount(membership)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
post, err := a.GetSinglePost(rctx, threadID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
membership.UnreadMentions, err = a.countThreadMentions(rctx, user, post, teamID, timestamp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, nErr = a.Srv().Store().Thread().UpdateMembership(membership)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
membership.LastViewed = timestamp
|
|
|
|
nErr = a.Srv().Store().Thread().MarkAsRead(userID, threadID, timestamp)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
thread, err := a.GetThreadForUser(rctx, membership, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Clear if user has read the messages
|
|
if thread.UnreadReplies == 0 && a.IsCRTEnabledForUser(rctx, userID) {
|
|
a.clearPushNotification(currentSessionId, userID, post.ChannelId, threadID)
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, teamID, "", userID, nil, "")
|
|
message.Add("thread_id", threadID)
|
|
message.Add("timestamp", timestamp)
|
|
message.Add("unread_mentions", membership.UnreadMentions)
|
|
message.Add("unread_replies", thread.UnreadReplies)
|
|
message.Add("previous_unread_mentions", previousUnreadMentions)
|
|
message.Add("previous_unread_replies", previousUnreadReplies)
|
|
message.Add("channel_id", post.ChannelId)
|
|
a.Publish(message)
|
|
return thread, nil
|
|
}
|
|
|
|
func (a *App) GetUsersWithInvalidEmails(page int, perPage int) ([]*model.User, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetUsersWithInvalidEmails(page, perPage, *a.Config().TeamSettings.RestrictCreationToDomains)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUsersPage", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func getProfileImagePath(userID string) string {
|
|
return filepath.Join("users", userID, "profile.png")
|
|
}
|
|
|
|
func getProfileImageDirectory(userID string) string {
|
|
return filepath.Join("users", userID)
|
|
}
|
|
|
|
func (a *App) UserIsFirstAdmin(rctx request.CTX, user *model.User) bool {
|
|
if !user.IsSystemAdmin() {
|
|
return false
|
|
}
|
|
|
|
systemAdminUsers, errServer := a.Srv().Store().User().GetSystemAdminProfiles()
|
|
if errServer != nil {
|
|
rctx.Logger().Warn("Failed to get system admins to check for first admin from Mattermost.")
|
|
return false
|
|
}
|
|
|
|
for _, systemAdminUser := range systemAdminUsers {
|
|
if systemAdminUser.CreateAt < user.CreateAt {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (a *App) ResetPasswordFailedAttempts(rctx request.CTX, user *model.User) *model.AppError {
|
|
err := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0)
|
|
if err != nil {
|
|
return model.NewAppError("ResetPasswordFailedAttempts", "app.user.reset_password_failed_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|