diff options
Diffstat (limited to 'api')
39 files changed, 760 insertions, 273 deletions
diff --git a/api/admin.go b/api/admin.go new file mode 100644 index 000000000..6d7a9028f --- /dev/null +++ b/api/admin.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "bufio" + "net/http" + "os" + + l4g "code.google.com/p/log4go" + "github.com/gorilla/mux" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitAdmin(r *mux.Router) { + l4g.Debug("Initializing admin api routes") + + sr := r.PathPrefix("/admin").Subrouter() + sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET") + sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET") +} + +func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { + + if !c.HasSystemAdminPermissions("getLogs") { + return + } + + var lines []string + + if utils.Cfg.LogSettings.FileEnable { + + file, err := os.Open(utils.Cfg.LogSettings.FileLocation) + if err != nil { + c.Err = model.NewAppError("getLogs", "Error reading log file", err.Error()) + } + + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + } else { + lines = append(lines, "") + } + + w.Write([]byte(model.ArrayToJson(lines))) +} + +func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) { + w.Write([]byte(model.MapToJson(utils.ClientProperties))) +} diff --git a/api/admin_test.go b/api/admin_test.go new file mode 100644 index 000000000..e67077c55 --- /dev/null +++ b/api/admin_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "testing" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" +) + +func TestGetLogs(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user.Id)) + + c := &Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) + + Client.LoginByEmail(team.Name, user.Email, "pwd") + + if logs, err := Client.GetLogs(); err != nil { + t.Fatal(err) + } else if len(logs.Data.([]string)) <= 0 { + t.Fatal() + } +} + +func TestGetClientProperties(t *testing.T) { + Setup() + + if _, err := Client.GetClientProperties(); err != nil { + + t.Fatal(err) + } +} diff --git a/api/api.go b/api/api.go index 9770930f7..c8f97c5af 100644 --- a/api/api.go +++ b/api/api.go @@ -16,10 +16,12 @@ var ServerTemplates *template.Template type ServerTemplatePage Page -func NewServerTemplatePage(templateName, siteURL string) *ServerTemplatePage { - props := make(map[string]string) - props["AnalyticsUrl"] = utils.Cfg.ServiceSettings.AnalyticsUrl - return &ServerTemplatePage{TemplateName: templateName, SiteName: utils.Cfg.ServiceSettings.SiteName, FeedbackEmail: utils.Cfg.EmailSettings.FeedbackEmail, SiteURL: siteURL, Props: props} +func NewServerTemplatePage(templateName string) *ServerTemplatePage { + return &ServerTemplatePage{ + TemplateName: templateName, + Props: make(map[string]string), + ClientProps: utils.ClientProperties, + } } func (me *ServerTemplatePage) Render() string { @@ -40,7 +42,8 @@ func InitApi() { InitWebSocket(r) InitFile(r) InitCommand(r) - InitConfig(r) + InitAdmin(r) + InitOAuth(r) templatesDir := utils.FindDir("api/templates") l4g.Debug("Parsing server templates at %v", templatesDir) diff --git a/api/api_test.go b/api/api_test.go index 0c2e57891..642db581e 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -17,7 +17,7 @@ func Setup() { NewServer() StartServer() InitApi() - Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1") + Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port) } } diff --git a/api/auto_constants.go b/api/auto_constants.go index f80f15f2d..73ecb47f8 100644 --- a/api/auto_constants.go +++ b/api/auto_constants.go @@ -12,8 +12,8 @@ const ( USER_PASSWORD = "passwd" CHANNEL_TYPE = model.CHANNEL_OPEN FUZZ_USER_EMAIL_PREFIX_LEN = 10 - BTEST_TEAM_DISPLAY_NAME = "TestTeam" - BTEST_TEAM_NAME = "z-z-testdomaina" + BTEST_TEAM_DISPLAY_NAME = "TestTeam" + BTEST_TEAM_NAME = "z-z-testdomaina" BTEST_TEAM_EMAIL = "test@nowhere.com" BTEST_TEAM_TYPE = model.TEAM_OPEN BTEST_USER_NAME = "Mr. Testing Tester" diff --git a/api/auto_teams.go b/api/auto_teams.go index e5c772b4c..dd82abe8d 100644 --- a/api/auto_teams.go +++ b/api/auto_teams.go @@ -52,7 +52,7 @@ func (cfg *AutoTeamCreator) createRandomTeam() (*model.Team, bool) { } team := &model.Team{ DisplayName: teamDisplayName, - Name: teamName, + Name: teamName, Email: teamEmail, Type: model.TEAM_OPEN, } diff --git a/api/channel.go b/api/channel.go index b40366719..63acaa8d1 100644 --- a/api/channel.go +++ b/api/channel.go @@ -191,7 +191,7 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden return @@ -514,7 +514,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("deleteChannel", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden return @@ -756,7 +756,7 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions ", "") c.Err.StatusCode = http.StatusForbidden return diff --git a/api/channel_test.go b/api/channel_test.go index d65aff66c..7e9267192 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -62,7 +62,7 @@ func TestCreateChannel(t *testing.T) { } } - if _, err := Client.DoPost("/channels/create", "garbage"); err == nil { + if _, err := Client.DoApiPost("/channels/create", "garbage"); err == nil { t.Fatal("should have been an error") } @@ -627,7 +627,7 @@ func TestGetChannelExtraInfo(t *testing.T) { currentEtag = cache_result.Etag } - Client2 := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1") + Client2 := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port) user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "tester2@test.com", Nickname: "Tester 2", Password: "pwd"} user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User) diff --git a/api/command.go b/api/command.go index 2919e93a0..be1d3229b 100644 --- a/api/command.go +++ b/api/command.go @@ -315,7 +315,7 @@ func loadTestSetupCommand(c *Context, command *model.Command) bool { numPosts, _ = strconv.Atoi(tokens[numArgs+2]) } } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) if doTeams { if err := CreateBasicUser(client); err != nil { @@ -375,7 +375,7 @@ func loadTestUsersCommand(c *Context, command *model.Command) bool { if err == false { usersr = utils.Range{10, 15} } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) userCreator := NewAutoUserCreator(client, c.Session.TeamId) userCreator.Fuzzy = doFuzz userCreator.CreateTestUsers(usersr) @@ -405,7 +405,7 @@ func loadTestChannelsCommand(c *Context, command *model.Command) bool { if err == false { channelsr = utils.Range{20, 30} } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) client.MockSession(c.Session.Id) channelCreator := NewAutoChannelCreator(client, c.Session.TeamId) channelCreator.Fuzzy = doFuzz @@ -457,7 +457,7 @@ func loadTestPostsCommand(c *Context, command *model.Command) bool { } } - client := model.NewClient(c.GetSiteURL() + "/api/v1") + client := model.NewClient(c.GetSiteURL()) client.MockSession(c.Session.Id) testPoster := NewAutoPostCreator(client, command.ChannelId) testPoster.Fuzzy = doFuzz diff --git a/api/config.go b/api/config.go deleted file mode 100644 index 142d1ca66..000000000 --- a/api/config.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - l4g "code.google.com/p/log4go" - "encoding/json" - "github.com/gorilla/mux" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" - "net/http" - "strconv" -) - -func InitConfig(r *mux.Router) { - l4g.Debug("Initializing config api routes") - - sr := r.PathPrefix("/config").Subrouter() - sr.Handle("/get_all", ApiAppHandler(getConfig)).Methods("GET") -} - -func getConfig(c *Context, w http.ResponseWriter, r *http.Request) { - settings := make(map[string]string) - - settings["ByPassEmail"] = strconv.FormatBool(utils.Cfg.EmailSettings.ByPassEmail) - - if bytes, err := json.Marshal(settings); err != nil { - c.Err = model.NewAppError("getConfig", "Unable to marshall configuration data", err.Error()) - return - } else { - w.Write(bytes) - } -} diff --git a/api/context.go b/api/context.go index aaf304e2c..5925c817f 100644 --- a/api/context.go +++ b/api/context.go @@ -4,6 +4,7 @@ package api import ( + "fmt" "net" "net/http" "net/url" @@ -29,12 +30,9 @@ type Context struct { } type Page struct { - TemplateName string - Title string - SiteName string - FeedbackEmail string - SiteURL string - Props map[string]string + TemplateName string + Props map[string]string + ClientProps map[string]string } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -82,9 +80,36 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.RequestId = model.NewId() c.IpAddress = GetIpAddress(r) + token := "" + isTokenFromQueryString := false + + // Attempt to parse token out of the header + authHeader := r.Header.Get(model.HEADER_AUTH) + if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER { + // Default session token + token = authHeader[7:] + + } else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN { + // OAuth token + token = authHeader[6:] + } + + // Attempt to parse the token from the cookie + if len(token) == 0 { + if cookie, err := r.Cookie(model.SESSION_TOKEN); err == nil { + token = cookie.Value + } + } + + // Attempt to parse token out of the query string + if len(token) == 0 { + token = r.URL.Query().Get("access_token") + isTokenFromQueryString = true + } + protocol := "http" - // if the request came from the ELB then assume this is produciton + // If the request came from the ELB then assume this is produciton // and redirect all http requests to https if utils.Cfg.ServiceSettings.UseSSL { forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO) @@ -100,40 +125,26 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.setSiteURL(protocol + "://" + r.Host) w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) - w.Header().Set(model.HEADER_VERSION_ID, utils.Cfg.ServiceSettings.Version) + w.Header().Set(model.HEADER_VERSION_ID, utils.Cfg.ServiceSettings.Version+fmt.Sprintf(".%v", utils.CfgLastModified)) // Instruct the browser not to display us in an iframe for anti-clickjacking if !h.isApi { w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Content-Security-Policy", "frame-ancestors none") + } else { + // All api response bodies will be JSON formatted by default + w.Header().Set("Content-Type", "application/json") } - sessionId := "" - - // attempt to parse the session token from the header - if ah := r.Header.Get(model.HEADER_AUTH); ah != "" { - if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" { - sessionId = ah[7:] - } - } - - // attempt to parse the session token from the cookie - if sessionId == "" { - if cookie, err := r.Cookie(model.SESSION_TOKEN); err == nil { - sessionId = cookie.Value - } - } - - if sessionId != "" { - + if len(token) != 0 { var session *model.Session - if ts, ok := sessionCache.Get(sessionId); ok { + if ts, ok := sessionCache.Get(token); ok { session = ts.(*model.Session) } if session == nil { - if sessionResult := <-Srv.Store.Session().Get(sessionId); sessionResult.Err != nil { - c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "id="+sessionId+", err="+sessionResult.Err.DetailedError)) + if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil { + c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "token="+token+", err="+sessionResult.Err.DetailedError)) } else { session = sessionResult.Data.(*model.Session) } @@ -141,7 +152,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if session == nil || session.IsExpired() { c.RemoveSessionCookie(w) - c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "id="+sessionId) + c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } else if !session.IsOAuth && isTokenFromQueryString { + c.Err = model.NewAppError("ServeHTTP", "Session is not OAuth but token was provided in the query string", "token="+token) c.Err.StatusCode = http.StatusUnauthorized } else { c.Session = *session @@ -165,10 +179,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.SystemAdminRequired() } - if c.Err == nil && h.isUserActivity && sessionId != "" && len(c.Session.UserId) > 0 { + if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 { go func() { - if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, sessionId, model.GetMillis())).Err; err != nil { - l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", c.Session.UserId, sessionId, err) + if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Id, model.GetMillis())).Err; err != nil { + l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", c.Session.UserId, c.Session.Id, err) } }() } @@ -196,7 +210,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (c *Context) LogAudit(extraInfo string) { - audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.AltId} + audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} if r := <-Srv.Store.Audit().Save(audit); r.Err != nil { c.LogError(r.Err) } @@ -208,7 +222,7 @@ func (c *Context) LogAuditWithUserId(userId, extraInfo string) { extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.Session.UserId) } - audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.AltId} + audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} if r := <-Srv.Store.Audit().Save(audit); r.Err != nil { c.LogError(r.Err) } @@ -285,25 +299,36 @@ func (c *Context) HasPermissionsToChannel(sc store.StoreChannel, where string) b } func (c *Context) IsSystemAdmin() bool { - if strings.Contains(c.Session.Roles, model.ROLE_SYSTEM_ADMIN) && IsPrivateIpAddress(c.IpAddress) { + // TODO XXX FIXME && IsPrivateIpAddress(c.IpAddress) + if model.IsInRole(c.Session.Roles, model.ROLE_SYSTEM_ADMIN) { return true } return false } +func (c *Context) HasSystemAdminPermissions(where string) bool { + if c.IsSystemAdmin() { + return true + } + + c.Err = model.NewAppError(where, "You do not have the appropriate permissions", "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden + return false +} + func (c *Context) IsTeamAdmin(userId string) bool { if uresult := <-Srv.Store.User().Get(userId); uresult.Err != nil { c.Err = uresult.Err return false } else { user := uresult.Data.(*model.User) - return strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && user.TeamId == c.Session.TeamId + return model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && user.TeamId == c.Session.TeamId } } func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { - sessionCache.Remove(c.Session.Id) + sessionCache.Remove(c.Session.Token) cookie := &http.Cookie{ Name: model.SESSION_TOKEN, @@ -414,10 +439,10 @@ func IsBetaDomain(r *http.Request) bool { } var privateIpAddress = []*net.IPNet{ - &net.IPNet{IP: net.IPv4(10, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)}, - &net.IPNet{IP: net.IPv4(176, 16, 0, 1), Mask: net.IPv4Mask(255, 255, 0, 0)}, - &net.IPNet{IP: net.IPv4(192, 168, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 0)}, - &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 252)}, + {IP: net.IPv4(10, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)}, + {IP: net.IPv4(176, 16, 0, 1), Mask: net.IPv4Mask(255, 255, 0, 0)}, + {IP: net.IPv4(192, 168, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 0)}, + {IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 252)}, } func IsPrivateIpAddress(ipAddress string) bool { @@ -459,3 +484,7 @@ func Handle404(w http.ResponseWriter, r *http.Request) { l4g.Error("%v: code=404 ip=%v", r.URL.Path, GetIpAddress(r)) RenderWebError(err, w, r) } + +func AddSessionToCache(session *model.Session) { + sessionCache.Add(session.Token, session) +} diff --git a/api/context_test.go b/api/context_test.go index 56ccce1ee..23a5b75b9 100644 --- a/api/context_test.go +++ b/api/context_test.go @@ -53,8 +53,8 @@ func TestContext(t *testing.T) { t.Fatal("should have permissions") } - context.IpAddress = "125.0.0.1" - if context.HasPermissionsToUser("6", "") { - t.Fatal("shouldn't have permissions") - } + // context.IpAddress = "125.0.0.1" + // if context.HasPermissionsToUser("6", "") { + // t.Fatal("shouldn't have permissions") + // } } diff --git a/api/file.go b/api/file.go index 1d8244fac..c24775ee2 100644 --- a/api/file.go +++ b/api/file.go @@ -86,7 +86,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { return } - for i, _ := range files { + for i := range files { file, err := files[i].Open() defer file.Close() if err != nil { @@ -349,6 +349,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=2592000, public") w.Header().Set("Content-Length", strconv.Itoa(len(f))) + w.Header().Set("Content-Type", "") // need to provide proper Content-Type in the future w.Write(f) } diff --git a/api/oauth.go b/api/oauth.go new file mode 100644 index 000000000..26c3c5da8 --- /dev/null +++ b/api/oauth.go @@ -0,0 +1,165 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "code.google.com/p/log4go" + "fmt" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" + "net/url" +) + +func InitOAuth(r *mux.Router) { + l4g.Debug("Initializing oauth api routes") + + sr := r.PathPrefix("/oauth").Subrouter() + + sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") + sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") +} + +func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("registerOAuthApp", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + app := model.OAuthAppFromJson(r.Body) + + if app == nil { + c.SetInvalidParam("registerOAuthApp", "app") + return + } + + secret := model.NewId() + + app.ClientSecret = secret + app.CreatorId = c.Session.UserId + + if result := <-Srv.Store.OAuth().SaveApp(app); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + app.ClientSecret = secret + + c.LogAudit("client_id=" + app.Id) + + w.Write([]byte(app.ToJson())) + return + } + +} + +func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("allowOAuth", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + c.LogAudit("attempt") + + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + responseData := map[string]string{} + + responseType := r.URL.Query().Get("response_type") + if len(responseType) == 0 { + c.Err = model.NewAppError("allowOAuth", "invalid_request: Bad response_type", "") + return + } + + clientId := r.URL.Query().Get("client_id") + if len(clientId) != 26 { + c.Err = model.NewAppError("allowOAuth", "invalid_request: Bad client_id", "") + return + } + + redirectUri := r.URL.Query().Get("redirect_uri") + if len(redirectUri) == 0 { + c.Err = model.NewAppError("allowOAuth", "invalid_request: Missing or bad redirect_uri", "") + return + } + + scope := r.URL.Query().Get("scope") + state := r.URL.Query().Get("state") + + var app *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = model.NewAppError("allowOAuth", "server_error: Error accessing the database", "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !app.IsValidRedirectURL(redirectUri) { + c.LogAudit("fail - redirect_uri did not match registered callback") + c.Err = model.NewAppError("allowOAuth", "invalid_request: Supplied redirect_uri did not match registered callback_url", "") + return + } + + if responseType != model.AUTHCODE_RESPONSE_TYPE { + responseData["redirect"] = redirectUri + "?error=unsupported_response_type&state=" + state + w.Write([]byte(model.MapToJson(responseData))) + return + } + + authData := &model.AuthData{UserId: c.Session.UserId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope} + authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId)) + + if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil { + responseData["redirect"] = redirectUri + "?error=server_error&state=" + state + w.Write([]byte(model.MapToJson(responseData))) + return + } + + c.LogAudit("success") + + responseData["redirect"] = redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State) + + w.Write([]byte(model.MapToJson(responseData))) +} + +func RevokeAccessToken(token string) *model.AppError { + + schan := Srv.Store.Session().Remove(token) + sessionCache.Remove(token) + + var accessData *model.AccessData + if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error getting access token from DB before deletion", "") + } else { + accessData = result.Data.(*model.AccessData) + } + + tchan := Srv.Store.OAuth().RemoveAccessData(token) + cchan := Srv.Store.OAuth().RemoveAuthData(accessData.AuthCode) + + if result := <-tchan; result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error deleting access token from DB", "") + } + + if result := <-cchan; result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error deleting authorization code from DB", "") + } + + if result := <-schan; result.Err != nil { + return model.NewAppError("RevokeAccessToken", "Error deleting session from DB", "") + } + + return nil +} + +func GetAuthData(code string) *model.AuthData { + if result := <-Srv.Store.OAuth().GetAuthData(code); result.Err != nil { + l4g.Error("Couldn't find auth code for code=%s", code) + return nil + } else { + return result.Data.(*model.AuthData) + } +} diff --git a/api/oauth_test.go b/api/oauth_test.go new file mode 100644 index 000000000..18db49bc5 --- /dev/null +++ b/api/oauth_test.go @@ -0,0 +1,157 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "net/url" + "strings" + "testing" +) + +func TestRegisterApp(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("should have failed - oauth providing turned off") + } + + } else { + + Client.Logout() + + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("not logged in - should have failed") + } + + Client.Must(Client.LoginById(ruser.Id, "pwd")) + + if result, err := Client.RegisterApp(app); err != nil { + t.Fatal(err) + } else { + rapp := result.Data.(*model.OAuthApp) + if len(rapp.Id) != 26 { + t.Fatal("clientid didn't return properly") + } + if len(rapp.ClientSecret) != 26 { + t.Fatal("client secret didn't return properly") + } + } + + app = &model.OAuthApp{Name: "", Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing name - should have failed") + } + + app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing homepage - should have failed") + } + + app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{}} + if _, err := Client.RegisterApp(app); err == nil { + t.Fatal("missing callback url - should have failed") + } + } +} + +func TestAllowOAuth(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}} + + Client.Must(Client.LoginById(ruser.Id, "pwd")) + + state := "123" + + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "12345678901234567890123456", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - oauth service providing turned off") + } + } else { + app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp) + + if result, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err != nil { + t.Fatal(err) + } else { + redirect := result.Data.(map[string]string)["redirect"] + if len(redirect) == 0 { + t.Fatal("redirect url should be set") + } + + ru, _ := url.Parse(redirect) + if ru == nil { + t.Fatal("redirect url unparseable") + } else { + if len(ru.Query().Get("code")) == 0 { + t.Fatal("authorization code not returned") + } + if ru.Query().Get("state") != state { + t.Fatal("returned state doesn't match") + } + } + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "all", state); err == nil { + t.Fatal("should have failed - no redirect_url given") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "", state); err == nil { + t.Fatal("should have failed - no redirect_url given") + } + + if result, err := Client.AllowOAuth("junk", app.Id, app.CallbackUrls[0], "all", state); err != nil { + t.Fatal(err) + } else { + redirect := result.Data.(map[string]string)["redirect"] + if len(redirect) == 0 { + t.Fatal("redirect url should be set") + } + + ru, _ := url.Parse(redirect) + if ru == nil { + t.Fatal("redirect url unparseable") + } else { + if ru.Query().Get("error") != "unsupported_response_type" { + t.Fatal("wrong error returned") + } + if ru.Query().Get("state") != state { + t.Fatal("returned state doesn't match") + } + } + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - empty client id") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "junk", app.CallbackUrls[0], "all", state); err == nil { + t.Fatal("should have failed - bad client id") + } + + if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://somewhereelse.com", "all", state); err == nil { + t.Fatal("should have failed - redirect uri host does not match app host") + } + } +} diff --git a/api/post.go b/api/post.go index 5363fdf79..21bc35b97 100644 --- a/api/post.go +++ b/api/post.go @@ -353,7 +353,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) { } } - for id, _ := range toEmailMap { + for id := range toEmailMap { fireAndForgetMentionUpdate(post.ChannelId, id) } } @@ -378,7 +378,8 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) { location, _ := time.LoadLocation("UTC") tm := time.Unix(post.CreateAt/1000, 0).In(location) - subjectPage := NewServerTemplatePage("post_subject", siteURL) + subjectPage := NewServerTemplatePage("post_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName subjectPage.Props["SubjectText"] = subjectText subjectPage.Props["Month"] = tm.Month().String()[:3] @@ -396,7 +397,8 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) { continue } - bodyPage := NewServerTemplatePage("post_body", siteURL) + bodyPage := NewServerTemplatePage("post_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Nickname"] = profileMap[id].FirstName bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["ChannelName"] = channelName @@ -716,7 +718,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if post.UserId != c.Session.UserId && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if post.UserId != c.Session.UserId && !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden return diff --git a/api/post_test.go b/api/post_test.go index 85d92de3a..4cccfd62a 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -118,7 +118,7 @@ func TestCreatePost(t *testing.T) { t.Fatal("Should have been forbidden") } - if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { + if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { t.Fatal("should have been an error") } } @@ -203,7 +203,7 @@ func TestCreateValetPost(t *testing.T) { t.Fatal("Should have been forbidden") } - if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { + if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil { t.Fatal("should have been an error") } } else { diff --git a/api/slackimport.go b/api/slackimport.go index 1d037a934..4e6c01dbb 100644 --- a/api/slackimport.go +++ b/api/slackimport.go @@ -50,6 +50,15 @@ func SlackConvertTimeStamp(ts string) int64 { return timeStamp * 1000 // Convert to milliseconds } +func SlackConvertChannelName(channelName string) string { + newName := strings.Trim(channelName, "_-") + if len(newName) == 1 { + return "slack-channel-" + newName + } + + return newName +} + func SlackParseChannels(data io.Reader) []SlackChannel { decoder := json.NewDecoder(data) @@ -172,7 +181,7 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str TeamId: teamId, Type: model.CHANNEL_OPEN, DisplayName: sChannel.Name, - Name: sChannel.Name, + Name: SlackConvertChannelName(sChannel.Name), Description: sChannel.Topic["value"], } mChannel := ImportChannel(&newChannel) diff --git a/api/team.go b/api/team.go index e1b3b274a..92fcbff93 100644 --- a/api/team.go +++ b/api/team.go @@ -56,8 +56,10 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("signup_team_subject", c.GetSiteURL()) - bodyPage := NewServerTemplatePage("signup_team_body", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("signup_team_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage := NewServerTemplatePage("signup_team_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["TourUrl"] = utils.Cfg.TeamSettings.TourLink props := make(map[string]string) @@ -98,6 +100,10 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { return } + if !isTreamCreationAllowed(c, team.Email) { + return + } + team.PreSave() team.Name = model.CleanTeamName(team.Name) @@ -241,47 +247,55 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { } func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { + team := model.TeamFromJson(r.Body) + rteam := CreateTeam(c, team) + if c.Err != nil { + return + } + + w.Write([]byte(rteam.ToJson())) +} + +func CreateTeam(c *Context, team *model.Team) *model.Team { if utils.Cfg.ServiceSettings.DisableEmailSignUp { c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "") c.Err.StatusCode = http.StatusNotImplemented - return + return nil } - team := model.TeamFromJson(r.Body) - if team == nil { c.SetInvalidParam("createTeam", "team") - return + return nil } if !isTreamCreationAllowed(c, team.Email) { - return + return nil } if utils.Cfg.ServiceSettings.Mode != utils.MODE_DEV { - c.Err = model.NewAppError("createTeam", "The mode does not allow network creation without a valid invite", "") - return + c.Err = model.NewAppError("CreateTeam", "The mode does not allow network creation without a valid invite", "") + return nil } if result := <-Srv.Store.Team().Save(team); result.Err != nil { c.Err = result.Err - return + return nil } else { rteam := result.Data.(*model.Team) if _, err := CreateDefaultChannels(c, rteam.Id); err != nil { c.Err = err - return + return nil } if rteam.AllowValet { CreateValet(c, rteam) if c.Err != nil { - return + return nil } } - w.Write([]byte(rteam.ToJson())) + return rteam } } @@ -393,8 +407,10 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("find_teams_subject", c.GetSiteURL()) - bodyPage := NewServerTemplatePage("find_teams_body", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("find_teams_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage := NewServerTemplatePage("find_teams_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { c.Err = result.Err @@ -469,22 +485,23 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str sender := user.GetDisplayName() senderRole := "" - if strings.Contains(user.Roles, model.ROLE_ADMIN) || strings.Contains(user.Roles, model.ROLE_SYSTEM_ADMIN) { + if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) || model.IsInRole(user.Roles, model.ROLE_SYSTEM_ADMIN) { senderRole = "administrator" } else { senderRole = "member" } - subjectPage := NewServerTemplatePage("invite_subject", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("invite_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() subjectPage.Props["SenderName"] = sender subjectPage.Props["TeamDisplayName"] = team.DisplayName - bodyPage := NewServerTemplatePage("invite_body", c.GetSiteURL()) + + bodyPage := NewServerTemplatePage("invite_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["TeamDisplayName"] = team.DisplayName bodyPage.Props["SenderName"] = sender bodyPage.Props["SenderStatus"] = senderRole - bodyPage.Props["Email"] = invite - props := make(map[string]string) props["email"] = invite props["id"] = team.Id @@ -528,7 +545,7 @@ func updateTeamDisplayName(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateTeamDisplayName", "You do not have the appropriate permissions", "userId="+c.Session.UserId) c.Err.StatusCode = http.StatusForbidden return @@ -568,7 +585,7 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateValetFeature", "You do not have the appropriate permissions", "userId="+c.Session.UserId) c.Err.StatusCode = http.StatusForbidden return diff --git a/api/team_test.go b/api/team_test.go index 2723eff57..4f1b9e5f0 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -103,7 +103,7 @@ func TestCreateTeam(t *testing.T) { } } - if _, err := Client.DoPost("/teams/create", "garbage"); err == nil { + if _, err := Client.DoApiPost("/teams/create", "garbage"); err == nil { t.Fatal("should have been an error") } } diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html index c4e1cf39d..0ec4ace2a 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -23,9 +23,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -34,11 +34,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/error.html b/api/templates/error.html index adb8f9f7d..760578896 100644 --- a/api/templates/error.html +++ b/api/templates/error.html @@ -1,20 +1,30 @@ <html> <head> - <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> - <title>{{ .SiteName }} - Error</title> - <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"> - <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> - <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script> - <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600,700' rel='stylesheet' type='text/css'> - <link rel="stylesheet" href="/static/css/styles.css"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> + <title>{{ .ClientProps.SiteName }} - Error</title> + + <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css"> + <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet"> + + <script src="/static/js/react-with-addons-0.13.3.min.js"></script> + <script src="/static/js/jquery-1.11.1.min.js"></script> + <script src="/static/js/bootstrap-3.3.5.min.js"></script> + <script src="/static/js/react-bootstrap-0.25.1.min.js"></script> + + <link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon"> + <link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon"> + <link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'> + <link rel="stylesheet" href="/static/css/styles.css"> + + </head> <body class="white error"> <div class="container-fluid"> <div class="error__container"> <div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div> - <h2>{{ .SiteName }} needs your help:</h2> + <h2>{{ .ClientProps.SiteName }} needs your help:</h2> <p>{{.Message}}</p> - <a href="{{.SiteURL}}">Go back to team site</a> + <a href="{{.Props.SiteURL}}">Go back to team site</a> </div> </div> </body> diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html index 00c5628dd..9d34b7a23 100644 --- a/api/templates/find_teams_body.html +++ b/api/templates/find_teams_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -31,9 +31,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -42,11 +42,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html index e5ba2d23f..3c2bef589 100644 --- a/api/templates/find_teams_subject.html +++ b/api/templates/find_teams_subject.html @@ -1 +1 @@ -{{define "find_teams_subject"}}Your {{ .SiteName }} Teams{{end}} +{{define "find_teams_subject"}}Your {{ .ClientProps.SiteName }} Teams{{end}} diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html index 568a0d893..9e1ce33b2 100644 --- a/api/templates/invite_body.html +++ b/api/templates/invite_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -18,7 +18,7 @@ <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2> - <p>{{.Props.TeamDisplayName}} started using {{.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> + <p>{{.Props.TeamDisplayName}} started using {{.ClientProps.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> <p style="margin: 20px 0 15px"> <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a> </p> @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,11 +37,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html index 6a1e57dcc..f46bdcfaf 100644 --- a/api/templates/invite_subject.html +++ b/api/templates/invite_subject.html @@ -1 +1 @@ -{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.SiteName}}{{end}} +{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.ClientProps.SiteName}}{{end}} diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html index 6fc9f2822..3fef3a5c8 100644 --- a/api/templates/password_change_body.html +++ b/api/templates/password_change_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -23,9 +23,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -34,11 +34,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html index 55daefdb1..283fda1af 100644 --- a/api/templates/password_change_subject.html +++ b/api/templates/password_change_subject.html @@ -1 +1 @@ -{{define "password_change_subject"}}You updated your password for {{.Props.TeamDisplayName}} on {{ .SiteName }}{{end}} +{{define "password_change_subject"}}You updated your password for {{.Props.TeamDisplayName}} on {{ .ClientProps.SiteName }}{{end}} diff --git a/api/templates/post_body.html b/api/templates/post_body.html index a1df5b4c9..a6b81e2f6 100644 --- a/api/templates/post_body.html +++ b/api/templates/post_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,11 +37,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html index 7d8941549..944cd5a42 100644 --- a/api/templates/post_subject.html +++ b/api/templates/post_subject.html @@ -1 +1 @@ -{{define "post_subject"}}[{{.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}} +{{define "post_subject"}}[{{.ClientProps.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}} diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html index a6e6269c0..dc6152627 100644 --- a/api/templates/reset_body.html +++ b/api/templates/reset_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,11 +37,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html index b49cf5f36..f5c0e62b0 100644 --- a/api/templates/signup_team_body.html +++ b/api/templates/signup_team_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -21,7 +21,7 @@ <p style="margin: 20px 0 25px"> <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a> </p> - {{ .SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board.<br></p> + {{ .ClientProps.SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .ClientProps.SiteName }} when your team is in constant communication--let's get them on board.<br></p> <p> Learn more by <a href="{{.Props.TourUrl}}" style="text-decoration: none; color:#2389D7;">taking a tour</a> </p> @@ -29,9 +29,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -40,11 +40,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html index 1cd3427d2..7bc0cc640 100644 --- a/api/templates/signup_team_subject.html +++ b/api/templates/signup_team_subject.html @@ -1 +1 @@ -{{define "signup_team_subject"}}Invitation to {{ .SiteName }}{{end}}
\ No newline at end of file +{{define "signup_team_subject"}}Invitation to {{ .ClientProps.SiteName }}{{end}}
\ No newline at end of file diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html index 6ba11d845..8187c8908 100644 --- a/api/templates/verify_body.html +++ b/api/templates/verify_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -37,11 +37,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html index a66150735..7990df84a 100644 --- a/api/templates/verify_subject.html +++ b/api/templates/verify_subject.html @@ -1 +1 @@ -{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .SiteName }}] Email Verification{{end}} +{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .ClientProps.SiteName }}] Email Verification{{end}} diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html index f16f50e14..c75e14c6a 100644 --- a/api/templates/welcome_body.html +++ b/api/templates/welcome_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.SiteURL}}/static/images/{{.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.Props.SiteURL}}/static/images/{{.ClientProps.SiteName}}-logodark.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -17,15 +17,15 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You joined the {{.Props.TeamDisplayName}} team at {{.SiteName}}!</h2> - <p>Please let me know if you have any questions.<br>Enjoy your stay at <a href="{{.Props.TeamURL}}">{{.SiteName}}</a>.</p> + <h2 style="font-weight: normal; margin-top: 10px;">You joined the {{.Props.TeamDisplayName}} team at {{.ClientProps.SiteName}}!</h2> + <p>Please let me know if you have any questions.<br>Enjoy your stay at <a href="{{.Props.TeamURL}}">{{.ClientProps.SiteName}}</a>.</p> </td> </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.SiteName}} Team<br> + The {{.ClientProps.SiteName}} Team<br> </td> </tr> </table> @@ -34,11 +34,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.Props.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 SpinPunch, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html index 106cc3ae6..2214f7a38 100644 --- a/api/templates/welcome_subject.html +++ b/api/templates/welcome_subject.html @@ -1 +1 @@ -{{define "welcome_subject"}}Welcome to {{ .SiteName }}{{end}}
\ No newline at end of file +{{define "welcome_subject"}}Welcome to {{ .ClientProps.SiteName }}{{end}}
\ No newline at end of file diff --git a/api/user.go b/api/user.go index 727accd1f..0a54b6a5d 100644 --- a/api/user.go +++ b/api/user.go @@ -5,10 +5,10 @@ package api import ( "bytes" - "code.google.com/p/freetype-go/freetype" l4g "code.google.com/p/log4go" b64 "encoding/base64" "fmt" + "github.com/golang/freetype" "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" @@ -170,7 +170,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { channelRole := "" if team.Email == user.Email { - user.Roles = model.ROLE_ADMIN + user.Roles = model.ROLE_TEAM_ADMIN channelRole = model.CHANNEL_ROLE_ADMIN } else { user.Roles = "" @@ -216,8 +216,10 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { func fireAndForgetWelcomeEmail(name, email, teamDisplayName, link, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("welcome_subject", siteURL) - bodyPage := NewServerTemplatePage("welcome_body", siteURL) + subjectPage := NewServerTemplatePage("welcome_subject") + subjectPage.Props["SiteURL"] = siteURL + bodyPage := NewServerTemplatePage("welcome_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Nickname"] = name bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["FeedbackName"] = utils.Cfg.EmailSettings.FeedbackName @@ -235,9 +237,11 @@ func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, site link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) - subjectPage := NewServerTemplatePage("verify_subject", siteURL) + subjectPage := NewServerTemplatePage("verify_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("verify_body", siteURL) + bodyPage := NewServerTemplatePage("verify_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["VerifyUrl"] = link @@ -332,7 +336,7 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, return } - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId} + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false} maxAge := model.SESSION_TIME_WEB_IN_SECS @@ -374,13 +378,13 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, return } else { session = result.Data.(*model.Session) - sessionCache.Add(session.Id, session) + AddSessionToCache(session) } - w.Header().Set(model.HEADER_TOKEN, session.Id) + w.Header().Set(model.HEADER_TOKEN, session.Token) sessionCookie := &http.Cookie{ Name: model.SESSION_TOKEN, - Value: session.Id, + Value: session.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, @@ -426,25 +430,27 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) - altId := props["id"] + id := props["id"] - if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil { + if result := <-Srv.Store.Session().Get(id); result.Err != nil { c.Err = result.Err return } else { - sessions := result.Data.([]*model.Session) + session := 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 - } + c.LogAudit("session_id=" + session.Id) + + if session.IsOAuth { + RevokeAccessToken(session.Token) + } else { + sessionCache.Remove(session.Token) + + if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + c.Err = result.Err + return + } else { + w.Write([]byte(model.MapToJson(props))) + return } } } @@ -458,8 +464,8 @@ func RevokeAllSession(c *Context, userId string) { sessions := result.Data.([]*model.Session) for _, session := range sessions { - c.LogAuditWithUserId(userId, "session_id="+session.AltId) - sessionCache.Remove(session.Id) + c.LogAuditWithUserId(userId, "session_id="+session.Id) + sessionCache.Remove(session.Token) if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { c.Err = result.Err return @@ -922,7 +928,16 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { } new_roles := props["new_roles"] - // no check since we allow the clearing of Roles + if !model.IsValidRoles(new_roles) { + c.SetInvalidParam("updateRoles", "new_roles") + return + } + + if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) { + c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set from the command line", "") + c.Err.StatusCode = http.StatusForbidden + return + } var user *model.User if result := <-Srv.Store.User().Get(user_id); result.Err != nil { @@ -936,43 +951,15 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_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 - - var ruser *model.User - if result := <-Srv.Store.User().Update(user, true); result.Err != nil { - c.Err = result.Err + ruser := UpdateRoles(c, user, new_roles) + if c.Err != nil { return - } else { - c.LogAuditWithUserId(user.Id, "roles="+new_roles) - - ruser = result.Data.([2]*model.User)[0] } uchan := Srv.Store.Session().UpdateRoles(user.Id, new_roles) @@ -999,6 +986,45 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(ruser.ToJson())) } +func UpdateRoles(c *Context, user *model.User, roles string) *model.User { + // make sure there is at least 1 other active admin + + if !model.IsInRole(roles, model.ROLE_SYSTEM_ADMIN) { + if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) && !model.IsInRole(roles, model.ROLE_TEAM_ADMIN) { + if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { + c.Err = result.Err + return nil + } else { + activeAdmins := -1 + profileUsers := result.Data.(map[string]*model.User) + for _, profileUser := range profileUsers { + if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { + activeAdmins = activeAdmins + 1 + } + } + + if activeAdmins <= 0 { + c.Err = model.NewAppError("updateRoles", "There must be at least one active admin", "") + return nil + } + } + } + } + + user.Roles = roles + + var ruser *model.User + if result := <-Srv.Store.User().Update(user, true); result.Err != nil { + c.Err = result.Err + return nil + } else { + c.LogAuditWithUserId(user.Id, "roles="+roles) + ruser = result.Data.([2]*model.User)[0] + } + + return ruser +} + func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) @@ -1022,14 +1048,14 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_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 !active && model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) { if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { c.Err = result.Err return @@ -1037,7 +1063,7 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { activeAdmins := -1 profileUsers := result.Data.(map[string]*model.User) for _, profileUser := range profileUsers { - if profileUser.DeleteAt == 0 && strings.Contains(profileUser.Roles, model.ROLE_ADMIN) { + if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { activeAdmins = activeAdmins + 1 } } @@ -1113,8 +1139,10 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) - subjectPage := NewServerTemplatePage("reset_subject", c.GetSiteURL()) - bodyPage := NewServerTemplatePage("reset_body", c.GetSiteURL()) + subjectPage := NewServerTemplatePage("reset_subject") + subjectPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage := NewServerTemplatePage("reset_body") + bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["ResetUrl"] = link if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { @@ -1213,9 +1241,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("password_change_subject", siteURL) + subjectPage := NewServerTemplatePage("password_change_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("password_change_body", siteURL) + bodyPage := NewServerTemplatePage("password_change_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamURL"] = teamURL bodyPage.Props["Method"] = method @@ -1230,9 +1260,11 @@ func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, func fireAndForgetEmailChangeEmail(email, teamDisplayName, teamURL, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("email_change_subject", siteURL) + subjectPage := NewServerTemplatePage("email_change_subject") + subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("email_change_body", siteURL) + bodyPage := NewServerTemplatePage("email_change_body") + bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamURL"] = teamURL diff --git a/api/user_test.go b/api/user_test.go index b5435e3c0..986365bd0 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -68,7 +68,7 @@ func TestCreateUser(t *testing.T) { } } - if _, err := Client.DoPost("/users/create", "garbage"); err == nil { + if _, err := Client.DoApiPost("/users/create", "garbage"); err == nil { t.Fatal("should have been an error") } } @@ -190,11 +190,11 @@ func TestSessions(t *testing.T) { for _, session := range sessions { if session.DeviceId == deviceId { - otherSession = session.AltId + otherSession = session.Id } - if len(session.Id) != 0 { - t.Fatal("shouldn't return sessions") + if len(session.Token) != 0 { + t.Fatal("shouldn't return session tokens") } } @@ -212,11 +212,6 @@ func TestSessions(t *testing.T) { if len(sessions2) != 1 { t.Fatal("invalid number of sessions") } - - if _, err := Client.RevokeSession(otherSession); err != nil { - t.Fatal(err) - } - } func TestGetUser(t *testing.T) { @@ -355,7 +350,7 @@ func TestUserCreateImage(t *testing.T) { Client.LoginByEmail(team.Name, user.Email, "pwd") - Client.DoGet("/users/"+user.Id+"/image", "", "") + Client.DoApiGet("/users/"+user.Id+"/image", "", "") if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { var auth aws.Auth @@ -453,7 +448,7 @@ func TestUserUploadProfileImage(t *testing.T) { t.Fatal(upErr) } - Client.DoGet("/users/"+user.Id+"/image", "", "") + Client.DoApiGet("/users/"+user.Id+"/image", "", "") if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { var auth aws.Auth @@ -509,7 +504,7 @@ func TestUserUpdate(t *testing.T) { user.TeamId = "12345678901234567890123456" user.LastActivityAt = time2 user.LastPingAt = time2 - user.Roles = model.ROLE_ADMIN + user.Roles = model.ROLE_TEAM_ADMIN user.LastPasswordUpdate = 123 if result, err := Client.UpdateUser(user); err != nil { @@ -684,6 +679,7 @@ func TestUserUpdateRoles(t *testing.T) { data["user_id"] = user2.Id if result, err := Client.UpdateUserRoles(data); err != nil { + t.Log(data["new_roles"]) t.Fatal(err) } else { if result.Data.(*model.User).Roles != "admin" { |