diff --git a/docs/docs.go b/docs/docs.go index 80841ca..a5b3551 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -276,6 +276,63 @@ const docTemplate = `{ } } }, + "/user/name/update": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the first or last name of a user (requires JWT)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Update name of user", + "parameters": [ + { + "description": "Data to update name of user", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateNameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + } + } + } + }, "/user/password/update": { "patch": { "security": [ @@ -332,7 +389,7 @@ const docTemplate = `{ "git_kundeng_us_phoenix_textsender-models_tx0_user.User": { "type": "object", "properties": { - "date_created": { + "created": { "type": "string" }, "first_name": { @@ -497,6 +554,34 @@ const docTemplate = `{ } } }, + "handler.UpdateNameRequest": { + "type": "object", + "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "handler.UpdateNameResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User" + } + }, + "message": { + "type": "string" + } + } + }, "handler.UpdatePasswordRequest": { "type": "object", "properties": { @@ -537,6 +622,9 @@ const docTemplate = `{ "expires_in": { "type": "integer" }, + "issued_at": { + "type": "integer" + }, "token_type": { "type": "string" }, @@ -548,16 +636,20 @@ const docTemplate = `{ "user.ServiceUser": { "type": "object", "properties": { - "date_created": { + "created": { "type": "string" }, "id": { "type": "string" }, + "last_login": { + "type": "string" + }, "passphrase": { "type": "string" }, "username": { + "description": "TODO: Remove the omitempty tags at a later point", "type": "string" } } diff --git a/docs/swagger.json b/docs/swagger.json index ffc891c..37f7ab1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -270,6 +270,63 @@ } } }, + "/user/name/update": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the first or last name of a user (requires JWT)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Update name of user", + "parameters": [ + { + "description": "Data to update name of user", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateNameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.UpdateNameResponse" + } + } + } + } + }, "/user/password/update": { "patch": { "security": [ @@ -326,7 +383,7 @@ "git_kundeng_us_phoenix_textsender-models_tx0_user.User": { "type": "object", "properties": { - "date_created": { + "created": { "type": "string" }, "first_name": { @@ -491,6 +548,34 @@ } } }, + "handler.UpdateNameRequest": { + "type": "object", + "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "handler.UpdateNameResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User" + } + }, + "message": { + "type": "string" + } + } + }, "handler.UpdatePasswordRequest": { "type": "object", "properties": { @@ -531,6 +616,9 @@ "expires_in": { "type": "integer" }, + "issued_at": { + "type": "integer" + }, "token_type": { "type": "string" }, @@ -542,16 +630,20 @@ "user.ServiceUser": { "type": "object", "properties": { - "date_created": { + "created": { "type": "string" }, "id": { "type": "string" }, + "last_login": { + "type": "string" + }, "passphrase": { "type": "string" }, "username": { + "description": "TODO: Remove the omitempty tags at a later point", "type": "string" } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4954593..c726852 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2,7 +2,7 @@ basePath: /api/v1 definitions: git_kundeng_us_phoenix_textsender-models_tx0_user.User: properties: - date_created: + created: type: string first_name: type: string @@ -108,6 +108,24 @@ definitions: message: type: string type: object + handler.UpdateNameRequest: + properties: + first_name: + type: string + last_name: + type: string + user_id: + type: string + type: object + handler.UpdateNameResponse: + properties: + data: + items: + $ref: '#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User' + type: array + message: + type: string + type: object handler.UpdatePasswordRequest: properties: confirmed_password: @@ -134,6 +152,8 @@ definitions: type: string expires_in: type: integer + issued_at: + type: integer token_type: type: string user_id: @@ -141,13 +161,16 @@ definitions: type: object user.ServiceUser: properties: - date_created: + created: type: string id: type: string + last_login: + type: string passphrase: type: string username: + description: 'TODO: Remove the omitempty tags at a later point' type: string type: object host: localhost:9080 @@ -321,6 +344,42 @@ paths: summary: Obtain a refresh token tags: - refresh + /user/name/update: + patch: + consumes: + - application/json + description: Update the first or last name of a user (requires JWT) + parameters: + - description: Data to update name of user + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdateNameRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.UpdateNameResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.UpdateNameResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/handler.UpdateNameResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.UpdateNameResponse' + security: + - BearerAuth: [] + summary: Update name of user + tags: + - users /user/password/update: patch: consumes: diff --git a/go.mod b/go.mod index 45cb701..69cd0ff 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.kundeng.us/phoenix/textsender-auth go 1.25.4 require ( - git.kundeng.us/phoenix/textsender-models v0.1.5-30-6f9e112b72-556 + git.kundeng.us/phoenix/textsender-models v0.1.6 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/cors v1.2.2 github.com/golang-jwt/jwt/v5 v5.3.0 diff --git a/go.sum b/go.sum index 042367d..ed9053a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.kundeng.us/phoenix/textsender-models v0.1.5-30-6f9e112b72-556 h1:Ispsun00898NAWwnTfTAQQxbbVz+Te0bTl8z6t9E+gs= -git.kundeng.us/phoenix/textsender-models v0.1.5-30-6f9e112b72-556/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0= +git.kundeng.us/phoenix/textsender-models v0.1.6 h1:d/rhvqjFSt1Ty/3UfwS21be+lYb9EtWEaDtkzfbzEp0= +git.kundeng.us/phoenix/textsender-models v0.1.6/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/internal/handler/endpoint/endpoint.go b/internal/handler/endpoint/endpoint.go index c198bde..8e3b61f 100644 --- a/internal/handler/endpoint/endpoint.go +++ b/internal/handler/endpoint/endpoint.go @@ -1,11 +1,14 @@ package endpoint -// Endpoint for registering a user -const Register = "/api/v1/register" -const Login = "/api/v1/login" -const UpdatePassword = "/api/v1/user/password/update" +const ( + // Endpoint for registering a user + Register = "/api/v1/register" + Login = "/api/v1/login" + UpdatePassword = "/api/v1/user/password/update" + UpdateName = "/api/v1/user/name/update" -const CreateServiceUser = "/api/v1/service/register" -const LoginServiceUser = "/api/v1/service/login" + CreateServiceUser = "/api/v1/service/register" + LoginServiceUser = "/api/v1/service/login" -const TokenRefresh = "/api/v1/token/refresh" + TokenRefresh = "/api/v1/token/refresh" +) diff --git a/internal/handler/register.go b/internal/handler/register.go index e5a7dbc..b9b22d6 100644 --- a/internal/handler/register.go +++ b/internal/handler/register.go @@ -2,6 +2,7 @@ package handler import ( "fmt" + "log" "net/http" "git.kundeng.us/phoenix/textsender-models/tx0/user" @@ -12,6 +13,15 @@ import ( "git.kundeng.us/phoenix/textsender-auth/internal/utility" ) +type UserHandler struct { + Config *config.Config + UserStore store.UserStore +} + +func NewUserHandler(cfg *config.Config, userStore store.UserStore) *UserHandler { + return &UserHandler{Config: cfg, UserStore: userStore} +} + type RegisterUser struct { PhoneNumber string `json:"phone_number"` Username string `json:"username"` @@ -29,15 +39,6 @@ type RegisterResponse struct { Data []RegisterResponseItem `json:"data"` } -type UserHandler struct { - Config *config.Config - UserStore store.UserStore -} - -func NewUserHandler(cfg *config.Config, userStore store.UserStore) *UserHandler { - return &UserHandler{Config: cfg, UserStore: userStore} -} - // Register godoc // @Summary Register user // @Description Create a user that can send texts (requires JWT) @@ -112,3 +113,76 @@ func (u *UserHandler) Register(w http.ResponseWriter, r *http.Request) { RespondWithJson(w, statusCode, &resp) } + +type UpdateNameRequest struct { + Firstname *string `json:"first_name,omitempty"` + Lastname *string `json:"last_name,omitempty"` + UserId uuid.UUID `json:"user_id"` +} + +type UpdateNameResponse struct { + Message string `json:"message"` + Data []*user.User `json:"data"` +} + +// UpdateName godoc +// @Summary Update name of user +// @Description Update the first or last name of a user (requires JWT) +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body UpdateNameRequest true "Data to update name of user" +// @Success 200 {object} UpdateNameResponse +// @Failure 400 {object} UpdateNameResponse +// @Failure 403 {object} UpdateNameResponse +// @Failure 500 {object} UpdateNameResponse +// @Router /user/name/update [patch] +func (u *UserHandler) UpdateName(w http.ResponseWriter, r *http.Request) { + var req UpdateNameRequest + err := ExtractFromRequest(r, &req) + if err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + defer r.Body.Close() + + var statusCode int + var resp UpdateNameResponse + updateFirstname := req.Firstname != nil && len(*req.Firstname) > 0 + updateLastname := req.Lastname != nil && len(*req.Lastname) > 0 + + if req.UserId == uuid.Nil { + statusCode = http.StatusBadRequest + resp.Message = "User Id not provided" + } else if !updateFirstname && !updateLastname { + statusCode = http.StatusBadRequest + resp.Message = "No name provided" + } else { + ctx := r.Context() + if usr, err := u.UserStore.GetUserByID(ctx, req.UserId); err != nil { + log.Println("Error:", err) + statusCode = http.StatusInternalServerError + resp.Message = err.Error() + } else { + if usr == nil { + statusCode = http.StatusNotFound + resp.Message = "User not found" + } else { + // Add query to update names + if rowsAffected, err := u.UserStore.UpdateName(ctx, req.Firstname, req.Lastname, usr); err != nil { + statusCode = http.StatusInternalServerError + resp.Message = err.Error() + } else { + log.Println("Rows updated:", rowsAffected) + statusCode = http.StatusOK + resp.Message = "Successful" + resp.Data = append(resp.Data, usr) + } + } + } + } + + RespondWithJson(w, statusCode, &resp) +} diff --git a/internal/handler/register_test.go b/internal/handler/register_test.go index 4aa043f..4bd35c7 100644 --- a/internal/handler/register_test.go +++ b/internal/handler/register_test.go @@ -34,3 +34,32 @@ func TestCreateUserWithMock(t *testing.T) { assert.NotNil(t, response.Data[0].Id, "Id should not be nil") } + +func TestUpdateNameOfUser(t *testing.T) { + ctx := t.Context() + cfg := GetConfig() + userStore := mock.NewMockUserStore() + + usr := GetTestUser() + if err := userStore.CreateUser(ctx, &usr); err != nil { + assert.NoError(t, err, "Error creating user") + } + + testNameChange := UpdateNameRequest{} + testNameChange.Firstname = &[]string{"Bob"}[0] + testNameChange.Lastname = &[]string{"De-Buildor"}[0] + testNameChange.UserId = usr.Id + jsonValue, _ := json.Marshal(testNameChange) + + req, _ := http.NewRequest("PATCH", endpoint.UpdateName, strings.NewReader(string(jsonValue))) + rr := httptest.NewRecorder() + + handler := NewUserHandler(cfg, userStore) + handler.UpdateName(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response UpdateNameResponse + err := json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) +} diff --git a/internal/store/mock/user_store.go b/internal/store/mock/user_store.go index 3380592..202a497 100644 --- a/internal/store/mock/user_store.go +++ b/internal/store/mock/user_store.go @@ -3,6 +3,7 @@ package mock import ( "context" "errors" + "fmt" "sync" "time" @@ -157,3 +158,35 @@ func (m *MockUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastL return 1, nil } + +func (m *MockUserStore) UpdateName(ctx context.Context, firstname *string, lastname *string, usr *user.User) (int64, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return 0, m.Error + } + + if firstname == nil && lastname == nil { + return 0, fmt.Errorf("Names not provided") + } + + user, exists := m.Users[usr.Id] + if !exists { + return 0, errors.New("User not found") + } + + if firstname != nil && lastname != nil { + user.Firstname = firstname + user.Lastname = lastname + } else if firstname != nil { + user.Firstname = firstname + } else { + user.Lastname = lastname + } + + m.Users[usr.Id] = user + m.UsersByUsername[user.Username] = user + + return 1, nil +} diff --git a/internal/store/user.go b/internal/store/user.go index e06197e..baa0444 100644 --- a/internal/store/user.go +++ b/internal/store/user.go @@ -20,6 +20,7 @@ type UserStore interface { UserExists(ctx context.Context, username string) (bool, error) UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) + UpdateName(ctx context.Context, firstname *string, lastname *string, usr *user.User) (int64, error) } type PGUserStore struct { @@ -134,3 +135,39 @@ func (s *PGUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLog return affected.RowsAffected(), nil } } + +func (s *PGUserStore) UpdateName(ctx context.Context, firstname *string, lastname *string, usr *user.User) (int64, error) { + var query string + + if firstname == nil && lastname == nil { + return 0, fmt.Errorf("Provided names are empty") + } else { + tableName := "users" + if firstname != nil && lastname != nil { + query = fmt.Sprintf("UPDATE %s SET first_name = $1, last_name = $2 WHERE id = $3", tableName) + if affected, err := s.db.Exec(ctx, query, firstname, lastname, usr.Id); err != nil { + return 0, err + } else { + usr.Firstname = firstname + usr.Lastname = lastname + return affected.RowsAffected(), nil + } + } else if firstname != nil { + query = fmt.Sprintf("UPDATE %s SET first_name = $1 WHERE id = $2", tableName) + if affected, err := s.db.Exec(ctx, query, firstname, usr.Id); err != nil { + return 0, err + } else { + usr.Firstname = firstname + return affected.RowsAffected(), nil + } + } else { + query = fmt.Sprintf("UPDATE %s SET last_name = $1 WHERE id = $2", tableName) + if affected, err := s.db.Exec(ctx, query, lastname, usr.Id); err != nil { + return 0, err + } else { + usr.Lastname = lastname + return affected.RowsAffected(), nil + } + } + } +} diff --git a/migrations/schema.sql b/migrations/schema.sql index fe980ea..2eb2c19 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -5,6 +5,8 @@ DROP TABLE IF EXISTS service_users CASCADE; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + first_name TEXT NULL, + last_name TEXT NULL, phone_number TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL,