diff options
Diffstat (limited to 'model')
-rw-r--r-- | model/access.go | 56 | ||||
-rw-r--r-- | model/access_test.go | 41 | ||||
-rw-r--r-- | model/authorize.go | 103 | ||||
-rw-r--r-- | model/authorize_test.go | 66 | ||||
-rw-r--r-- | model/channel.go | 11 | ||||
-rw-r--r-- | model/channel_count.go | 2 | ||||
-rw-r--r-- | model/client.go | 295 | ||||
-rw-r--r-- | model/config.go | 158 | ||||
-rw-r--r-- | model/oauth.go | 151 | ||||
-rw-r--r-- | model/oauth_test.go | 95 | ||||
-rw-r--r-- | model/post.go | 3 | ||||
-rw-r--r-- | model/session.go | 9 | ||||
-rw-r--r-- | model/system.go | 34 | ||||
-rw-r--r-- | model/system_test.go | 19 | ||||
-rw-r--r-- | model/team.go | 26 | ||||
-rw-r--r-- | model/user.go | 70 | ||||
-rw-r--r-- | model/user_test.go | 23 | ||||
-rw-r--r-- | model/utils.go | 21 | ||||
-rw-r--r-- | model/version.go | 90 | ||||
-rw-r--r-- | model/version_test.go | 74 | ||||
-rw-r--r-- | model/webhook.go | 101 | ||||
-rw-r--r-- | model/webhook_test.go | 82 |
22 files changed, 1423 insertions, 107 deletions
diff --git a/model/access.go b/model/access.go index f9e36ce07..44a0463ac 100644 --- a/model/access.go +++ b/model/access.go @@ -9,17 +9,69 @@ import ( ) const ( - ACCESS_TOKEN_GRANT_TYPE = "authorization_code" - ACCESS_TOKEN_TYPE = "bearer" + ACCESS_TOKEN_GRANT_TYPE = "authorization_code" + ACCESS_TOKEN_TYPE = "bearer" + REFRESH_TOKEN_GRANT_TYPE = "refresh_token" ) +type AccessData struct { + AuthCode string `json:"auth_code"` + Token string `json"token"` + RefreshToken string `json:"refresh_token"` + RedirectUri string `json:"redirect_uri"` +} + type AccessResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int32 `json:"expires_in"` + Scope string `json:"scope"` RefreshToken string `json:"refresh_token"` } +// IsValid validates the AccessData and returns an error if it isn't configured +// correctly. +func (ad *AccessData) IsValid() *AppError { + + if len(ad.AuthCode) == 0 || len(ad.AuthCode) > 128 { + return NewAppError("AccessData.IsValid", "Invalid auth code", "") + } + + if len(ad.Token) != 26 { + return NewAppError("AccessData.IsValid", "Invalid access token", "") + } + + if len(ad.RefreshToken) > 26 { + return NewAppError("AccessData.IsValid", "Invalid refresh token", "") + } + + if len(ad.RedirectUri) > 256 { + return NewAppError("AccessData.IsValid", "Invalid redirect uri", "") + } + + return nil +} + +func (ad *AccessData) ToJson() string { + b, err := json.Marshal(ad) + if err != nil { + return "" + } else { + return string(b) + } +} + +func AccessDataFromJson(data io.Reader) *AccessData { + decoder := json.NewDecoder(data) + var ad AccessData + err := decoder.Decode(&ad) + if err == nil { + return &ad + } else { + return nil + } +} + func (ar *AccessResponse) ToJson() string { b, err := json.Marshal(ar) if err != nil { diff --git a/model/access_test.go b/model/access_test.go new file mode 100644 index 000000000..e385c0586 --- /dev/null +++ b/model/access_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestAccessJson(t *testing.T) { + a1 := AccessData{} + a1.AuthCode = NewId() + a1.Token = NewId() + a1.RefreshToken = NewId() + + json := a1.ToJson() + ra1 := AccessDataFromJson(strings.NewReader(json)) + + if a1.Token != ra1.Token { + t.Fatal("tokens didn't match") + } +} + +func TestAccessIsValid(t *testing.T) { + ad := AccessData{} + + if err := ad.IsValid(); err == nil { + t.Fatal("should have failed") + } + + ad.AuthCode = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal("should have failed") + } + + ad.Token = NewId() + if err := ad.IsValid(); err != nil { + t.Fatal(err) + } +} diff --git a/model/authorize.go b/model/authorize.go new file mode 100644 index 000000000..6eaac97f1 --- /dev/null +++ b/model/authorize.go @@ -0,0 +1,103 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes + AUTHCODE_RESPONSE_TYPE = "code" +) + +type AuthData struct { + ClientId string `json:"client_id"` + UserId string `json:"user_id"` + Code string `json:"code"` + ExpiresIn int32 `json:"expires_in"` + CreateAt int64 `json:"create_at"` + RedirectUri string `json:"redirect_uri"` + State string `json:"state"` + Scope string `json:"scope"` +} + +// IsValid validates the AuthData and returns an error if it isn't configured +// correctly. +func (ad *AuthData) IsValid() *AppError { + + if len(ad.ClientId) != 26 { + return NewAppError("AuthData.IsValid", "Invalid client id", "") + } + + if len(ad.UserId) != 26 { + return NewAppError("AuthData.IsValid", "Invalid user id", "") + } + + if len(ad.Code) == 0 || len(ad.Code) > 128 { + return NewAppError("AuthData.IsValid", "Invalid authorization code", "client_id="+ad.ClientId) + } + + if ad.ExpiresIn == 0 { + return NewAppError("AuthData.IsValid", "Expires in must be set", "") + } + + if ad.CreateAt <= 0 { + return NewAppError("AuthData.IsValid", "Create at must be a valid time", "client_id="+ad.ClientId) + } + + if len(ad.RedirectUri) > 256 { + return NewAppError("AuthData.IsValid", "Invalid redirect uri", "client_id="+ad.ClientId) + } + + if len(ad.State) > 128 { + return NewAppError("AuthData.IsValid", "Invalid state", "client_id="+ad.ClientId) + } + + if len(ad.Scope) > 128 { + return NewAppError("AuthData.IsValid", "Invalid scope", "client_id="+ad.ClientId) + } + + return nil +} + +func (ad *AuthData) PreSave() { + if ad.ExpiresIn == 0 { + ad.ExpiresIn = AUTHCODE_EXPIRE_TIME + } + + if ad.CreateAt == 0 { + ad.CreateAt = GetMillis() + } +} + +func (ad *AuthData) ToJson() string { + b, err := json.Marshal(ad) + if err != nil { + return "" + } else { + return string(b) + } +} + +func AuthDataFromJson(data io.Reader) *AuthData { + decoder := json.NewDecoder(data) + var ad AuthData + err := decoder.Decode(&ad) + if err == nil { + return &ad + } else { + return nil + } +} + +func (ad *AuthData) IsExpired() bool { + + if GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000) { + return true + } + + return false +} diff --git a/model/authorize_test.go b/model/authorize_test.go new file mode 100644 index 000000000..14524ad84 --- /dev/null +++ b/model/authorize_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestAuthJson(t *testing.T) { + a1 := AuthData{} + a1.ClientId = NewId() + a1.UserId = NewId() + a1.Code = NewId() + + json := a1.ToJson() + ra1 := AuthDataFromJson(strings.NewReader(json)) + + if a1.Code != ra1.Code { + t.Fatal("codes didn't match") + } +} + +func TestAuthPreSave(t *testing.T) { + a1 := AuthData{} + a1.ClientId = NewId() + a1.UserId = NewId() + a1.Code = NewId() + a1.PreSave() + a1.IsExpired() +} + +func TestAuthIsValid(t *testing.T) { + + ad := AuthData{} + + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.ClientId = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.UserId = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.Code = NewId() + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.ExpiresIn = 1 + if err := ad.IsValid(); err == nil { + t.Fatal() + } + + ad.CreateAt = 1 + if err := ad.IsValid(); err != nil { + t.Fatal() + } +} diff --git a/model/channel.go b/model/channel.go index b46f79f75..a7f007960 100644 --- a/model/channel.go +++ b/model/channel.go @@ -117,3 +117,14 @@ func (o *Channel) PreUpdate() { func (o *Channel) ExtraUpdated() { o.ExtraUpdateAt = GetMillis() } + +func (o *Channel) PreExport() { +} + +func GetDMNameFromIds(userId1, userId2 string) string { + if userId1 > userId2 { + return userId2 + "__" + userId1 + } else { + return userId1 + "__" + userId2 + } +} diff --git a/model/channel_count.go b/model/channel_count.go index d5daba14e..19d6ac150 100644 --- a/model/channel_count.go +++ b/model/channel_count.go @@ -20,7 +20,7 @@ type ChannelCounts struct { func (o *ChannelCounts) Etag() string { ids := []string{} - for id, _ := range o.Counts { + for id := range o.Counts { ids = append(ids, id) } sort.Strings(ids) diff --git a/model/client.go b/model/client.go index c355b90f5..26e00864d 100644 --- a/model/client.go +++ b/model/client.go @@ -21,9 +21,12 @@ const ( HEADER_ETAG_SERVER = "ETag" HEADER_ETAG_CLIENT = "If-None-Match" HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" HEADER_AUTH = "Authorization" + API_URL_SUFFIX = "/api/v1" ) type Result struct { @@ -33,22 +36,37 @@ type Result struct { } type Client struct { - Url string // The location of the server like "http://localhost/api/v1" + Url string // The location of the server like "http://localhost:8065" + ApiUrl string // The api location of the server like "http://localhost:8065/api/v1" HttpClient *http.Client // The http client AuthToken string + AuthType string } // NewClient constructs a new client with convienence methods for talking to // the server. func NewClient(url string) *Client { - return &Client{url, &http.Client{}, ""} + return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", ""} } -func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) { +func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) { rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data)) + rq.Header.Set("Content-Type", contentType) + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + } else if rp.StatusCode >= 300 { + return nil, AppErrorFromJson(rp.Body) + } else { + return rp, nil + } +} + +func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) { + rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data)) if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) } if rp, err := c.HttpClient.Do(rq); err != nil { @@ -60,15 +78,15 @@ func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) { } } -func (c *Client) DoGet(url string, data string, etag string) (*http.Response, *AppError) { - rq, _ := http.NewRequest("GET", c.Url+url, strings.NewReader(data)) +func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) { + rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data)) if len(etag) > 0 { rq.Header.Set(HEADER_ETAG_CLIENT, etag) } if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) } if rp, err := c.HttpClient.Do(rq); err != nil { @@ -106,7 +124,7 @@ func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppErro m := make(map[string]string) m["email"] = email m["display_name"] = displayName - if r, err := c.DoPost("/teams/signup", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/signup", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -115,7 +133,7 @@ func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppErro } func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppError) { - if r, err := c.DoPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil { + if r, err := c.DoApiPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -124,7 +142,7 @@ func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppErro } func (c *Client) CreateTeam(team *Team) (*Result, *AppError) { - if r, err := c.DoPost("/teams/create", team.ToJson()); err != nil { + if r, err := c.DoApiPost("/teams/create", team.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -132,11 +150,21 @@ func (c *Client) CreateTeam(team *Team) (*Result, *AppError) { } } +func (c *Client) GetAllTeams() (*Result, *AppError) { + if r, err := c.DoApiGet("/teams/all", "", ""); err != nil { + return nil, err + } else { + + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil + } +} + func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppError) { m := make(map[string]string) m["name"] = name m["all"] = fmt.Sprintf("%v", allServers) - if r, err := c.DoPost("/teams/find_team_by_name", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/find_team_by_name", MapToJson(m)); err != nil { return nil, err } else { val := false @@ -152,7 +180,7 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro func (c *Client) FindTeams(email string) (*Result, *AppError) { m := make(map[string]string) m["email"] = email - if r, err := c.DoPost("/teams/find_teams", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil { return nil, err } else { @@ -164,7 +192,7 @@ func (c *Client) FindTeams(email string) (*Result, *AppError) { func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { m := make(map[string]string) m["email"] = email - if r, err := c.DoPost("/teams/email_teams", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -173,7 +201,7 @@ func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { } func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { - if r, err := c.DoPost("/teams/invite_members", invites.ToJson()); err != nil { + if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -182,16 +210,7 @@ func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { } func (c *Client) UpdateTeamDisplayName(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/teams/update_name", MapToJson(data)); err != nil { - return nil, err - } else { - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/teams/update_valet_feature", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/teams/update_name", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -200,7 +219,7 @@ func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) } func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) { - if r, err := c.DoPost("/users/create", user.ToJson()); err != nil { + if r, err := c.DoApiPost("/users/create", user.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -209,7 +228,7 @@ func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) { } func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Result, *AppError) { - if r, err := c.DoPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil { + if r, err := c.DoApiPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -218,7 +237,7 @@ func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Re } func (c *Client) GetUser(id string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/"+id, "", etag); err != nil { + if r, err := c.DoApiGet("/users/"+id, "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -227,7 +246,7 @@ func (c *Client) GetUser(id string, etag string) (*Result, *AppError) { } func (c *Client) GetMe(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/me", "", etag); err != nil { + if r, err := c.DoApiGet("/users/me", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -236,7 +255,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) { } func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/profiles", "", etag); err != nil { + if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -269,13 +288,14 @@ func (c *Client) LoginByEmailWithDevice(name string, email string, password stri } func (c *Client) login(m map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/login", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil { return nil, err } else { c.AuthToken = r.Header.Get(HEADER_TOKEN) - sessionId := getCookie(SESSION_TOKEN, r) + c.AuthType = HEADER_BEARER + sessionToken := getCookie(SESSION_TOKEN, r) - if c.AuthToken != sessionId.Value { + if c.AuthToken != sessionToken.Value { NewAppError("/users/login", "Authentication tokens didn't match", "") } @@ -285,21 +305,32 @@ func (c *Client) login(m map[string]string) (*Result, *AppError) { } func (c *Client) Logout() (*Result, *AppError) { - if r, err := c.DoPost("/users/logout", ""); err != nil { + if r, err := c.DoApiPost("/users/logout", ""); err != nil { return nil, err } else { c.AuthToken = "" + c.AuthType = HEADER_BEARER return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil } } +func (c *Client) SetOAuthToken(token string) { + c.AuthToken = token + c.AuthType = HEADER_TOKEN +} + +func (c *Client) ClearOAuthToken() { + c.AuthToken = "" + c.AuthType = HEADER_BEARER +} + func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) { m := make(map[string]string) m["id"] = sessionAltId - if r, err := c.DoPost("/users/revoke_session", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/users/revoke_session", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -308,7 +339,7 @@ func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) { } func (c *Client) GetSessions(id string) (*Result, *AppError) { - if r, err := c.DoGet("/users/"+id+"/sessions", "", ""); err != nil { + if r, err := c.DoApiGet("/users/"+id+"/sessions", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -321,7 +352,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul m["command"] = command m["channelId"] = channelId m["suggest"] = strconv.FormatBool(suggest) - if r, err := c.DoPost("/command", MapToJson(m)); err != nil { + if r, err := c.DoApiPost("/command", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -330,7 +361,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul } func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/users/"+id+"/audits", "", etag); err != nil { + if r, err := c.DoApiGet("/users/"+id+"/audits", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -338,8 +369,53 @@ func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) { } } +func (c *Client) GetLogs() (*Result, *AppError) { + if r, err := c.DoApiGet("/admin/logs", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil + } +} + +func (c *Client) GetClientProperties() (*Result, *AppError) { + if r, err := c.DoApiGet("/admin/client_props", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) GetConfig() (*Result, *AppError) { + if r, err := c.DoApiGet("/admin/config", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil + } +} + +func (c *Client) SaveConfig(config *Config) (*Result, *AppError) { + if r, err := c.DoApiPost("/admin/save_config", config.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil + } +} + +func (c *Client) TestEmail(config *Config) (*Result, *AppError) { + if r, err := c.DoApiPost("/admin/test_email", config.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoPost("/channels/create", channel.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -348,7 +424,7 @@ func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { } func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/create_direct", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/create_direct", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -357,7 +433,7 @@ func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError } func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoPost("/channels/update", channel.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/update", channel.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -366,7 +442,7 @@ func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { } func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/update_desc", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/update_desc", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -375,7 +451,7 @@ func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) } func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/update_notify_level", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/update_notify_level", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -384,7 +460,7 @@ func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) } func (c *Client) GetChannels(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -393,7 +469,7 @@ func (c *Client) GetChannels(etag string) (*Result, *AppError) { } func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/"+id+"/", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/"+id+"/", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -402,7 +478,7 @@ func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { } func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/more", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/more", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -411,7 +487,7 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { } func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/counts", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/counts", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -420,7 +496,7 @@ func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { } func (c *Client) JoinChannel(id string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+id+"/join", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/join", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -429,7 +505,7 @@ func (c *Client) JoinChannel(id string) (*Result, *AppError) { } func (c *Client) LeaveChannel(id string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+id+"/leave", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/leave", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -438,7 +514,7 @@ func (c *Client) LeaveChannel(id string) (*Result, *AppError) { } func (c *Client) DeleteChannel(id string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+id+"/delete", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/delete", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -449,7 +525,7 @@ func (c *Client) DeleteChannel(id string) (*Result, *AppError) { func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = user_id - if r, err := c.DoPost("/channels/"+id+"/add", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/add", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -460,7 +536,7 @@ func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = user_id - if r, err := c.DoPost("/channels/"+id+"/remove", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/channels/"+id+"/remove", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -469,7 +545,7 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { } func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil { + if r, err := c.DoApiPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -478,7 +554,7 @@ func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) { } func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError) { - if r, err := c.DoGet("/channels/"+id+"/extra_info", "", etag); err != nil { + if r, err := c.DoApiGet("/channels/"+id+"/extra_info", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -487,16 +563,7 @@ func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError } func (c *Client) CreatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil { - return nil, err - } else { - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil - } -} - -func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -505,7 +572,7 @@ func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) { } func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil { + if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -514,7 +581,7 @@ func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { } func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -523,7 +590,7 @@ func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) } func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError) { - if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -532,7 +599,7 @@ func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError } func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) { - if r, err := c.DoGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -541,7 +608,7 @@ func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, } func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) { - if r, err := c.DoPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil { + if r, err := c.DoApiPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -550,7 +617,7 @@ func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError } func (c *Client) SearchPosts(terms string) (*Result, *AppError) { - if r, err := c.DoGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil { + if r, err := c.DoApiGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -559,7 +626,7 @@ func (c *Client) SearchPosts(terms string) (*Result, *AppError) { } func (c *Client) UploadFile(url string, data []byte, contentType string) (*Result, *AppError) { - rq, _ := http.NewRequest("POST", c.Url+url, bytes.NewReader(data)) + rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) if len(c.AuthToken) > 0 { @@ -581,7 +648,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { if isFullUrl { rq, _ = http.NewRequest("GET", url, nil) } else { - rq, _ = http.NewRequest("GET", c.Url+"/files/get"+url, nil) + rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get"+url, nil) } if len(c.AuthToken) > 0 { @@ -600,7 +667,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { func (c *Client) GetFileInfo(url string) (*Result, *AppError) { var rq *http.Request - rq, _ = http.NewRequest("GET", c.Url+"/files/get_info"+url, nil) + rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get_info"+url, nil) if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) @@ -617,7 +684,7 @@ func (c *Client) GetFileInfo(url string) (*Result, *AppError) { } func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/files/get_public_link", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/files/get_public_link", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -626,7 +693,7 @@ func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) { } func (c *Client) UpdateUser(user *User) (*Result, *AppError) { - if r, err := c.DoPost("/users/update", user.ToJson()); err != nil { + if r, err := c.DoApiPost("/users/update", user.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -635,7 +702,7 @@ func (c *Client) UpdateUser(user *User) (*Result, *AppError) { } func (c *Client) UpdateUserRoles(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/update_roles", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/update_roles", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -647,7 +714,7 @@ func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) { data := make(map[string]string) data["user_id"] = userId data["active"] = strconv.FormatBool(active) - if r, err := c.DoPost("/users/update_active", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/update_active", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -656,7 +723,7 @@ func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) { } func (c *Client) UpdateUserNotify(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/update_notify", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/update_notify", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -670,7 +737,7 @@ func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) data["new_password"] = newPassword data["user_id"] = userId - if r, err := c.DoPost("/users/newpassword", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/newpassword", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -679,7 +746,7 @@ func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) } func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/send_password_reset", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/send_password_reset", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -688,7 +755,7 @@ func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) } func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) { - if r, err := c.DoPost("/users/reset_password", MapToJson(data)); err != nil { + if r, err := c.DoApiPost("/users/reset_password", MapToJson(data)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -697,7 +764,7 @@ func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) { } func (c *Client) GetStatuses() (*Result, *AppError) { - if r, err := c.DoGet("/users/status", "", ""); err != nil { + if r, err := c.DoApiGet("/users/status", "", ""); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -706,7 +773,7 @@ func (c *Client) GetStatuses() (*Result, *AppError) { } func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { - if r, err := c.DoGet("/teams/me", "", etag); err != nil { + if r, err := c.DoApiGet("/teams/me", "", etag); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -714,6 +781,70 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { } } +func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) { + if r, err := c.DoApiPost("/oauth/register", app.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil + } +} + +func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*Result, *AppError) { + if r, err := c.DoApiGet("/oauth/allow?response_type="+rspType+"&client_id="+clientId+"&redirect_uri="+url.QueryEscape(redirect)+"&scope="+scope+"&state="+url.QueryEscape(state), "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { + if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), AccessResponseFromJson(r.Body)}, nil + } +} + +func (c *Client) CreateIncomingWebhook(hook *IncomingWebhook) (*Result, *AppError) { + if r, err := c.DoApiPost("/hooks/incoming/create", hook.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), IncomingWebhookFromJson(r.Body)}, nil + } +} + +func (c *Client) PostToWebhook(id, payload string) (*Result, *AppError) { + if r, err := c.DoPost("/hooks/"+id, payload, "application/x-www-form-urlencoded"); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) DeleteIncomingWebhook(data map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/hooks/incoming/delete", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) ListIncomingWebhooks() (*Result, *AppError) { + if r, err := c.DoApiGet("/hooks/incoming/list", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), IncomingWebhookListFromJson(r.Body)}, nil + } +} + func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken + c.AuthType = HEADER_BEARER } diff --git a/model/config.go b/model/config.go new file mode 100644 index 000000000..31c2b16dc --- /dev/null +++ b/model/config.go @@ -0,0 +1,158 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + CONN_SECURITY_NONE = "" + CONN_SECURITY_TLS = "TLS" + CONN_SECURITY_STARTTLS = "STARTTLS" + + IMAGE_DRIVER_LOCAL = "local" + IMAGE_DRIVER_S3 = "amazons3" + + SERVICE_GITLAB = "gitlab" +) + +type ServiceSettings struct { + ListenAddress string + MaximumLoginAttempts int + SegmentDeveloperKey string + GoogleDeveloperKey string + EnableOAuthServiceProvider bool + EnableIncomingWebhooks bool + EnableTesting bool +} + +type SSOSettings struct { + Enable bool + Secret string + Id string + Scope string + AuthEndpoint string + TokenEndpoint string + UserApiEndpoint string +} + +type SqlSettings struct { + DriverName string + DataSource string + DataSourceReplicas []string + MaxIdleConns int + MaxOpenConns int + Trace bool + AtRestEncryptKey string +} + +type LogSettings struct { + EnableConsole bool + ConsoleLevel string + EnableFile bool + FileLevel string + FileFormat string + FileLocation string +} + +type FileSettings struct { + DriverName string + Directory string + EnablePublicLink bool + PublicLinkSalt string + ThumbnailWidth uint + ThumbnailHeight uint + PreviewWidth uint + PreviewHeight uint + ProfileWidth uint + ProfileHeight uint + InitialFont string + AmazonS3AccessKeyId string + AmazonS3SecretAccessKey string + AmazonS3Bucket string + AmazonS3Region string +} + +type EmailSettings struct { + EnableSignUpWithEmail bool + SendEmailNotifications bool + RequireEmailVerification bool + FeedbackName string + FeedbackEmail string + SMTPUsername string + SMTPPassword string + SMTPServer string + SMTPPort string + ConnectionSecurity string + InviteSalt string + PasswordResetSalt string + + // For Future Use + ApplePushServer string + ApplePushCertPublic string + ApplePushCertPrivate string +} + +type RateLimitSettings struct { + EnableRateLimiter bool + PerSec int + MemoryStoreSize int + VaryByRemoteAddr bool + VaryByHeader string +} + +type PrivacySettings struct { + ShowEmailAddress bool + ShowFullName bool +} + +type TeamSettings struct { + SiteName string + MaxUsersPerTeam int + EnableTeamCreation bool + EnableUserCreation bool + RestrictCreationToDomains string +} + +type Config struct { + ServiceSettings ServiceSettings + TeamSettings TeamSettings + SqlSettings SqlSettings + LogSettings LogSettings + FileSettings FileSettings + EmailSettings EmailSettings + RateLimitSettings RateLimitSettings + PrivacySettings PrivacySettings + GitLabSettings SSOSettings +} + +func (o *Config) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func (o *Config) GetSSOService(service string) *SSOSettings { + if service == SERVICE_GITLAB { + return &o.GitLabSettings + } + + return nil +} + +func ConfigFromJson(data io.Reader) *Config { + decoder := json.NewDecoder(data) + var o Config + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/oauth.go b/model/oauth.go new file mode 100644 index 000000000..3b31e677d --- /dev/null +++ b/model/oauth.go @@ -0,0 +1,151 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" +) + +type OAuthApp struct { + Id string `json:"id"` + CreatorId string `json:"creator_id"` + CreateAt int64 `json:"update_at"` + UpdateAt int64 `json:"update_at"` + ClientSecret string `json:"client_secret"` + Name string `json:"name"` + Description string `json:"description"` + CallbackUrls StringArray `json:"callback_urls"` + Homepage string `json:"homepage"` +} + +// IsValid validates the app and returns an error if it isn't configured +// correctly. +func (a *OAuthApp) IsValid() *AppError { + + if len(a.Id) != 26 { + return NewAppError("OAuthApp.IsValid", "Invalid app id", "") + } + + if a.CreateAt == 0 { + return NewAppError("OAuthApp.IsValid", "Create at must be a valid time", "app_id="+a.Id) + } + + if a.UpdateAt == 0 { + return NewAppError("OAuthApp.IsValid", "Update at must be a valid time", "app_id="+a.Id) + } + + if len(a.CreatorId) != 26 { + return NewAppError("OAuthApp.IsValid", "Invalid creator id", "app_id="+a.Id) + } + + if len(a.ClientSecret) == 0 || len(a.ClientSecret) > 128 { + return NewAppError("OAuthApp.IsValid", "Invalid client secret", "app_id="+a.Id) + } + + if len(a.Name) == 0 || len(a.Name) > 64 { + return NewAppError("OAuthApp.IsValid", "Invalid name", "app_id="+a.Id) + } + + if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 { + return NewAppError("OAuthApp.IsValid", "Invalid callback urls", "app_id="+a.Id) + } + + if len(a.Homepage) == 0 || len(a.Homepage) > 256 { + return NewAppError("OAuthApp.IsValid", "Invalid homepage", "app_id="+a.Id) + } + + if len(a.Description) > 512 { + return NewAppError("OAuthApp.IsValid", "Invalid description", "app_id="+a.Id) + } + + return nil +} + +// PreSave will set the Id and ClientSecret if missing. It will also fill +// in the CreateAt, UpdateAt times. It should be run before saving the app to the db. +func (a *OAuthApp) PreSave() { + if a.Id == "" { + a.Id = NewId() + } + + if a.ClientSecret == "" { + a.ClientSecret = NewId() + } + + a.CreateAt = GetMillis() + a.UpdateAt = a.CreateAt + + if len(a.ClientSecret) > 0 { + a.ClientSecret = HashPassword(a.ClientSecret) + } +} + +// PreUpdate should be run before updating the app in the db. +func (a *OAuthApp) PreUpdate() { + a.UpdateAt = GetMillis() +} + +// ToJson convert a User to a json string +func (a *OAuthApp) ToJson() string { + b, err := json.Marshal(a) + if err != nil { + return "" + } else { + return string(b) + } +} + +// Generate a valid strong etag so the browser can cache the results +func (a *OAuthApp) Etag() string { + return Etag(a.Id, a.UpdateAt) +} + +// Remove any private data from the app object +func (a *OAuthApp) Sanitize() { + a.ClientSecret = "" +} + +func (a *OAuthApp) IsValidRedirectURL(url string) bool { + for _, u := range a.CallbackUrls { + if u == url { + return true + } + } + + return false +} + +// OAuthAppFromJson will decode the input and return a User +func OAuthAppFromJson(data io.Reader) *OAuthApp { + decoder := json.NewDecoder(data) + var app OAuthApp + err := decoder.Decode(&app) + if err == nil { + return &app + } else { + return nil + } +} + +func OAuthAppMapToJson(a map[string]*OAuthApp) string { + b, err := json.Marshal(a) + if err != nil { + return "" + } else { + return string(b) + } +} + +func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp { + decoder := json.NewDecoder(data) + var apps map[string]*OAuthApp + err := decoder.Decode(&apps) + if err == nil { + return apps + } else { + return nil + } +} diff --git a/model/oauth_test.go b/model/oauth_test.go new file mode 100644 index 000000000..2530ead98 --- /dev/null +++ b/model/oauth_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestOAuthAppJson(t *testing.T) { + a1 := OAuthApp{} + a1.Id = NewId() + a1.Name = "TestOAuthApp" + NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + a1.ClientSecret = NewId() + + json := a1.ToJson() + ra1 := OAuthAppFromJson(strings.NewReader(json)) + + if a1.Id != ra1.Id { + t.Fatal("ids did not match") + } +} + +func TestOAuthAppPreSave(t *testing.T) { + a1 := OAuthApp{} + a1.Id = NewId() + a1.Name = "TestOAuthApp" + NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + a1.ClientSecret = NewId() + a1.PreSave() + a1.Etag() + a1.Sanitize() +} + +func TestOAuthAppPreUpdate(t *testing.T) { + a1 := OAuthApp{} + a1.Id = NewId() + a1.Name = "TestOAuthApp" + NewId() + a1.CallbackUrls = []string{"https://nowhere.com"} + a1.Homepage = "https://nowhere.com" + a1.ClientSecret = NewId() + a1.PreUpdate() +} + +func TestOAuthAppIsValid(t *testing.T) { + app := OAuthApp{} + + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.Id = NewId() + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.CreateAt = 1 + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.UpdateAt = 1 + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.CreatorId = NewId() + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.ClientSecret = NewId() + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.Name = "TestOAuthApp" + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.CallbackUrls = []string{"https://nowhere.com"} + if err := app.IsValid(); err == nil { + t.Fatal() + } + + app.Homepage = "https://nowhere.com" + if err := app.IsValid(); err != nil { + t.Fatal() + } +} diff --git a/model/post.go b/model/post.go index 0c035d4e7..e78469940 100644 --- a/model/post.go +++ b/model/post.go @@ -147,3 +147,6 @@ func (o *Post) AddProp(key string, value string) { o.Props[key] = value } + +func (o *Post) PreExport() { +} diff --git a/model/session.go b/model/session.go index c812f83e2..3c7c75eb4 100644 --- a/model/session.go +++ b/model/session.go @@ -14,6 +14,8 @@ const ( SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS SESSION_TIME_MOBILE_IN_DAYS = 30 SESSION_TIME_MOBILE_IN_SECS = 60 * 60 * 24 * SESSION_TIME_MOBILE_IN_DAYS + SESSION_TIME_OAUTH_IN_DAYS = 365 + SESSION_TIME_OAUTH_IN_SECS = 60 * 60 * 24 * SESSION_TIME_OAUTH_IN_DAYS SESSION_CACHE_IN_SECS = 60 * 10 SESSION_CACHE_SIZE = 10000 SESSION_PROP_PLATFORM = "platform" @@ -23,7 +25,7 @@ const ( type Session struct { Id string `json:"id"` - AltId string `json:"alt_id"` + Token string `json:"token"` CreateAt int64 `json:"create_at"` ExpiresAt int64 `json:"expires_at"` LastActivityAt int64 `json:"last_activity_at"` @@ -31,6 +33,7 @@ type Session struct { TeamId string `json:"team_id"` DeviceId string `json:"device_id"` Roles string `json:"roles"` + IsOAuth bool `json:"is_oauth"` Props StringMap `json:"props"` } @@ -59,7 +62,7 @@ func (me *Session) PreSave() { me.Id = NewId() } - me.AltId = NewId() + me.Token = NewId() me.CreateAt = GetMillis() me.LastActivityAt = me.CreateAt @@ -70,7 +73,7 @@ func (me *Session) PreSave() { } func (me *Session) Sanitize() { - me.Id = "" + me.Token = "" } func (me *Session) IsExpired() bool { diff --git a/model/system.go b/model/system.go new file mode 100644 index 000000000..c79391cca --- /dev/null +++ b/model/system.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type System struct { + Name string `json:"name"` + Value string `json:"value"` +} + +func (o *System) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func SystemFromJson(data io.Reader) *System { + decoder := json.NewDecoder(data) + var o System + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/system_test.go b/model/system_test.go new file mode 100644 index 000000000..14ba0db2e --- /dev/null +++ b/model/system_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestSystemJson(t *testing.T) { + system := System{Name: "test", Value: NewId()} + json := system.ToJson() + result := SystemFromJson(strings.NewReader(json)) + + if result.Name != "test" { + t.Fatal("Ids do not match") + } +} diff --git a/model/team.go b/model/team.go index 95e2757c8..f80fa3b11 100644 --- a/model/team.go +++ b/model/team.go @@ -27,7 +27,6 @@ type Team struct { Type string `json:"type"` CompanyName string `json:"company_name"` AllowedDomains string `json:"allowed_domains"` - AllowValet bool `json:"allow_valet"` } type Invites struct { @@ -74,6 +73,26 @@ func TeamFromJson(data io.Reader) *Team { } } +func TeamMapToJson(u map[string]*Team) string { + b, err := json.Marshal(u) + if err != nil { + return "" + } else { + return string(b) + } +} + +func TeamMapFromJson(data io.Reader) map[string]*Team { + decoder := json.NewDecoder(data) + var teams map[string]*Team + err := decoder.Decode(&teams) + if err == nil { + return teams + } else { + return nil + } +} + func (o *Team) Etag() string { return Etag(o.Id, o.UpdateAt) } @@ -158,7 +177,7 @@ func IsReservedTeamName(s string) bool { func IsValidTeamName(s string) bool { - if !IsValidAlphaNum(s) { + if !IsValidAlphaNum(s, false) { return false } @@ -197,3 +216,6 @@ func CleanTeamName(s string) string { return s } + +func (o *Team) PreExport() { +} diff --git a/model/user.go b/model/user.go index d82f96db3..5cb774478 100644 --- a/model/user.go +++ b/model/user.go @@ -13,9 +13,8 @@ import ( ) const ( - ROLE_ADMIN = "admin" + ROLE_TEAM_ADMIN = "admin" ROLE_SYSTEM_ADMIN = "system_admin" - ROLE_SYSTEM_SUPPORT = "system_support" USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute USER_OFFLINE = "offline" @@ -24,7 +23,6 @@ const ( USER_NOTIFY_ALL = "all" USER_NOTIFY_MENTION = "mention" USER_NOTIFY_NONE = "none" - BOT_USERNAME = "valet" ) type User struct { @@ -48,6 +46,7 @@ type User struct { AllowMarketing bool `json:"allow_marketing"` Props StringMap `json:"props"` NotifyProps StringMap `json:"notify_props"` + ThemeProps StringMap `json:"theme_props"` LastPasswordUpdate int64 `json:"last_password_update"` LastPictureUpdate int64 `json:"last_picture_update"` FailedAttempts int `json:"failed_attempts"` @@ -109,6 +108,10 @@ func (u *User) IsValid() *AppError { return NewAppError("User.IsValid", "Invalid user, password and auth data cannot both be set", "user_id="+u.Id) } + if len(u.ThemeProps) > 2000 { + return NewAppError("User.IsValid", "Invalid theme", "user_id="+u.Id) + } + return nil } @@ -272,6 +275,62 @@ func (u *User) GetDisplayName() string { } } +func IsValidRoles(userRoles string) bool { + + roles := strings.Split(userRoles, " ") + + for _, r := range roles { + if !isValidRole(r) { + return false + } + } + + return true +} + +func isValidRole(role string) bool { + if role == "" { + return true + } + + if role == ROLE_TEAM_ADMIN { + return true + } + + if role == ROLE_SYSTEM_ADMIN { + return true + } + + return false +} + +func (u *User) IsInRole(inRole string) bool { + return IsInRole(u.Roles, inRole) +} + +func IsInRole(userRoles string, inRole string) bool { + roles := strings.Split(userRoles, " ") + + for _, r := range roles { + if r == inRole { + return true + } + + } + + return false +} + +func (u *User) PreExport() { + u.Password = "" + u.AuthData = "" + u.LastActivityAt = 0 + u.LastPingAt = 0 + u.LastPasswordUpdate = 0 + u.LastPictureUpdate = 0 + u.FailedAttempts = 0 +} + // UserFromJson will decode the input and return a User func UserFromJson(data io.Reader) *User { decoder := json.NewDecoder(data) @@ -325,11 +384,6 @@ func ComparePassword(hash string, password string) bool { return err == nil } -func IsUsernameValid(username string) bool { - - return true -} - var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`) var restrictedUsernames = []string{ diff --git a/model/user_test.go b/model/user_test.go index a3b4be091..d9c1a00b6 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -192,3 +192,26 @@ func TestCleanUsername(t *testing.T) { t.Fatal("didn't clean name properly") } } + +func TestRoles(t *testing.T) { + + if !IsValidRoles("admin") { + t.Fatal() + } + + if IsValidRoles("junk") { + t.Fatal() + } + + if IsInRole("system_admin junk", "admin") { + t.Fatal() + } + + if !IsInRole("system_admin junk", "system_admin") { + t.Fatal() + } + + if IsInRole("admin", "system_admin") { + t.Fatal() + } +} diff --git a/model/utils.go b/model/utils.go index 17d1c6317..e19cceba5 100644 --- a/model/utils.go +++ b/model/utils.go @@ -16,11 +16,6 @@ import ( "time" ) -const ( - // Also change web/react/stores/browser_store.jsx BROWSER_STORE_VERSION - ETAG_ROOT_VERSION = "12" -) - type StringMap map[string]string type StringArray []string type EncryptStringMap map[string]string @@ -32,6 +27,7 @@ type AppError struct { RequestId string `json:"request_id"` // The RequestId that's also set in the header StatusCode int `json:"status_code"` // The http status code Where string `json:"-"` // The function where it happened in the form of Struct.Func + IsOAuth bool `json:"is_oauth"` // Whether the error is OAuth specific } func (er *AppError) Error() string { @@ -65,6 +61,7 @@ func NewAppError(where string, message string, details string) *AppError { ap.Where = where ap.DetailedError = details ap.StatusCode = 500 + ap.IsOAuth = false return ap } @@ -202,7 +199,7 @@ func GetSubDomain(s string) (string, string) { func IsValidChannelIdentifier(s string) bool { - if !IsValidAlphaNum(s) { + if !IsValidAlphaNum(s, true) { return false } @@ -213,10 +210,16 @@ func IsValidChannelIdentifier(s string) bool { return true } +var validAlphaNumUnderscore = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`) var validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) -func IsValidAlphaNum(s string) bool { - match := validAlphaNum.MatchString(s) +func IsValidAlphaNum(s string, allowUnderscores bool) bool { + var match bool + if allowUnderscores { + match = validAlphaNumUnderscore.MatchString(s) + } else { + match = validAlphaNum.MatchString(s) + } if !match { return false @@ -227,7 +230,7 @@ func IsValidAlphaNum(s string) bool { func Etag(parts ...interface{}) string { - etag := ETAG_ROOT_VERSION + etag := CurrentVersion for _, part := range parts { etag += fmt.Sprintf(".%v", part) diff --git a/model/version.go b/model/version.go new file mode 100644 index 000000000..8f0c76ebe --- /dev/null +++ b/model/version.go @@ -0,0 +1,90 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strconv" + "strings" +) + +// This is a list of all the current viersions including any patches. +// It should be maitained in chronological order with most current +// release at the front of the list. +var versions = []string{ + "0.8.0", + "0.7.1", + "0.7.0", + "0.6.0", + "0.5.0", +} + +var CurrentVersion string = versions[0] +var BuildNumber = "_BUILD_NUMBER_" +var BuildDate = "_BUILD_DATE_" +var BuildHash = "_BUILD_HASH_" + +func SplitVersion(version string) (int64, int64, int64) { + parts := strings.Split(version, ".") + + major := int64(0) + minor := int64(0) + patch := int64(0) + + if len(parts) > 0 { + major, _ = strconv.ParseInt(parts[0], 10, 64) + } + + if len(parts) > 1 { + minor, _ = strconv.ParseInt(parts[1], 10, 64) + } + + if len(parts) > 2 { + patch, _ = strconv.ParseInt(parts[2], 10, 64) + } + + return major, minor, patch +} + +func GetPreviousVersion(currentVersion string) (int64, int64) { + currentIndex := -1 + currentMajor, currentMinor, _ := SplitVersion(currentVersion) + + for index, version := range versions { + major, minor, _ := SplitVersion(version) + + if currentMajor == major && currentMinor == minor { + currentIndex = index + } + + if currentIndex >= 0 { + if currentMajor != major || currentMinor != minor { + return major, minor + } + } + } + + return 0, 0 +} + +func IsCurrentVersion(versionToCheck string) bool { + currentMajor, currentMinor, _ := SplitVersion(CurrentVersion) + toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) + + if toCheckMajor == currentMajor && toCheckMinor == currentMinor { + return true + } else { + return false + } +} + +func IsPreviousVersion(versionToCheck string) bool { + toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) + prevMajor, prevMinor := GetPreviousVersion(CurrentVersion) + + if toCheckMajor == prevMajor && toCheckMinor == prevMinor { + return true + } else { + return false + } +} diff --git a/model/version_test.go b/model/version_test.go new file mode 100644 index 000000000..da40006be --- /dev/null +++ b/model/version_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "fmt" + "testing" +) + +func TestSplitVersion(t *testing.T) { + major1, minor1, patch1 := SplitVersion("junk") + if major1 != 0 || minor1 != 0 || patch1 != 0 { + t.Fatal() + } + + major2, minor2, patch2 := SplitVersion("1.2.3") + if major2 != 1 || minor2 != 2 || patch2 != 3 { + t.Fatal() + } + + major3, minor3, patch3 := SplitVersion("1.2") + if major3 != 1 || minor3 != 2 || patch3 != 0 { + t.Fatal() + } + + major4, minor4, patch4 := SplitVersion("1") + if major4 != 1 || minor4 != 0 || patch4 != 0 { + t.Fatal() + } + + major5, minor5, patch5 := SplitVersion("1.2.3.junkgoeswhere") + if major5 != 1 || minor5 != 2 || patch5 != 3 { + t.Fatal() + } +} + +func TestGetPreviousVersion(t *testing.T) { + if major, minor := GetPreviousVersion("0.8.0"); major != 0 || minor != 7 { + t.Fatal(major, minor) + } + + if major, minor := GetPreviousVersion("0.7.0"); major != 0 || minor != 6 { + t.Fatal(major, minor) + } + + if major, minor := GetPreviousVersion("0.7.1"); major != 0 || minor != 6 { + t.Fatal(major, minor) + } + + if major, minor := GetPreviousVersion("0.7111.1"); major != 0 || minor != 0 { + t.Fatal(major, minor) + } +} + +func TestIsCurrentVersion(t *testing.T) { + major, minor, patch := SplitVersion(CurrentVersion) + + if !IsCurrentVersion(CurrentVersion) { + t.Fatal() + } + + if !IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor, patch+100)) { + t.Fatal() + } + + if IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor+1, patch)) { + t.Fatal() + } + + if IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major+1, minor, patch)) { + t.Fatal() + } +} diff --git a/model/webhook.go b/model/webhook.go new file mode 100644 index 000000000..9b4db3246 --- /dev/null +++ b/model/webhook.go @@ -0,0 +1,101 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type IncomingWebhook struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` +} + +func (o *IncomingWebhook) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func IncomingWebhookFromJson(data io.Reader) *IncomingWebhook { + decoder := json.NewDecoder(data) + var o IncomingWebhook + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func IncomingWebhookListToJson(l []*IncomingWebhook) string { + b, err := json.Marshal(l) + if err != nil { + return "" + } else { + return string(b) + } +} + +func IncomingWebhookListFromJson(data io.Reader) []*IncomingWebhook { + decoder := json.NewDecoder(data) + var o []*IncomingWebhook + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} + +func (o *IncomingWebhook) IsValid() *AppError { + + if len(o.Id) != 26 { + return NewAppError("IncomingWebhook.IsValid", "Invalid Id", "") + } + + if o.CreateAt == 0 { + return NewAppError("IncomingWebhook.IsValid", "Create at must be a valid time", "id="+o.Id) + } + + if o.UpdateAt == 0 { + return NewAppError("IncomingWebhook.IsValid", "Update at must be a valid time", "id="+o.Id) + } + + if len(o.UserId) != 26 { + return NewAppError("IncomingWebhook.IsValid", "Invalid user id", "") + } + + if len(o.ChannelId) != 26 { + return NewAppError("IncomingWebhook.IsValid", "Invalid channel id", "") + } + + if len(o.TeamId) != 26 { + return NewAppError("IncomingWebhook.IsValid", "Invalid channel id", "") + } + + return nil +} + +func (o *IncomingWebhook) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *IncomingWebhook) PreUpdate() { + o.UpdateAt = GetMillis() +} diff --git a/model/webhook_test.go b/model/webhook_test.go new file mode 100644 index 000000000..ddbe18cd3 --- /dev/null +++ b/model/webhook_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestIncomingWebhookJson(t *testing.T) { + o := IncomingWebhook{Id: NewId()} + json := o.ToJson() + ro := IncomingWebhookFromJson(strings.NewReader(json)) + + if o.Id != ro.Id { + t.Fatal("Ids do not match") + } +} + +func TestIncomingWebhookIsValid(t *testing.T) { + o := IncomingWebhook{} + + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Id = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.CreateAt = GetMillis() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.UpdateAt = GetMillis() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.UserId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.UserId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.ChannelId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.ChannelId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.TeamId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.TeamId = NewId() + if err := o.IsValid(); err != nil { + t.Fatal(err) + } +} + +func TestIncomingWebhookPreSave(t *testing.T) { + o := IncomingWebhook{} + o.PreSave() +} + +func TestIncomingWebhookPreUpdate(t *testing.T) { + o := IncomingWebhook{} + o.PreUpdate() +} |