summaryrefslogtreecommitdiffstats
path: root/api/user.go
diff options
context:
space:
mode:
Diffstat (limited to 'api/user.go')
-rw-r--r--api/user.go1258
1 files changed, 1258 insertions, 0 deletions
diff --git a/api/user.go b/api/user.go
new file mode 100644
index 000000000..c0ebc05e0
--- /dev/null
+++ b/api/user.go
@@ -0,0 +1,1258 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "bytes"
+ "code.google.com/p/draw2d/draw2d"
+ l4g "code.google.com/p/log4go"
+ "fmt"
+ "github.com/goamz/goamz/aws"
+ "github.com/goamz/goamz/s3"
+ "github.com/gorilla/mux"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+ "github.com/mssola/user_agent"
+ "github.com/nfnt/resize"
+ "hash/fnv"
+ "image"
+ "image/color"
+ _ "image/gif"
+ _ "image/jpeg"
+ "image/png"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+func InitUser(r *mux.Router) {
+ l4g.Debug("Initializing user api routes")
+
+ sr := r.PathPrefix("/users").Subrouter()
+ sr.Handle("/create", ApiAppHandler(createUser)).Methods("POST")
+ sr.Handle("/update", ApiUserRequired(updateUser)).Methods("POST")
+ sr.Handle("/update_roles", ApiUserRequired(updateRoles)).Methods("POST")
+ sr.Handle("/update_active", ApiUserRequired(updateActive)).Methods("POST")
+ sr.Handle("/update_notify", ApiUserRequired(updateUserNotify)).Methods("POST")
+ sr.Handle("/newpassword", ApiUserRequired(updatePassword)).Methods("POST")
+ sr.Handle("/send_password_reset", ApiAppHandler(sendPasswordReset)).Methods("POST")
+ sr.Handle("/reset_password", ApiAppHandler(resetPassword)).Methods("POST")
+ sr.Handle("/login", ApiAppHandler(login)).Methods("POST")
+ sr.Handle("/logout", ApiUserRequired(logout)).Methods("POST")
+ sr.Handle("/revoke_session", ApiUserRequired(revokeSession)).Methods("POST")
+
+ sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
+
+ sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
+ sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET")
+ sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
+ sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET")
+ sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET")
+ sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET")
+ sr.Handle("/{id:[A-Za-z0-9]+}/image", ApiUserRequired(getProfileImage)).Methods("GET")
+}
+
+func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ user := model.UserFromJson(r.Body)
+
+ if user == nil {
+ c.SetInvalidParam("createUser", "user")
+ return
+ }
+
+ if !model.IsUsernameValid(user.Username) {
+ c.Err = model.NewAppError("createUser", "That username is invalid", "might be using a resrved username")
+ return
+ }
+
+ user.EmailVerified = false
+
+ var team *model.Team
+
+ if result := <-Srv.Store.Team().Get(user.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ hash := r.URL.Query().Get("h")
+
+ shouldVerifyHash := true
+
+ if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 {
+ domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1))))
+
+ matched := false
+ for _, d := range domains {
+ if strings.HasSuffix(user.Email, "@"+d) {
+ matched = true
+ break
+ }
+ }
+
+ if matched {
+ shouldVerifyHash = false
+ } else {
+ c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "allowed domains failed")
+ return
+ }
+ }
+
+ if team.Type == model.TEAM_OPEN {
+ shouldVerifyHash = false
+ }
+
+ if len(hash) > 0 {
+ shouldVerifyHash = true
+ }
+
+ if shouldVerifyHash {
+ data := r.URL.Query().Get("d")
+ props := model.MapFromJson(strings.NewReader(data))
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) {
+ c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "")
+ return
+ }
+
+ t, err := strconv.ParseInt(props["time"], 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
+ c.Err = model.NewAppError("createUser", "The signup link has expired", "")
+ return
+ }
+
+ if user.TeamId != props["id"] {
+ c.Err = model.NewAppError("createUser", "Invalid team name", data)
+ return
+ }
+
+ user.Email = props["email"]
+ user.EmailVerified = true
+ }
+
+ ruser := CreateUser(c, team, user)
+ if c.Err != nil {
+ return
+ }
+
+ w.Write([]byte(ruser.ToJson()))
+
+}
+
+func CreateValet(c *Context, team *model.Team) *model.User {
+ valet := &model.User{}
+ valet.TeamId = team.Id
+ valet.Email = utils.Cfg.EmailSettings.FeedbackEmail
+ valet.EmailVerified = true
+ valet.Username = model.BOT_USERNAME
+ valet.Password = model.NewId()
+
+ return CreateUser(c, team, valet)
+}
+
+func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
+
+ channelRole := ""
+ if team.Email == user.Email {
+ user.Roles = model.ROLE_ADMIN
+ channelRole = model.CHANNEL_ROLE_ADMIN
+ } else {
+ user.Roles = ""
+ }
+
+ user.MakeNonNil()
+ if len(user.Props["theme"]) == 0 {
+ user.AddProp("theme", utils.Cfg.TeamSettings.DefaultThemeColor)
+ }
+
+ if result := <-Srv.Store.User().Save(user); result.Err != nil {
+ c.Err = result.Err
+ return nil
+ } else {
+ ruser := result.Data.(*model.User)
+
+ // Do not error if user cannot be added to the town-square channel
+ if cresult := <-Srv.Store.Channel().GetByName(team.Id, "town-square"); cresult.Err != nil {
+ l4g.Error("Failed to get town-square err=%v", cresult.Err)
+ } else {
+ cm := &model.ChannelMember{ChannelId: cresult.Data.(*model.Channel).Id, UserId: ruser.Id, NotifyLevel: model.CHANNEL_NOTIFY_ALL, Roles: channelRole}
+ if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
+ l4g.Error("Failed to add member town-square err=%v", cmresult.Err)
+ }
+ }
+
+ //fireAndForgetWelcomeEmail(strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl+"/channels/town-square")
+
+ if user.EmailVerified {
+ if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
+ l4g.Error("Failed to get town-square err=%v", cresult.Err)
+ }
+ } else {
+ FireAndForgetVerifyEmail(result.Data.(*model.User).Id, strings.Split(ruser.FullName, " ")[0], ruser.Email, team.Name, c.TeamUrl)
+ }
+
+ ruser.Sanitize(map[string]bool{})
+
+ //This message goes to every channel, so the channelId is irrelevant
+ message := model.NewMessage(team.Id, "", ruser.Id, model.ACTION_NEW_USER)
+
+ store.PublishAndForget(message)
+
+ return ruser
+ }
+}
+
+func fireAndForgetWelcomeEmail(name, email, teamName, link string) {
+ go func() {
+
+ subjectPage := NewServerTemplatePage("welcome_subject", link)
+ bodyPage := NewServerTemplatePage("welcome_body", link)
+ bodyPage.Props["FullName"] = name
+ bodyPage.Props["TeamName"] = teamName
+ bodyPage.Props["FeedbackName"] = utils.Cfg.EmailSettings.FeedbackName
+
+ if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("Failed to send welcome email successfully err=%v", err)
+ }
+
+ }()
+}
+
+func FireAndForgetVerifyEmail(userId, name, email, teamName, teamUrl string) {
+ go func() {
+
+ link := fmt.Sprintf("%s/verify?uid=%s&hid=%s", teamUrl, userId, model.HashPassword(userId))
+
+ subjectPage := NewServerTemplatePage("verify_subject", teamUrl)
+ subjectPage.Props["TeamName"] = teamName
+ bodyPage := NewServerTemplatePage("verify_body", teamUrl)
+ bodyPage.Props["FullName"] = name
+ bodyPage.Props["TeamName"] = teamName
+ bodyPage.Props["VerifyUrl"] = link
+
+ if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("Failed to send verification email successfully err=%v", err)
+ }
+ }()
+}
+
+func login(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ extraInfo := ""
+ var result store.StoreResult
+
+ if len(props["id"]) != 0 {
+ extraInfo = props["id"]
+ if result = <-Srv.Store.User().Get(props["id"]); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+ }
+
+ var team *model.Team
+ if result.Data == nil && len(props["email"]) != 0 && len(props["domain"]) != 0 {
+ extraInfo = props["email"] + " in " + props["domain"]
+
+ if nr := <-Srv.Store.Team().GetByDomain(props["domain"]); nr.Err != nil {
+ c.Err = nr.Err
+ return
+ } else {
+ team = nr.Data.(*model.Team)
+
+ if result = <-Srv.Store.User().GetByEmail(team.Id, props["email"]); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+ }
+ }
+
+ if result.Data == nil {
+ c.Err = model.NewAppError("login", "Login failed because we couldn't find a valid account", extraInfo)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ user := result.Data.(*model.User)
+
+ if team == nil {
+ if tResult := <-Srv.Store.Team().Get(user.TeamId); tResult.Err != nil {
+ c.Err = tResult.Err
+ return
+ } else {
+ team = tResult.Data.(*model.Team)
+ }
+ }
+
+ c.LogAuditWithUserId(user.Id, "attempt")
+
+ if !model.ComparePassword(user.Password, props["password"]) {
+ c.LogAuditWithUserId(user.Id, "fail")
+ c.Err = model.NewAppError("login", "Login failed because of invalid password", extraInfo)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ if !user.EmailVerified {
+ c.Err = model.NewAppError("login", "Login failed because email address has not been verified", extraInfo)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ if user.DeleteAt > 0 {
+ c.Err = model.NewAppError("login", "Login failed because your account has been set to inactive. Please contact an administrator.", extraInfo)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ session := &model.Session{UserId: user.Id, TeamId: team.Id, Roles: user.Roles, DeviceId: props["device_id"]}
+
+ maxAge := model.SESSION_TIME_WEB_IN_SECS
+
+ if len(props["device_id"]) > 0 {
+ session.SetExpireInDays(model.SESSION_TIME_MOBILE_IN_DAYS)
+ maxAge = model.SESSION_TIME_MOBILE_IN_SECS
+ } else {
+ session.SetExpireInDays(model.SESSION_TIME_WEB_IN_DAYS)
+ }
+
+ ua := user_agent.New(r.UserAgent())
+
+ plat := ua.Platform()
+ if plat == "" {
+ plat = "unknown"
+ }
+
+ os := ua.OS()
+ if os == "" {
+ os = "unknown"
+ }
+
+ bname, bversion := ua.Browser()
+ if bname == "" {
+ bname = "unknown"
+ }
+
+ if bversion == "" {
+ bversion = "0.0"
+ }
+
+ session.AddProp(model.SESSION_PROP_PLATFORM, plat)
+ session.AddProp(model.SESSION_PROP_OS, os)
+ session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion))
+
+ if result := <-Srv.Store.Session().Save(session); result.Err != nil {
+ c.Err = result.Err
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ } else {
+ session = result.Data.(*model.Session)
+ sessionCache.Add(session.Id, session)
+ }
+
+ w.Header().Set(model.HEADER_TOKEN, session.Id)
+ sessionCookie := &http.Cookie{
+ Name: model.SESSION_TOKEN,
+ Value: session.Id,
+ Path: "/",
+ MaxAge: maxAge,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, sessionCookie)
+ user.Sanitize(map[string]bool{})
+
+ c.Session = *session
+ c.LogAuditWithUserId(user.Id, "success")
+
+ w.Write([]byte(result.Data.(*model.User).ToJson()))
+}
+
+func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+ altId := props["id"]
+
+ if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ sessions := result.Data.([]*model.Session)
+
+ for _, session := range sessions {
+ if session.AltId == altId {
+ c.LogAudit("session_id=" + session.AltId)
+ sessionCache.Remove(session.Id)
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(model.MapToJson(props)))
+ return
+ }
+ }
+ }
+ }
+}
+
+func RevokeAllSession(c *Context, userId string) {
+ if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ sessions := result.Data.([]*model.Session)
+
+ for _, session := range sessions {
+ c.LogAuditWithUserId(userId, "session_id="+session.AltId)
+ sessionCache.Remove(session.Id)
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+ }
+ }
+}
+
+func getSessions(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ params := mux.Vars(r)
+ id := params["id"]
+
+ if !c.HasPermissionsToUser(id, "getAudits") {
+ return
+ }
+
+ if result := <-Srv.Store.Session().GetSessions(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ sessions := result.Data.([]*model.Session)
+ for _, session := range sessions {
+ session.Sanitize()
+ }
+
+ w.Write([]byte(model.SessionsToJson(sessions)))
+ }
+}
+
+func logout(c *Context, w http.ResponseWriter, r *http.Request) {
+ data := make(map[string]string)
+ data["user_id"] = c.Session.UserId
+
+ Logout(c, w, r)
+ if c.Err == nil {
+ w.Write([]byte(model.MapToJson(data)))
+ }
+}
+
+func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.LogAudit("")
+ c.RemoveSessionCookie(w)
+ if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+}
+
+func getMe(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ if len(c.Session.UserId) == 0 {
+ return
+ }
+
+ if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ c.RemoveSessionCookie(w)
+ l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
+ return
+ } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) {
+ return
+ } else {
+ result.Data.(*model.User).Sanitize(map[string]bool{})
+ w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag())
+ w.Header().Set("Expires", "-1")
+ w.Write([]byte(result.Data.(*model.User).ToJson()))
+ return
+ }
+}
+
+func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id := params["id"]
+
+ if !c.HasPermissionsToUser(id, "getUser") {
+ return
+ }
+
+ if result := <-Srv.Store.User().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) {
+ return
+ } else {
+ result.Data.(*model.User).Sanitize(map[string]bool{})
+ w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag())
+ w.Write([]byte(result.Data.(*model.User).ToJson()))
+ return
+ }
+}
+
+func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ etag := (<-Srv.Store.User().GetEtagForProfiles(c.Session.TeamId)).Data.(string)
+ if HandleEtag(etag, w, r) {
+ return
+ }
+
+ if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.(map[string]*model.User)
+
+ for k, p := range profiles {
+ options := utils.SanitizeOptions
+ options["passwordupdate"] = false
+ p.Sanitize(options)
+ profiles[k] = p
+ }
+
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ w.Header().Set("Cache-Control", "max-age=120, public") // 2 mins
+ w.Write([]byte(model.UserMapToJson(profiles)))
+ return
+ }
+}
+
+func getAudits(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id := params["id"]
+
+ if !c.HasPermissionsToUser(id, "getAudits") {
+ return
+ }
+
+ userChan := Srv.Store.User().Get(id)
+ auditChan := Srv.Store.Audit().Get(id, 20)
+
+ if c.Err = (<-userChan).Err; c.Err != nil {
+ return
+ }
+
+ if result := <-auditChan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ audits := result.Data.(model.Audits)
+ etag := audits.Etag()
+
+ if HandleEtag(etag, w, r) {
+ return
+ }
+
+ if len(etag) > 0 {
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ }
+
+ w.Write([]byte(audits.ToJson()))
+ return
+ }
+}
+
+func createProfileImage(username string, userId string) *image.RGBA {
+
+ colors := []color.NRGBA{
+ {197, 8, 126, 255},
+ {227, 207, 18, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ }
+
+ h := fnv.New32a()
+ h.Write([]byte(userId))
+ seed := h.Sum32()
+
+ initials := ""
+ parts := strings.Split(username, " ")
+
+ for _, v := range parts {
+
+ if len(v) > 0 {
+ initials += string(strings.ToUpper(v)[0])
+ }
+ }
+
+ if len(initials) == 0 {
+ initials = "^"
+ }
+
+ if len(initials) > 2 {
+ initials = initials[0:2]
+ }
+
+ draw2d.SetFontFolder(utils.FindDir("web/static/fonts"))
+ i := image.NewRGBA(image.Rect(0, 0, 128, 128))
+ gc := draw2d.NewGraphicContext(i)
+ draw2d.Rect(gc, 0, 0, 128, 128)
+ gc.SetFillColor(colors[int(seed)%len(colors)])
+ gc.Fill()
+ gc.SetFontSize(50)
+ gc.SetFontData(draw2d.FontData{"luxi", draw2d.FontFamilyMono, draw2d.FontStyleBold | draw2d.FontStyleItalic})
+ left, top, right, bottom := gc.GetStringBounds("CH")
+ width := (128 - (right - left + 10)) / 2
+ height := (128 - (top - bottom + 6)) / 2
+ gc.Translate(width, height)
+ gc.SetFillColor(image.White)
+ gc.FillString(initials)
+ return i
+}
+
+func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.IsS3Configured() {
+ c.Err = model.NewAppError("getProfileImage", "Unable to get image. Amazon S3 not configured. ", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ params := mux.Vars(r)
+ id := params["id"]
+
+ if result := <-Srv.Store.User().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ var auth aws.Auth
+ auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
+ auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+
+ s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
+ bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+
+ path := "teams/" + c.Session.TeamId + "/users/" + id + "/profile.png"
+
+ var img []byte
+
+ if data, getErr := bucket.Get(path); getErr != nil {
+ rawImg := createProfileImage(result.Data.(*model.User).Username, id)
+ buf := new(bytes.Buffer)
+
+ if imgErr := png.Encode(buf, rawImg); imgErr != nil {
+ c.Err = model.NewAppError("getProfileImage", "Could not encode default profile image", imgErr.Error())
+ return
+ } else {
+ img = buf.Bytes()
+ }
+
+ options := s3.Options{}
+ if err := bucket.Put(path, buf.Bytes(), "image", s3.Private, options); err != nil {
+ c.Err = model.NewAppError("getImage", "Couldn't upload default profile image", err.Error())
+ return
+ }
+
+ } else {
+ img = data
+ }
+
+ if c.Session.UserId == id {
+ w.Header().Set("Cache-Control", "max-age=300, public") // 5 mins
+ } else {
+ w.Header().Set("Cache-Control", "max-age=86400, public") // 24 hrs
+ }
+
+ w.Write(img)
+ }
+}
+
+func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.IsS3Configured() {
+ c.Err = model.NewAppError("uploadProfileImage", "Unable to upload image. Amazon S3 not configured. ", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ if err := r.ParseMultipartForm(10000000); err != nil {
+ c.Err = model.NewAppError("uploadProfileImage", "Could not parse multipart form", "")
+ return
+ }
+
+ var auth aws.Auth
+ auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId
+ auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey
+
+ s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region])
+ bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket)
+
+ m := r.MultipartForm
+
+ imageArray, ok := m.File["image"]
+ if !ok {
+ c.Err = model.NewAppError("uploadProfileImage", "No file under 'image' in request", "")
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ if len(imageArray) <= 0 {
+ c.Err = model.NewAppError("uploadProfileImage", "Empty array under 'image' in request", "")
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ imageData := imageArray[0]
+
+ file, err := imageData.Open()
+ defer file.Close()
+ if err != nil {
+ c.Err = model.NewAppError("uploadProfileImage", "Could not open image file", err.Error())
+ return
+ }
+
+ // Decode image into Image object
+ img, _, err := image.Decode(file)
+ if err != nil {
+ c.Err = model.NewAppError("uploadProfileImage", "Could not decode profile image", err.Error())
+ return
+ }
+
+ // Scale profile image
+ img = resize.Resize(utils.Cfg.ImageSettings.ProfileWidth, utils.Cfg.ImageSettings.ProfileHeight, img, resize.Lanczos3)
+
+ buf := new(bytes.Buffer)
+ err = png.Encode(buf, img)
+ if err != nil {
+ c.Err = model.NewAppError("uploadProfileImage", "Could not encode profile image", err.Error())
+ return
+ }
+
+ path := "teams/" + c.Session.TeamId + "/users/" + c.Session.UserId + "/profile.png"
+
+ options := s3.Options{}
+ if err := bucket.Put(path, buf.Bytes(), "image", s3.Private, options); err != nil {
+ c.Err = model.NewAppError("uploadProfileImage", "Couldn't upload profile image", "")
+ return
+ }
+
+ c.LogAudit("")
+}
+
+func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
+ user := model.UserFromJson(r.Body)
+
+ if user == nil {
+ c.SetInvalidParam("updateUser", "user")
+ return
+ }
+
+ if !c.HasPermissionsToUser(user.Id, "updateUsers") {
+ return
+ }
+
+ if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAudit("")
+
+ rusers := result.Data.([2]*model.User)
+
+ if rusers[0].Email != rusers[1].Email {
+ if tresult := <-Srv.Store.Team().Get(rusers[1].TeamId); tresult.Err != nil {
+ l4g.Error(tresult.Err.Message)
+ } else {
+ fireAndForgetEmailChangeEmail(rusers[1].Email, tresult.Data.(*model.Team).Name, c.TeamUrl)
+ }
+ }
+
+ rusers[0].Password = ""
+ rusers[0].AuthData = ""
+ w.Write([]byte(rusers[0].ToJson()))
+ }
+}
+
+func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
+ c.LogAudit("attempted")
+
+ props := model.MapFromJson(r.Body)
+ userId := props["user_id"]
+ if len(userId) != 26 {
+ c.SetInvalidParam("updatePassword", "user_id")
+ return
+ }
+
+ currentPassword := props["current_password"]
+ if len(currentPassword) <= 0 {
+ c.SetInvalidParam("updatePassword", "current_password")
+ return
+ }
+
+ newPassword := props["new_password"]
+ if len(newPassword) < 5 {
+ c.SetInvalidParam("updatePassword", "new_password")
+ return
+ }
+
+ if userId != c.Session.UserId {
+ c.Err = model.NewAppError("updatePassword", "Update password failed because context user_id did not match props user_id", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ var result store.StoreResult
+
+ if result = <-Srv.Store.User().Get(userId); result.Err != nil {
+ c.Err = result.Err
+ return
+ }
+
+ if result.Data == nil {
+ c.Err = model.NewAppError("updatePassword", "Update password failed because we couldn't find a valid account", "")
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ user := result.Data.(*model.User)
+
+ tchan := Srv.Store.Team().Get(user.TeamId)
+
+ if !model.ComparePassword(user.Password, currentPassword) {
+ c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "")
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ if uresult := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(newPassword)); uresult.Err != nil {
+ c.Err = uresult.Err
+ return
+ } else {
+ c.LogAudit("completed")
+
+ if tresult := <-tchan; tresult.Err != nil {
+ l4g.Error(tresult.Err.Message)
+ } else {
+ fireAndForgetPasswordChangeEmail(user.Email, tresult.Data.(*model.Team).Name, c.TeamUrl, "using the settings menu")
+ }
+
+ data := make(map[string]string)
+ data["user_id"] = uresult.Data.(string)
+ w.Write([]byte(model.MapToJson(data)))
+ }
+}
+
+func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ user_id := props["user_id"]
+ if len(user_id) != 26 {
+ c.SetInvalidParam("updateRoles", "user_id")
+ return
+ }
+
+ new_roles := props["new_roles"]
+ // no check since we allow the clearing of Roles
+
+ var user *model.User
+ if result := <-Srv.Store.User().Get(user_id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if !c.HasPermissionsToTeam(user.TeamId, "updateRoles") {
+ return
+ }
+
+ if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() {
+ c.Err = model.NewAppError("updateRoles", "You do not have the appropriate permissions", "userId="+user_id)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ // make sure there is at least 1 other active admin
+ if strings.Contains(user.Roles, model.ROLE_ADMIN) && !strings.Contains(new_roles, model.ROLE_ADMIN) {
+ if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ activeAdmins := -1
+ profileUsers := result.Data.(map[string]*model.User)
+ for _, profileUser := range profileUsers {
+ if profileUser.DeleteAt == 0 && strings.Contains(profileUser.Roles, model.ROLE_ADMIN) {
+ activeAdmins = activeAdmins + 1
+ }
+ }
+
+ if activeAdmins <= 0 {
+ c.Err = model.NewAppError("updateRoles", "There must be at least one active admin", "userId="+user_id)
+ return
+ }
+ }
+ }
+
+ user.Roles = new_roles
+
+ if result := <-Srv.Store.User().Update(user, true); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAuditWithUserId(user.Id, "roles="+new_roles)
+
+ ruser := result.Data.([2]*model.User)[0]
+ options := utils.SanitizeOptions
+ options["passwordupdate"] = false
+ ruser.Sanitize(options)
+ w.Write([]byte(ruser.ToJson()))
+ }
+}
+
+func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ user_id := props["user_id"]
+ if len(user_id) != 26 {
+ c.SetInvalidParam("updateActive", "user_id")
+ return
+ }
+
+ active := props["active"] == "true"
+
+ var user *model.User
+ if result := <-Srv.Store.User().Get(user_id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if !c.HasPermissionsToTeam(user.TeamId, "updateActive") {
+ return
+ }
+
+ if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() {
+ c.Err = model.NewAppError("updateActive", "You do not have the appropriate permissions", "userId="+user_id)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ // make sure there is at least 1 other active admin
+ if !active && strings.Contains(user.Roles, model.ROLE_ADMIN) {
+ if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ activeAdmins := -1
+ profileUsers := result.Data.(map[string]*model.User)
+ for _, profileUser := range profileUsers {
+ if profileUser.DeleteAt == 0 && strings.Contains(profileUser.Roles, model.ROLE_ADMIN) {
+ activeAdmins = activeAdmins + 1
+ }
+ }
+
+ if activeAdmins <= 0 {
+ c.Err = model.NewAppError("updateRoles", "There must be at least one active admin", "userId="+user_id)
+ return
+ }
+ }
+ }
+
+ if active {
+ user.DeleteAt = 0
+ } else {
+ user.DeleteAt = model.GetMillis()
+ }
+
+ if result := <-Srv.Store.User().Update(user, true); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAuditWithUserId(user.Id, fmt.Sprintf("active=%v", active))
+
+ if user.DeleteAt > 0 {
+ RevokeAllSession(c, user.Id)
+ }
+
+ ruser := result.Data.([2]*model.User)[0]
+ options := utils.SanitizeOptions
+ options["passwordupdate"] = false
+ ruser.Sanitize(options)
+ w.Write([]byte(ruser.ToJson()))
+ }
+}
+
+func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ email := props["email"]
+ if len(email) == 0 {
+ c.SetInvalidParam("sendPasswordReset", "email")
+ return
+ }
+
+ domain := props["domain"]
+ if len(domain) == 0 {
+ c.SetInvalidParam("sendPasswordReset", "domain")
+ return
+ }
+
+ var team *model.Team
+ if result := <-Srv.Store.Team().GetByDomain(domain); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
+ c.Err = model.NewAppError("sendPasswordReset", "We couldn’t find an account with that address.", "email="+email+" team_id="+team.Id)
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ newProps := make(map[string]string)
+ newProps["user_id"] = user.Id
+ newProps["time"] = fmt.Sprintf("%v", model.GetMillis())
+
+ data := model.MapToJson(newProps)
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.ResetSalt))
+
+ link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.TeamUrl, url.QueryEscape(data), url.QueryEscape(hash))
+
+ subjectPage := NewServerTemplatePage("reset_subject", c.TeamUrl)
+ bodyPage := NewServerTemplatePage("reset_body", c.TeamUrl)
+ bodyPage.Props["ResetUrl"] = link
+
+ if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
+ c.Err = model.NewAppError("sendPasswordReset", "Failed to send password reset email successfully", "err="+err.Message)
+ return
+ }
+
+ c.LogAuditWithUserId(user.Id, "sent="+email)
+
+ w.Write([]byte(model.MapToJson(props)))
+}
+
+func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ newPassword := props["new_password"]
+ if len(newPassword) < 5 {
+ c.SetInvalidParam("resetPassword", "new_password")
+ return
+ }
+
+ hash := props["hash"]
+ if len(hash) == 0 {
+ c.SetInvalidParam("resetPassword", "hash")
+ return
+ }
+
+ data := model.MapFromJson(strings.NewReader(props["data"]))
+
+ userId := data["user_id"]
+ if len(userId) != 26 {
+ c.SetInvalidParam("resetPassword", "data:user_id")
+ return
+ }
+
+ timeStr := data["time"]
+ if len(timeStr) == 0 {
+ c.SetInvalidParam("resetPassword", "data:time")
+ return
+ }
+
+ domain := props["domain"]
+ if len(domain) == 0 {
+ c.SetInvalidParam("resetPassword", "domain")
+ return
+ }
+
+ c.LogAuditWithUserId(userId, "attempt")
+
+ var team *model.Team
+ if result := <-Srv.Store.Team().GetByDomain(domain); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-Srv.Store.User().Get(userId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if user.TeamId != team.Id {
+ c.Err = model.NewAppError("resetPassword", "Trying to reset password for user on wrong team.", "userId="+user.Id+", teamId="+team.Id)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.ServiceSettings.ResetSalt)) {
+ c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
+ return
+ }
+
+ t, err := strconv.ParseInt(timeStr, 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
+ c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
+ return
+ }
+
+ if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAuditWithUserId(userId, "success")
+ }
+
+ fireAndForgetPasswordChangeEmail(user.Email, team.Name, c.TeamUrl, "using a reset password link")
+
+ props["new_password"] = ""
+ w.Write([]byte(model.MapToJson(props)))
+}
+
+func fireAndForgetPasswordChangeEmail(email, teamName, teamUrl, method string) {
+ go func() {
+
+ subjectPage := NewServerTemplatePage("password_change_subject", teamUrl)
+ subjectPage.Props["TeamName"] = teamName
+ bodyPage := NewServerTemplatePage("password_change_body", teamUrl)
+ bodyPage.Props["TeamName"] = teamName
+ bodyPage.Props["Method"] = method
+
+ if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("Failed to send update password email successfully err=%v", err)
+ }
+
+ }()
+}
+
+func fireAndForgetEmailChangeEmail(email, teamName, teamUrl string) {
+ go func() {
+
+ subjectPage := NewServerTemplatePage("email_change_subject", teamUrl)
+ subjectPage.Props["TeamName"] = teamName
+ bodyPage := NewServerTemplatePage("email_change_body", teamUrl)
+ bodyPage.Props["TeamName"] = teamName
+
+ if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("Failed to send update password email successfully err=%v", err)
+ }
+
+ }()
+}
+
+func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+
+ user_id := props["user_id"]
+ if len(user_id) != 26 {
+ c.SetInvalidParam("updateUserNotify", "user_id")
+ return
+ }
+
+ uchan := Srv.Store.User().Get(user_id)
+
+ if !c.HasPermissionsToUser(user_id, "updateUserNotify") {
+ return
+ }
+
+ delete(props, "user_id")
+
+ email := props["email"]
+ if len(email) == 0 {
+ c.SetInvalidParam("updateUserNotify", "email")
+ return
+ }
+
+ desktop_sound := props["desktop_sound"]
+ if len(desktop_sound) == 0 {
+ c.SetInvalidParam("updateUserNotify", "desktop_sound")
+ return
+ }
+
+ desktop := props["desktop"]
+ if len(desktop) == 0 {
+ c.SetInvalidParam("updateUserNotify", "desktop")
+ return
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ user.NotifyProps = props
+
+ if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAuditWithUserId(user.Id, "")
+
+ ruser := result.Data.([2]*model.User)[0]
+ options := utils.SanitizeOptions
+ options["passwordupdate"] = false
+ ruser.Sanitize(options)
+ w.Write([]byte(ruser.ToJson()))
+ }
+}
+
+func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
+
+ if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+
+ profiles := result.Data.(map[string]*model.User)
+
+ statuses := map[string]string{}
+ for _, profile := range profiles {
+ if profile.IsOffline() {
+ statuses[profile.Id] = model.USER_OFFLINE
+ } else if profile.IsAway() {
+ statuses[profile.Id] = model.USER_AWAY
+ } else {
+ statuses[profile.Id] = model.USER_ONLINE
+ }
+ }
+
+ //w.Header().Set("Cache-Control", "max-age=9, public") // 2 mins
+ w.Write([]byte(model.MapToJson(statuses)))
+ return
+ }
+}