tsk-35: Update name of user (#38)

Closes #35

Reviewed-on: phoenix/textsender-auth#38
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2026-01-01 22:41:08 +00:00
committed by phoenix
parent de171b790d
commit 689762c72a
11 changed files with 446 additions and 25 deletions
+94 -2
View File
@@ -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": { "/user/password/update": {
"patch": { "patch": {
"security": [ "security": [
@@ -332,7 +389,7 @@ const docTemplate = `{
"git_kundeng_us_phoenix_textsender-models_tx0_user.User": { "git_kundeng_us_phoenix_textsender-models_tx0_user.User": {
"type": "object", "type": "object",
"properties": { "properties": {
"date_created": { "created": {
"type": "string" "type": "string"
}, },
"first_name": { "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": { "handler.UpdatePasswordRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -537,6 +622,9 @@ const docTemplate = `{
"expires_in": { "expires_in": {
"type": "integer" "type": "integer"
}, },
"issued_at": {
"type": "integer"
},
"token_type": { "token_type": {
"type": "string" "type": "string"
}, },
@@ -548,16 +636,20 @@ const docTemplate = `{
"user.ServiceUser": { "user.ServiceUser": {
"type": "object", "type": "object",
"properties": { "properties": {
"date_created": { "created": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "string" "type": "string"
}, },
"last_login": {
"type": "string"
},
"passphrase": { "passphrase": {
"type": "string" "type": "string"
}, },
"username": { "username": {
"description": "TODO: Remove the omitempty tags at a later point",
"type": "string" "type": "string"
} }
} }
+94 -2
View File
@@ -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": { "/user/password/update": {
"patch": { "patch": {
"security": [ "security": [
@@ -326,7 +383,7 @@
"git_kundeng_us_phoenix_textsender-models_tx0_user.User": { "git_kundeng_us_phoenix_textsender-models_tx0_user.User": {
"type": "object", "type": "object",
"properties": { "properties": {
"date_created": { "created": {
"type": "string" "type": "string"
}, },
"first_name": { "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": { "handler.UpdatePasswordRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -531,6 +616,9 @@
"expires_in": { "expires_in": {
"type": "integer" "type": "integer"
}, },
"issued_at": {
"type": "integer"
},
"token_type": { "token_type": {
"type": "string" "type": "string"
}, },
@@ -542,16 +630,20 @@
"user.ServiceUser": { "user.ServiceUser": {
"type": "object", "type": "object",
"properties": { "properties": {
"date_created": { "created": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "string" "type": "string"
}, },
"last_login": {
"type": "string"
},
"passphrase": { "passphrase": {
"type": "string" "type": "string"
}, },
"username": { "username": {
"description": "TODO: Remove the omitempty tags at a later point",
"type": "string" "type": "string"
} }
} }
+61 -2
View File
@@ -2,7 +2,7 @@ basePath: /api/v1
definitions: definitions:
git_kundeng_us_phoenix_textsender-models_tx0_user.User: git_kundeng_us_phoenix_textsender-models_tx0_user.User:
properties: properties:
date_created: created:
type: string type: string
first_name: first_name:
type: string type: string
@@ -108,6 +108,24 @@ definitions:
message: message:
type: string type: string
type: object 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: handler.UpdatePasswordRequest:
properties: properties:
confirmed_password: confirmed_password:
@@ -134,6 +152,8 @@ definitions:
type: string type: string
expires_in: expires_in:
type: integer type: integer
issued_at:
type: integer
token_type: token_type:
type: string type: string
user_id: user_id:
@@ -141,13 +161,16 @@ definitions:
type: object type: object
user.ServiceUser: user.ServiceUser:
properties: properties:
date_created: created:
type: string type: string
id: id:
type: string type: string
last_login:
type: string
passphrase: passphrase:
type: string type: string
username: username:
description: 'TODO: Remove the omitempty tags at a later point'
type: string type: string
type: object type: object
host: localhost:9080 host: localhost:9080
@@ -321,6 +344,42 @@ paths:
summary: Obtain a refresh token summary: Obtain a refresh token
tags: tags:
- refresh - 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: /user/password/update:
patch: patch:
consumes: consumes:
+1 -1
View File
@@ -3,7 +3,7 @@ module git.kundeng.us/phoenix/textsender-auth
go 1.25.4 go 1.25.4
require ( 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/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
+2 -2
View File
@@ -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.6 h1:d/rhvqjFSt1Ty/3UfwS21be+lYb9EtWEaDtkzfbzEp0=
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/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+10 -7
View File
@@ -1,11 +1,14 @@
package endpoint package endpoint
// Endpoint for registering a user const (
const Register = "/api/v1/register" // Endpoint for registering a user
const Login = "/api/v1/login" Register = "/api/v1/register"
const UpdatePassword = "/api/v1/user/password/update" Login = "/api/v1/login"
UpdatePassword = "/api/v1/user/password/update"
UpdateName = "/api/v1/user/name/update"
const CreateServiceUser = "/api/v1/service/register" CreateServiceUser = "/api/v1/service/register"
const LoginServiceUser = "/api/v1/service/login" LoginServiceUser = "/api/v1/service/login"
const TokenRefresh = "/api/v1/token/refresh" TokenRefresh = "/api/v1/token/refresh"
)
+83 -9
View File
@@ -2,6 +2,7 @@ package handler
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/user" "git.kundeng.us/phoenix/textsender-models/tx0/user"
@@ -12,6 +13,15 @@ import (
"git.kundeng.us/phoenix/textsender-auth/internal/utility" "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 { type RegisterUser struct {
PhoneNumber string `json:"phone_number"` PhoneNumber string `json:"phone_number"`
Username string `json:"username"` Username string `json:"username"`
@@ -29,15 +39,6 @@ type RegisterResponse struct {
Data []RegisterResponseItem `json:"data"` 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 // Register godoc
// @Summary Register user // @Summary Register user
// @Description Create a user that can send texts (requires JWT) // @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) 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)
}
+29
View File
@@ -34,3 +34,32 @@ func TestCreateUserWithMock(t *testing.T) {
assert.NotNil(t, response.Data[0].Id, "Id should not be nil") 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)
}
+33
View File
@@ -3,6 +3,7 @@ package mock
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"sync" "sync"
"time" "time"
@@ -157,3 +158,35 @@ func (m *MockUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastL
return 1, nil 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
}
+37
View File
@@ -20,6 +20,7 @@ type UserStore interface {
UserExists(ctx context.Context, username string) (bool, error) UserExists(ctx context.Context, username string) (bool, error)
UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error) UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error)
UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (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 { type PGUserStore struct {
@@ -134,3 +135,39 @@ func (s *PGUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLog
return affected.RowsAffected(), nil 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
}
}
}
}
+2
View File
@@ -5,6 +5,8 @@ DROP TABLE IF EXISTS service_users CASCADE;
CREATE TABLE users ( CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
first_name TEXT NULL,
last_name TEXT NULL,
phone_number TEXT NOT NULL, phone_number TEXT NOT NULL,
username TEXT NOT NULL, username TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,