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
+1
View File
@@ -1,3 +1,4 @@
SECRET_KEY=XY0aIEg8qe0svz4IXUSIvxPdbDHIwoda
DB_NAME=textsender_auth_db
DB_USER=textsender_auth
DB_PASSWORD=yEDjWZCH2vdctjn!
+16
View File
@@ -51,6 +51,22 @@ jobs:
with:
go-version: '1.25.1'
- name: Install PostgreSQL client
run: sudo apt update && sudo apt-get install -y postgresql-client
- name: Wait for PostgreSQL to be ready
run: |
for i in {1..30}; do
if pg_isready -h postgres -p 5432; then
echo "PostgreSQL is ready"
exit 0
fi
echo "Waiting for PostgreSQL... Attempt $i"
sleep 2
done
echo "PostgreSQL did not start in time"
exit 1
- name: Run tests
env:
DB_NAME: ${{ secrets.DB_TEST_NAME }}
+2
View File
@@ -46,6 +46,7 @@ func main() {
// Services
userStore := model.NewUserStore(db.Pool)
userHandler := handler.NewUserHandler(userStore)
loginHandler := handler.NewLoginHandler(userStore)
router := chi.NewRouter()
@@ -55,6 +56,7 @@ func main() {
router.Use(mdleware.JSONContentType)
router.Post(endpoint.Register, userHandler.Register)
router.Post(endpoint.Login, loginHandler.Login)
// Start server
server := &http.Server{
+1
View File
@@ -4,6 +4,7 @@ go 1.25.1
require (
github.com/go-chi/chi/v5 v5.2.3
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
+2
View File
@@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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=
+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()),
},
}
}