From c01d9ad6cf3f8bb2ad4145441816598d8ffa2d9e Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 30 Jan 2017 08:30:02 -0500 Subject: Implement APIv4 infrastructure (#5191) * Implement APIv4 infrastructure * Update parameter requirement functions per feedback --- model/client.go | 5 +- model/client4.go | 222 ++++++++++++++++++++++++++++++++++++++++++++++ model/user.go | 25 +++--- model/utils.go | 12 +++ model/websocket_client.go | 4 +- 5 files changed, 252 insertions(+), 16 deletions(-) create mode 100644 model/client4.go (limited to 'model') diff --git a/model/client.go b/model/client.go index c75121e97..52e1ae989 100644 --- a/model/client.go +++ b/model/client.go @@ -40,7 +40,8 @@ const ( API_URL_SUFFIX_V1 = "/api/v1" API_URL_SUFFIX_V3 = "/api/v3" - API_URL_SUFFIX = API_URL_SUFFIX_V3 + API_URL_SUFFIX_V4 = "/api/v4" + API_URL_SUFFIX = API_URL_SUFFIX_V4 ) type Result struct { @@ -71,7 +72,7 @@ type Client struct { // NewClient constructs a new client with convienence methods for talking to // the server. func NewClient(url string) *Client { - return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", "", "", "", "", ""} + return &Client{url, url + API_URL_SUFFIX_V3, &http.Client{}, "", "", "", "", "", ""} } func closeBody(r *http.Response) { diff --git a/model/client4.go b/model/client4.go new file mode 100644 index 000000000..3c585fbe4 --- /dev/null +++ b/model/client4.go @@ -0,0 +1,222 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "fmt" + "net/http" + "strings" +) + +type Response struct { + StatusCode int + Error *AppError + RequestId string + Etag string + ServerVersion string +} + +type Client4 struct { + Url string // The location of the server, for example "http://localhost:8065" + ApiUrl string // The api location of the server, for example "http://localhost:8065/api/v4" + HttpClient *http.Client // The http client + AuthToken string + AuthType string +} + +func NewAPIv4Client(url string) *Client4 { + return &Client4{url, url + API_URL_SUFFIX, &http.Client{}, "", ""} +} + +func BuildResponse(r *http.Response) *Response { + return &Response{ + StatusCode: r.StatusCode, + RequestId: r.Header.Get(HEADER_REQUEST_ID), + Etag: r.Header.Get(HEADER_ETAG_SERVER), + ServerVersion: r.Header.Get(HEADER_VERSION_ID), + } +} + +func (c *Client4) SetOAuthToken(token string) { + c.AuthToken = token + c.AuthType = HEADER_TOKEN +} + +func (c *Client4) ClearOAuthToken() { + c.AuthToken = "" + c.AuthType = HEADER_BEARER +} + +func (c *Client4) GetUsersRoute() string { + return fmt.Sprintf("/users") +} + +func (c *Client4) GetUserRoute(userId string) string { + return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId) +} + +func (c *Client4) GetTeamsRoute() string { + return fmt.Sprintf("/teams") +} + +func (c *Client4) GetTeamRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamsRoute()+"/%v", teamId) +} + +func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodGet, url, "", etag) +} + +func (c *Client4) DoApiPost(url string, data string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodPost, url, data, "") +} + +func (c *Client4) DoApiPut(url string, data string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodPut, url, data, "") +} + +func (c *Client4) DoApiDelete(url string, data string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodDelete, url, "", "") +} + +func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, *AppError) { + rq, _ := http.NewRequest(method, c.ApiUrl+url, strings.NewReader(data)) + rq.Close = true + + if len(etag) > 0 { + rq.Header.Set(HEADER_ETAG_CLIENT, etag) + } + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) + } else if rp.StatusCode == 304 { + return rp, nil + } else if rp.StatusCode >= 300 { + defer closeBody(rp) + return rp, AppErrorFromJson(rp.Body) + } else { + return rp, nil + } +} + +// CheckStatusOK is a convenience function for checking the standard OK response +// from the web service. +func CheckStatusOK(r *http.Response) bool { + m := MapFromJson(r.Body) + defer closeBody(r) + + if m != nil && m[STATUS] == STATUS_OK { + return true + } + + return false +} + +// Authentication Section + +// LoginById authenticates a user by user id and password. +func (c *Client4) LoginById(id string, password string) (*User, *Response) { + m := make(map[string]string) + m["id"] = id + m["password"] = password + return c.login(m) +} + +// Login authenticates a user by login id, which can be username, email or some sort +// of SSO identifier based on server configuration, and a password. +func (c *Client4) Login(loginId string, password string) (*User, *Response) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + return c.login(m) +} + +// LoginByLdap authenticates a user by LDAP id and password. +func (c *Client4) LoginByLdap(loginId string, password string) (*User, *Response) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + m["ldap_only"] = "true" + return c.login(m) +} + +// LoginWithDevice authenticates a user by login id (username, email or some sort +// of SSO identifier based on configuration), password and attaches a device id to +// the session. +func (c *Client4) LoginWithDevice(loginId string, password string, deviceId string) (*User, *Response) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + m["device_id"] = deviceId + return c.login(m) +} + +func (c *Client4) login(m map[string]string) (*User, *Response) { + if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + c.AuthToken = r.Header.Get(HEADER_TOKEN) + c.AuthType = HEADER_BEARER + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) + } +} + +// Logout terminates the current user's session. +func (c *Client4) Logout() (bool, *Response) { + if r, err := c.DoApiPost("/users/logout", ""); err != nil { + return false, &Response{StatusCode: r.StatusCode, Error: err} + } else { + c.AuthToken = "" + c.AuthType = HEADER_BEARER + + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + +// User Section + +// CreateUser creates a user in the system based on the provided user struct. +func (c *Client4) CreateUser(user *User) (*User, *Response) { + if r, err := c.DoApiPost(c.GetUsersRoute(), user.ToJson()); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) + } +} + +// GetUser returns a user based on the provided user id string. +func (c *Client4) GetUser(userId, etag string) (*User, *Response) { + if r, err := c.DoApiGet(c.GetUserRoute(userId), etag); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) + } +} + +// UpdateUser updates a user in the system based on the provided user struct. +func (c *Client4) UpdateUser(user *User) (*User, *Response) { + if r, err := c.DoApiPut(c.GetUserRoute(user.Id), user.ToJson()); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) + } +} + +// Team Section +// to be filled in.. + +// Channel Section +// to be filled in.. + +// Post Section +// to be filled in.. diff --git a/model/user.go b/model/user.go index 876ba70e7..49963bb41 100644 --- a/model/user.go +++ b/model/user.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "regexp" "strings" "unicode/utf8" @@ -56,51 +57,51 @@ type User struct { func (u *User) IsValid() *AppError { if len(u.Id) != 26 { - return NewLocAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "") + return NewAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "", http.StatusBadRequest) } if u.CreateAt == 0 { - return NewLocAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if u.UpdateAt == 0 { - return NewLocAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if !IsValidUsername(u.Username) { - return NewLocAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if len(u.Email) > 128 || len(u.Email) == 0 { - return NewLocAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if utf8.RuneCountInString(u.Nickname) > 64 { - return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if utf8.RuneCountInString(u.Position) > 35 { - return NewLocAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if utf8.RuneCountInString(u.FirstName) > 64 { - return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if utf8.RuneCountInString(u.LastName) > 64 { - return NewLocAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if u.AuthData != nil && len(*u.AuthData) > 128 { - return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if u.AuthData != nil && len(*u.AuthData) > 0 && len(u.AuthService) == 0 { - return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } if len(u.Password) > 0 && u.AuthData != nil && len(*u.AuthData) > 0 { - return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id) + return NewAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id, http.StatusBadRequest) } return nil diff --git a/model/utils.go b/model/utils.go index 0ce243fe7..05143b20d 100644 --- a/model/utils.go +++ b/model/utils.go @@ -93,6 +93,18 @@ func AppErrorFromJson(data io.Reader) *AppError { } } +func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError { + ap := &AppError{} + ap.Id = id + ap.params = params + ap.Message = id + ap.Where = where + ap.DetailedError = details + ap.StatusCode = status + ap.IsOAuth = false + return ap +} + func NewLocAppError(where string, id string, params map[string]interface{}, details string) *AppError { ap := &AppError{} ap.Id = id diff --git a/model/websocket_client.go b/model/websocket_client.go index c91855134..083fe110a 100644 --- a/model/websocket_client.go +++ b/model/websocket_client.go @@ -26,14 +26,14 @@ type WebSocketClient struct { // NewWebSocketClient constructs a new WebSocket client with convienence // methods for talking to the server. func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { - conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", nil) + conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil) if err != nil { return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error()) } client := &WebSocketClient{ url, - url + API_URL_SUFFIX, + url + API_URL_SUFFIX_V3, conn, authToken, 1, -- cgit v1.2.3-1-g7c22