Reviewed-on: phoenix/textsender-auth#6
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-09-13 17:47:15 +00:00
committed by phoenix
parent 976a017b4f
commit 3e28db7552
16 changed files with 256 additions and 28 deletions
+4
View File
@@ -51,6 +51,10 @@ func Load() *Config {
}
}
func GetSecretKey() string {
return os.Getenv("SECRET_KEY")
}
func UnpackDBConnString() (connInfo ConnectionInfo) {
username := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
+1
View File
@@ -2,3 +2,4 @@ package endpoint
// Endpoint for registering a user
const Register = "/api/v1/register"
const Login = "/api/v1/login"
+83
View File
@@ -0,0 +1,83 @@
package handler
import (
"fmt"
"net/http"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
type LoginAccount struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Message string `json:"message"`
Data []model.Login `json:"data"`
}
type LoginHandler struct {
UserStore model.UserStore
}
func NewLoginHandler(userStore model.UserStore) *LoginHandler {
return &LoginHandler{UserStore: userStore}
}
func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req LoginAccount
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 LoginResponse
ctx := r.Context()
if exists, err := l.UserStore.UserExists(ctx, req.Username); err != nil {
fmt.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if !exists {
statusCode = http.StatusBadRequest
resp.Message = "Failure in user check"
} else {
if user, err := l.UserStore.GetUserByUsername(ctx, req.Username); err != nil {
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 token, 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, *token)
resp.Message = "Successful"
}
} else {
statusCode = http.StatusNotFound
resp.Message = "User not found"
}
}
}
}
RespondWithJson(w, statusCode, &resp)
}
+50
View File
@@ -0,0 +1,50 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
func TestLogin(t *testing.T) {
mockstore := NewMockUserStore()
handler := NewLoginHandler(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)
loginUser := LoginAccount{Username: testUser.Username, Password: unhashedPassword}
jsonValue, _ := json.Marshal(loginUser)
req, _ := http.NewRequest("POST", endpoint.Login, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.Login(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response LoginResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response.Data, "An access token should have been returned")
}
+8 -23
View File
@@ -1,11 +1,9 @@
package handler
import "net/http"
import "encoding/json"
import "fmt"
import (
"fmt"
"github.com/google/uuid"
"net/http"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
@@ -42,7 +40,8 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
return
}
user, err := extractUserFromReq(r)
var req RegisterUser
err := ExtractFromRequest(r, &req)
if err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
return
@@ -50,6 +49,8 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
user := model.User{Username: req.Username, Password: req.Password, PhoneNumber: req.PhoneNumber}
var statusCode int
resp := RegisterResponse{}
@@ -69,7 +70,7 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusBadRequest
resp.Message = "Failure in creating User"
} else {
hashing := utility.HashMash{user.Password}
hashing := utility.HashMash{Password: user.Password}
hashedPassword, err := hashing.HashPassword()
if err != nil {
statusCode = http.StatusInternalServerError
@@ -88,21 +89,5 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
}
}
respondWithJson(w, statusCode, &resp)
}
func extractUserFromReq(r *http.Request) (user model.User, myError error) {
var usr RegisterUser
err := json.NewDecoder(r.Body).Decode(&usr)
if err != nil {
return user, err
}
return model.User{PhoneNumber: usr.PhoneNumber, Username: usr.Username, Password: usr.Password}, nil
}
func respondWithJson(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
RespondWithJson(w, statusCode, &resp)
}
+3 -4
View File
@@ -8,18 +8,17 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-auth/internal/db"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
"github.com/stretchr/testify/assert"
)
func TestCreateUserWithMock(t *testing.T) {
mockstore := NewMockUserStore()
handler := NewUserHandler(mockstore)
testUser := model.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "dfgdffddfd"}
testUser := GetTestUser()
jsonValue, _ := json.Marshal(testUser)
req, _ := http.NewRequest("POST", endpoint.Register, strings.NewReader(string(jsonValue)))
+21
View File
@@ -0,0 +1,21 @@
package handler
import (
"encoding/json"
"net/http"
)
func ExtractFromRequest(r *http.Request, reqItem interface{}) error {
err := json.NewDecoder(r.Body).Decode(&reqItem)
if err != nil {
return err
} else {
return nil
}
}
func RespondWithJson(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
+9
View File
@@ -0,0 +1,9 @@
package handler
import (
"git.kundeng.us/phoenix/textsender-auth/internal/model"
)
func GetTestUser() model.User {
return model.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "dfgdffddfd"}
}
+15
View File
@@ -0,0 +1,15 @@
package model
import (
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserId string `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
type Login struct {
AccessToken string `json:"access_token"`
}
-1
View File
@@ -1,6 +1,5 @@
package utility
import (
"golang.org/x/crypto/bcrypt"
)
+40
View File
@@ -0,0 +1,40 @@
package utility
import (
"time"
"github.com/golang-jwt/jwt/v5"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
)
type TokenGenerator struct {
SecretKey []byte
}
func (t *TokenGenerator) SetSecretKey(secretKey string) {
t.SecretKey = []byte(secretKey)
}
func (t *TokenGenerator) GenerateToken(user model.User) (*model.Login, error) {
claims := t.generateClaims(user, "regular")
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
if tokenString, err := token.SignedString(t.SecretKey); err != nil {
return nil, err
} else {
return &model.Login{AccessToken: tokenString}, nil
}
}
func (t *TokenGenerator) generateClaims(user model.User, role string) model.Claims {
return model.Claims{
UserId: user.Id.String(),
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "textsender-auth",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
}