tsk-30: Update password (#33)

Closes #30

Reviewed-on: phoenix/textsender-auth#33
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-12-26 21:14:40 +00:00
committed by phoenix
parent 760a778a27
commit d34cad033d
22 changed files with 727 additions and 98 deletions
-1
View File
@@ -23,7 +23,6 @@ make build
Generate API documentation
```
go install github.com/swaggo/swag/cmd/swag@latest
go get -u github.com/swaggo/http-swagger/v2
swag init --generalInfo main.go --dir ./cmd/api,./internal/handler --output docs/ --parseDependency --parseInternal
```
+3
View File
@@ -21,6 +21,7 @@ import (
"git.kundeng.us/phoenix/textsender-auth/internal/handler"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
mdleware "git.kundeng.us/phoenix/textsender-auth/internal/middleware"
"git.kundeng.us/phoenix/textsender-auth/internal/services"
"git.kundeng.us/phoenix/textsender-auth/internal/store"
)
@@ -82,6 +83,7 @@ func main() {
refreshHandler := handler.NewRefreshHandler(cfg, userStore, serviceStore)
router := chi.NewRouter()
jwtService := services.NewJWTService(config.GetSecretKey())
// Configure CORS
router.Use(cors.Handler(cors.Options{
@@ -102,6 +104,7 @@ func main() {
router.Method("Post", endpoint.CreateServiceUser, http.HandlerFunc(serviceHandler.Register))
router.Method("Post", endpoint.LoginServiceUser, http.HandlerFunc(serviceHandler.Login))
router.Method("Post", endpoint.TokenRefresh, http.HandlerFunc(refreshHandler.Refresh))
router.Method("PATCH", endpoint.UpdatePassword, mdleware.AuthMiddleware(jwtService)(http.HandlerFunc(loginHandler.UpdatePassword)))
router.Method("GET", "/swagger/*", httpSwagger.Handler(
httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.Port)),
+13 -8
View File
@@ -4,21 +4,24 @@ import (
"context"
"flag"
"fmt"
"net/http"
"os"
"path"
"testing"
"github.com/gorilla/mux"
"github.com/go-chi/chi/v5"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/db"
"git.kundeng.us/phoenix/textsender-auth/internal/handler"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
mdleware "git.kundeng.us/phoenix/textsender-auth/internal/middleware"
"git.kundeng.us/phoenix/textsender-auth/internal/services"
"git.kundeng.us/phoenix/textsender-auth/internal/store"
)
var testRouter *mux.Router
var testRouter *chi.Mux
func TestMain(m *testing.M) {
cfg := load()
@@ -44,12 +47,14 @@ func TestMain(m *testing.M) {
serviceHandler := handler.NewServiceHandler(cfg, serviceStore)
refreshHandler := handler.NewRefreshHandler(cfg, userStore, serviceStore)
testRouter = mux.NewRouter()
testRouter.HandleFunc(endpoint.Register, userHandler.Register).Methods("POST")
testRouter.HandleFunc(endpoint.Login, loginHandler.Login).Methods("POST")
testRouter.HandleFunc(endpoint.CreateServiceUser, serviceHandler.Register).Methods("POST")
testRouter.HandleFunc(endpoint.LoginServiceUser, serviceHandler.Login).Methods("POST")
testRouter.HandleFunc(endpoint.TokenRefresh, refreshHandler.Refresh).Methods("POST")
testRouter = chi.NewRouter()
jwtService := services.NewJWTService(config.GetSecretKey())
testRouter.Method("POST", endpoint.Register, http.HandlerFunc(userHandler.Register))
testRouter.Method("POST", endpoint.Login, http.HandlerFunc(loginHandler.Login))
testRouter.Method("POST", endpoint.CreateServiceUser, http.HandlerFunc(serviceHandler.Register))
testRouter.Method("POST", endpoint.LoginServiceUser, http.HandlerFunc(serviceHandler.Login))
testRouter.Method("POST", endpoint.TokenRefresh, http.HandlerFunc(refreshHandler.Refresh))
testRouter.Method("PATCH", endpoint.UpdatePassword, mdleware.AuthMiddleware(jwtService)(http.HandlerFunc(loginHandler.UpdatePassword)))
code := m.Run()
os.Exit(code)
+114
View File
@@ -275,9 +275,89 @@ const docTemplate = `{
}
}
}
},
"/user/password/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the password of a regular account (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Update Password",
"parameters": [
{
"description": "Needed data to update password",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
}
}
}
}
},
"definitions": {
"git_kundeng_us_phoenix_textsender-models_tx0_user.User": {
"type": "object",
"properties": {
"date_created": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "string"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"password": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.LoginAccount": {
"type": "object",
"properties": {
@@ -417,6 +497,37 @@ const docTemplate = `{
}
}
},
"handler.UpdatePasswordRequest": {
"type": "object",
"properties": {
"confirmed_password": {
"type": "string"
},
"current_password": {
"type": "string"
},
"updated_password": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdatePasswordResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User"
}
},
"message": {
"type": "string"
}
}
},
"token.Login": {
"type": "object",
"properties": {
@@ -428,6 +539,9 @@ const docTemplate = `{
},
"token_type": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
+114
View File
@@ -269,9 +269,89 @@
}
}
}
},
"/user/password/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the password of a regular account (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Update Password",
"parameters": [
{
"description": "Needed data to update password",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
}
}
}
}
},
"definitions": {
"git_kundeng_us_phoenix_textsender-models_tx0_user.User": {
"type": "object",
"properties": {
"date_created": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "string"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"password": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.LoginAccount": {
"type": "object",
"properties": {
@@ -411,6 +491,37 @@
}
}
},
"handler.UpdatePasswordRequest": {
"type": "object",
"properties": {
"confirmed_password": {
"type": "string"
},
"current_password": {
"type": "string"
},
"updated_password": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdatePasswordResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User"
}
},
"message": {
"type": "string"
}
}
},
"token.Login": {
"type": "object",
"properties": {
@@ -422,6 +533,9 @@
},
"token_type": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
+73
View File
@@ -1,5 +1,24 @@
basePath: /api/v1
definitions:
git_kundeng_us_phoenix_textsender-models_tx0_user.User:
properties:
date_created:
type: string
first_name:
type: string
id:
type: string
last_login:
type: string
last_name:
type: string
password:
type: string
phone_number:
type: string
username:
type: string
type: object
handler.LoginAccount:
properties:
password:
@@ -89,6 +108,26 @@ definitions:
message:
type: string
type: object
handler.UpdatePasswordRequest:
properties:
confirmed_password:
type: string
current_password:
type: string
updated_password:
type: string
user_id:
type: string
type: object
handler.UpdatePasswordResponse:
properties:
data:
items:
$ref: '#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User'
type: array
message:
type: string
type: object
token.Login:
properties:
access_token:
@@ -97,6 +136,8 @@ definitions:
type: integer
token_type:
type: string
user_id:
type: string
type: object
user.ServiceUser:
properties:
@@ -280,6 +321,38 @@ paths:
summary: Obtain a refresh token
tags:
- refresh
/user/password/update:
patch:
consumes:
- application/json
description: Update the password of a regular account (requires JWT)
parameters:
- description: Needed data to update password
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.UpdatePasswordRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.UpdatePasswordResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.UpdatePasswordResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.UpdatePasswordResponse'
security:
- BearerAuth: []
summary: Update Password
tags:
- users
securityDefinitions:
BearerAuth:
description: JWT Bearer Token
-1
View File
@@ -8,7 +8,6 @@ require (
github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.7.5
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.11.1
-2
View File
@@ -26,8 +26,6 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+1
View File
@@ -6,3 +6,4 @@ const Login = "/api/v1/login"
const CreateServiceUser = "/api/v1/service/register"
const LoginServiceUser = "/api/v1/service/login"
const TokenRefresh = "/api/v1/token/refresh"
const UpdatePassword = "/api/v1/user/password/update"
+116 -27
View File
@@ -1,16 +1,27 @@
package handler
import (
"fmt"
"log"
"net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/token"
"git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/store"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
type LoginHandler struct {
Config *config.Config
UserStore store.UserStore
}
func NewLoginHandler(cfg *config.Config, userStore store.UserStore) *LoginHandler {
return &LoginHandler{Config: cfg, UserStore: userStore}
}
type LoginAccount struct {
Username string `json:"username"`
Password string `json:"password"`
@@ -21,15 +32,6 @@ type LoginResponse struct {
Data []token.Login `json:"data"`
}
type LoginHandler struct {
Config *config.Config
UserStore store.UserStore
}
func NewLoginHandler(cfg *config.Config, userStore store.UserStore) *LoginHandler {
return &LoginHandler{Config: cfg, UserStore: userStore}
}
// Login godoc
// @Summary Login
// @Description Login and be given an access token (requires JWT)
@@ -55,7 +57,7 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if exists, err := l.UserStore.UserExists(ctx, req.Username); err != nil {
fmt.Printf("Error: %v", err)
log.Println("Error:", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
@@ -67,23 +69,28 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
hashing := utility.HashMash{Password: req.Password}
if hashing.CheckPasswordHash(req.Password, user.Password) {
var tokGen utility.TokenGenerator
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*user); err != nil {
fmt.Println(err.Error())
statusCode = http.StatusInternalServerError
resp.Message = "Error generating token"
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, *myToken)
resp.Message = "Successful"
}
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Password); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusNotFound
resp.Message = "User not found"
if hashing.CheckPasswordHash(req.Password, user.Password) {
var tokGen utility.TokenGenerator
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*user); err != nil {
log.Println(err.Error())
statusCode = http.StatusInternalServerError
resp.Message = "Error generating token"
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, *myToken)
resp.Message = "Successful"
}
} else {
statusCode = http.StatusNotFound
resp.Message = "User not found"
}
}
}
}
@@ -91,3 +98,85 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
RespondWithJson(w, statusCode, &resp)
}
type UpdatePasswordRequest struct {
UserId uuid.UUID `json:"user_id"`
CurrentPassword string `json:"current_password"`
UpdatedPassword string `json:"updated_password"`
ConfirmedPassword string `json:"confirmed_password"`
}
type UpdatePasswordResponse struct {
Message string `json:"message"`
Data []user.User `json:"data"`
}
// UpdatePassword godoc
// @Summary Update Password
// @Description Update the password of a regular account (requires JWT)
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body UpdatePasswordRequest true "Needed data to update password"
// @Success 200 {object} UpdatePasswordResponse
// @Failure 400 {object} UpdatePasswordResponse
// @Failure 500 {object} UpdatePasswordResponse
// @Router /user/password/update [patch]
func (l *LoginHandler) UpdatePassword(w http.ResponseWriter, r *http.Request) {
var req UpdatePasswordRequest
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var resp UpdatePasswordResponse
ctx := r.Context()
if usr, err := l.UserStore.GetUserByID(ctx, req.UserId); err != nil {
log.Println("Error:", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.CurrentPassword); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if hashing.CheckPasswordHash(req.CurrentPassword, usr.Password) {
if req.UpdatedPassword == req.ConfirmedPassword {
// Hash password
err := hashing.SetPassword(req.UpdatedPassword)
hashedPassword, err := hashing.HashPassword()
if err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
// Update user password
usr.Password = hashedPassword
// Save user in DB
if rowsAffected, err := l.UserStore.UpdatePassword(ctx, usr.Id, usr.Password); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Rows affected:", rowsAffected)
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, *usr)
}
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Passwords do not match"
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "User not found"
}
}
}
RespondWithJson(w, statusCode, &resp)
}
+29 -11
View File
@@ -13,7 +13,6 @@ import (
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-auth/internal/store/mock"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
func TestLogin(t *testing.T) {
@@ -21,20 +20,13 @@ func TestLogin(t *testing.T) {
mockstore := mock.NewMockUserStore()
handler := NewLoginHandler(cfg, mockstore)
testUser := GetTestUser()
unhashedPassword := testUser.Password
hashing := utility.HashMash{Password: testUser.Password}
hashedPassword, err := hashing.HashPassword()
assert.NoError(t, err)
testUser.Password = hashedPassword
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
mockstore.CreateUser(ctx, &testUser)
testUser, unhashedPassword, err := createUser(ctx, mockstore)
assert.NoError(t, err, "Error Creating user")
loginUser := LoginAccount{Username: testUser.Username, Password: unhashedPassword}
loginUser := LoginAccount{Username: testUser.Username, Password: *unhashedPassword}
jsonValue, _ := json.Marshal(loginUser)
req, _ := http.NewRequest("POST", endpoint.Login, strings.NewReader(string(jsonValue)))
@@ -50,3 +42,29 @@ func TestLogin(t *testing.T) {
assert.NotEmpty(t, response.Data, "An access token should have been returned")
}
func TestUpdatePassword(t *testing.T) {
cfg := GetConfig()
mockstore := mock.NewMockUserStore()
handler := NewLoginHandler(cfg, mockstore)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
testUser, unhashedPassword, err := createUser(ctx, mockstore)
assert.NoError(t, err, "Error Creating user")
assert.NotNil(t, testUser, "User should not be nil")
updatedPassword := "TakeATrip2yonder!"
newPassword := UpdatePasswordRequest{UserId: testUser.Id, CurrentPassword: *unhashedPassword, UpdatedPassword: updatedPassword, ConfirmedPassword: updatedPassword}
jsonValue, _ := json.Marshal(newPassword)
req, _ := http.NewRequest("PATCH", endpoint.UpdatePassword, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.UpdatePassword(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response UpdatePasswordResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
}
+6 -2
View File
@@ -19,8 +19,12 @@ func TestRefreshTokenWithMock(t *testing.T) {
var serviceUser user.ServiceUser
var hashedPassword string
var err error
unhashed := "9328nr29nudx3292m320!"
hashing := utility.HashMash{Password: unhashed}
unhashed := "A9328nr29nudx3292m320!"
hashing := utility.HashMash{}
if err := hashing.SetPassword(unhashed); err != nil {
assert.NoError(t, err, "Error setting password")
}
if hashedPassword, err = hashing.HashPassword(); err != nil {
assert.NoError(t, err, "Error hashing password: %v", err)
} else {
+13 -8
View File
@@ -85,20 +85,25 @@ func (u *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusBadRequest
resp.Message = "Failure in creating User"
} else {
hashing := utility.HashMash{Password: user.Password}
if hashedPassword, err := hashing.HashPassword(); err != nil {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Password); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
user.Password = hashedPassword
err := u.UserStore.CreateUser(ctx, &user)
if err != nil {
if hashedPassword, err := hashing.HashPassword(); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
resp.Message = "Successful"
statusCode = http.StatusOK
resp.Data = append(resp.Data, RegisterResponseItem{Id: user.Id, PhoneNumber: user.PhoneNumber, Username: user.Username})
user.Password = hashedPassword
err := u.UserStore.CreateUser(ctx, &user)
if err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
resp.Message = "Successful"
statusCode = http.StatusOK
resp.Data = append(resp.Data, RegisterResponseItem{Id: user.Id, PhoneNumber: user.PhoneNumber, Username: user.Username})
}
}
}
}
+30 -20
View File
@@ -70,19 +70,24 @@ func (s *ServiceHandler) Register(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusBadRequest
resp.Message = "Service user already exists"
} else {
hashing := utility.HashMash{Password: req.Passphrase}
if hashedPassword, err := hashing.HashPassword(); err != nil {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Passphrase); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
serviceUser := user.ServiceUser{Username: req.Username, Passphrase: hashedPassword}
if err := s.ServiceStore.Create(ctx, &serviceUser); err != nil {
if hashedPassword, err := hashing.HashPassword(); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Successful"
resp.Data = append(resp.Data, &serviceUser)
serviceUser := user.ServiceUser{Username: req.Username, Passphrase: hashedPassword}
if err := s.ServiceStore.Create(ctx, &serviceUser); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Successful"
resp.Data = append(resp.Data, &serviceUser)
}
}
}
}
@@ -139,23 +144,28 @@ func (s *ServiceHandler) Login(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusNotFound
resp.Message = "Not found"
} else {
hashing := utility.HashMash{Password: req.Passphrase}
if !hashing.CheckPasswordHash(req.Passphrase, serviceUser.Passphrase) {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Passphrase); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = "Not valid"
resp.Message = err.Error()
} else {
var tokGen utility.TokenGenerator
tokGen.SetHourOffset(8)
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*serviceUser); err != nil {
if !hashing.CheckPasswordHash(req.Passphrase, serviceUser.Passphrase) {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
resp.Message = "Not valid"
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, myToken)
resp.Message = "Successful"
var tokGen utility.TokenGenerator
tokGen.SetHourOffset(8)
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*serviceUser); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, myToken)
resp.Message = "Successful"
}
}
}
}
+7 -3
View File
@@ -20,7 +20,7 @@ func TestCreateServiceUserWithMock(t *testing.T) {
mockStore := mock.NewMockServiceUserStore()
handler := NewServiceHandler(cfg, mockStore)
testService := ServiceCreationRequest{Username: "swoon", Passphrase: "ewrewr329n12y3x2!2"}
testService := ServiceCreationRequest{Username: "swoon", Passphrase: "Ewrewr329n12y3x2!2"}
jsonValue, err := json.Marshal(testService)
assert.NoError(t, err, "Error marshaling request")
@@ -39,8 +39,12 @@ func TestLoginServiceUserWithMock(t *testing.T) {
var serviceUser user.ServiceUser
var hashedPassword string
var err error
unhashed := "9328nr29nudx3292m320!"
hashing := utility.HashMash{Password: unhashed}
unhashed := "A9328nr29nudx3292m320!"
hashing := utility.HashMash{}
if err := hashing.SetPassword(unhashed); err != nil {
assert.NoError(t, err, "Error setting password")
}
if hashedPassword, err = hashing.HashPassword(); err != nil {
assert.NoError(t, err, "Error hashing password: %v", err)
} else {
+24 -1
View File
@@ -1,6 +1,8 @@
package handler
import (
"context"
"fmt"
"log"
"os"
"path"
@@ -9,10 +11,12 @@ import (
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/store/mock"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
func GetTestUser() user.User {
return user.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "dfgdffddfd"}
return user.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "Dfgdffd343dfd!"}
}
func GetConfig() *config.Config {
@@ -38,3 +42,22 @@ func GetConfig() *config.Config {
EnableRegistration: config.CheckRegistration(),
}
}
func createUser(ctx context.Context, userStore *mock.MockUserStore) (*user.User, *string, error) {
testUser := GetTestUser()
unhashedPassword := testUser.Password
hashing := utility.HashMash{}
if err := hashing.SetPassword(testUser.Password); err != nil {
return nil, nil, fmt.Errorf("Error setting password: %v", err)
}
hashedPassword, err := hashing.HashPassword()
if err != nil {
return nil, nil, err
}
testUser.Password = hashedPassword
userStore.CreateUser(ctx, &testUser)
return &testUser, &unhashedPassword, nil
}
+47
View File
@@ -0,0 +1,47 @@
package middleware
import (
"context"
"net/http"
"strings"
"git.kundeng.us/phoenix/textsender-auth/internal/services"
)
type contextKey string
const (
UserContextKey contextKey = "user"
)
func AuthMiddleware(authService *services.JWTService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Extract token from "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
token := parts[1]
// Validate token with auth service
user, err := authService.ValidateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), UserContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
+49
View File
@@ -0,0 +1,49 @@
package services
import (
"time"
txtmodels_token "git.kundeng.us/phoenix/textsender-models/tx0/token"
txtmodels_user "git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/golang-jwt/jwt/v5"
)
type JWTService struct {
secretKey []byte
}
func NewJWTService(secretKey string) *JWTService {
return &JWTService{
secretKey: []byte(secretKey),
}
}
func (s *JWTService) ValidateToken(tokenString string) (*txtmodels_user.User, error) {
// TODO: Include more user information in the claims to populate user
claims := &txtmodels_token.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return s.secretKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}
// Check token expiration
if time.Now().After(claims.ExpiresAt.Time) {
return nil, jwt.ErrTokenExpired
}
return &txtmodels_user.User{
Id: claims.UserId,
}, nil
}
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"sync"
"time"
"github.com/google/uuid"
@@ -37,9 +38,11 @@ func (m *MockUserStore) CreateUser(ctx context.Context, user *user.User) error {
}
if _, exists := m.UsersByUsername[user.Username]; exists {
return errors.New("User with email already exists")
return errors.New("User with username already exists")
}
user.Created = time.Now()
m.Users[user.Id] = user
m.UsersByUsername[user.Username] = user
return nil
@@ -112,3 +115,24 @@ func (m *MockUserStore) UserExists(ctx context.Context, username string) (bool,
return exists, nil
}
}
func (m *MockUserStore) UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
}
user, exists := m.Users[id]
if !exists {
return 0, errors.New("User not found")
}
user.Password = password
m.Users[id] = user
m.UsersByUsername[user.Username] = user
return 1, nil
}
+18 -8
View File
@@ -17,6 +17,7 @@ type UserStore interface {
GetUserByUsername(ctx context.Context, username string) (*user.User, error)
GetAllUsers(ctx context.Context) ([]*user.User, error)
UserExists(ctx context.Context, username string) (bool, error)
UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error)
}
type PGUserStore struct {
@@ -31,20 +32,20 @@ func (s *PGUserStore) CreateUser(ctx context.Context, user *user.User) error {
query := `
INSERT INTO users (phone_number, username, password)
VALUES ($1, $2, $3)
RETURNING id, phone_number, username
RETURNING id, phone_number, username, created
`
return s.db.QueryRow(ctx, query, user.PhoneNumber, user.Username, user.Password).Scan(
&user.Id, &user.PhoneNumber, &user.Username,
&user.Id, &user.PhoneNumber, &user.Username, &user.Created,
)
}
func (s *PGUserStore) GetUserByID(ctx context.Context, id uuid.UUID) (*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users WHERE id = $1`
query := `SELECT id, username, password, phone_number, created FROM users WHERE id = $1`
var user user.User
err := s.db.QueryRow(ctx, query, id).Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
&user.Id, &user.Username, &user.Password, &user.PhoneNumber, &user.Created,
)
if err == pgx.ErrNoRows {
@@ -58,11 +59,11 @@ func (s *PGUserStore) GetUserByID(ctx context.Context, id uuid.UUID) (*user.User
}
func (s *PGUserStore) GetUserByUsername(ctx context.Context, username string) (*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users WHERE username = $1`
query := `SELECT id, username, password, phone_number, created FROM users WHERE username = $1`
var user user.User
err := s.db.QueryRow(ctx, query, username).Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
&user.Id, &user.Username, &user.Password, &user.PhoneNumber, &user.Created,
)
if err == pgx.ErrNoRows {
@@ -76,7 +77,7 @@ func (s *PGUserStore) GetUserByUsername(ctx context.Context, username string) (*
}
func (s *PGUserStore) GetAllUsers(ctx context.Context) ([]*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users`
query := `SELECT id, username, password, phone_number, created FROM users`
rows, err := s.db.Query(ctx, query)
if err != nil {
@@ -88,7 +89,7 @@ func (s *PGUserStore) GetAllUsers(ctx context.Context) ([]*user.User, error) {
for rows.Next() {
var user user.User
if err := rows.Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
&user.Id, &user.Username, &user.Password, &user.PhoneNumber, &user.Created,
); err != nil {
return nil, fmt.Errorf("scanning user row: %w", err)
}
@@ -113,3 +114,12 @@ func (s *PGUserStore) UserExists(ctx context.Context, username string) (bool, er
return exists, nil
}
func (s *PGUserStore) UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error) {
query := `UPDATE users SET password = $1 WHERE id = $2`
if affected, err := s.db.Exec(ctx, query, password, id); err != nil {
return 0, err
} else {
return affected.RowsAffected(), nil
}
}
+43 -4
View File
@@ -1,15 +1,19 @@
package utility
import (
"fmt"
"strings"
"unicode"
"golang.org/x/crypto/bcrypt"
)
type HashMash struct {
Password string
password string
}
func (h *HashMash) HashPassword() (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(h.Password), bcrypt.DefaultCost)
bytes, err := bcrypt.GenerateFromPassword([]byte(h.password), bcrypt.DefaultCost)
return string(bytes), err
}
@@ -18,6 +22,41 @@ func (h *HashMash) CheckPasswordHash(password string, hash string) bool {
return err == nil
}
func (h *HashMash) SetPassword(password string) {
h.Password = password
func (h *HashMash) SetPassword(password string) error {
if len(password) < 8 {
return fmt.Errorf("Password length is not enought")
} else if len(password) > 32 {
return fmt.Errorf("Password length is too long")
} else {
specialCharacters := "!@#$%^&*?"
numbers := "0123456789"
if strings.ContainsAny(password, specialCharacters) && strings.ContainsAny(password, numbers) {
var hasAtleastOneUpper, hasAtleastOneLower bool
for _, c := range password {
if unicode.IsUpper(c) {
hasAtleastOneUpper = true
} else if unicode.IsLower(c) {
hasAtleastOneLower = true
}
if hasAtleastOneLower && hasAtleastOneUpper {
break
}
}
if hasAtleastOneUpper && hasAtleastOneLower {
h.password = password
return nil
} else {
if !hasAtleastOneUpper {
return fmt.Errorf("Password requires at least one upper case letter")
} else {
return fmt.Errorf("Password requires at least one lower case letter")
}
}
} else {
return fmt.Errorf("Password should contain special characters and numbers")
}
}
}
+2 -1
View File
@@ -7,7 +7,8 @@ CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL
password TEXT NOT NULL,
created timestamptz DEFAULT now()
);
CREATE TABLE service_users (