Repository: Neil-urk12/Surplus-Sales-Management-System
Pull request #67: OOP-65 Implement login with username feature and add page loader to login page
Author: login: Neil-urk12
Created: 2025-05-10T16:24:05Z | Merged: 2025-05-10T22:59:47Z

https://oopfinalscs22agroup3.atlassian.net/browse/OOP-65




Diff:
diff --git a/Backend/internal/handlers/user_handlers.go b/Backend/internal/handlers/user_handlers.go
index 23007ea..631f672 100644
--- a/Backend/internal/handlers/user_handlers.go
+++ b/Backend/internal/handlers/user_handlers.go
@@ -10,6 +10,7 @@ import (
 	"github.com/gofiber/fiber/v2"
 	"github.com/golang-jwt/jwt/v5"
 	"github.com/google/uuid"
+	"golang.org/x/crypto/bcrypt"
 )
 
 const (
@@ -50,14 +51,19 @@ type UserRepository interface {
 	Create(user *models.User) error
 	GetByID(id string) (*models.User, error)
 	GetByEmail(email string) (*models.User, error)
+	GetByUsername(username string) (*models.User, error)
 	Update(user *models.User) error
 	UpdatePassword(userID string, newPassword string) error
 	Delete(id string) error
 	GetAll() ([]*models.User, error)
-	VerifyPassword(email, password string) (*models.User, error)
+	VerifyPassword(identifier, password string) (*models.User, error)
 	ActivateUser(id string) error
 	DeactivateUser(id string) error
 	EmailExists(email string) (bool, error)
+	UsernameExists(username string) (bool, error)
+	// FindByEmailOrUsernameConstantTime attempts to find a user by email or username
+	// in a way that takes a consistent amount of time.
+	FindByEmailOrUsernameConstantTime(identifier string) (*models.User, error)
 }
 
 // UserHandler handles HTTP requests related to users
@@ -110,6 +116,7 @@ func generateToken() (string, error) {
 // @Router /users/register [post]
 func (h *UserHandler) Register(c *fiber.Ctx) error {
 	var input struct {
+		Username string `json:"username"`
 		Name     string `json:"fullName"`
 		Email    string `json:"email"`
 		Password string `json:"password"`
@@ -124,9 +131,9 @@ func (h *UserHandler) Register(c *fiber.Ctx) error {
 	}
 
 	// Validate input
-	if input.Name == "" || input.Email == "" || input.Password == "" {
+	if input.Username == "" || input.Name == "" || input.Email == "" || input.Password == "" {
 		return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
-			Error:      "Name, email, and password are required",
+			Error:      "Username, name, email, and password are required",
 			StatusCode: fiber.StatusBadRequest,
 		})
 	}
@@ -159,9 +166,27 @@ func (h *UserHandler) Register(c *fiber.Ctx) error {
 	}
 
 	// Create user
+	// Check if username already exists
+	exists, err = h.userRepo.UsernameExists(input.Username)
+	if err != nil {
+		log.Printf("Error checking username existence: %v", err)
+		return c.Status(fiber.StatusInternalServerError).JSON(ErrorResponse{
+			Error:      "Internal server error",
+			StatusCode: fiber.StatusInternalServerError,
+		})
+	}
+
+	if exists {
+		return c.Status(fiber.StatusConflict).JSON(ErrorResponse{
+			Error:      "Username already in use",
+			StatusCode: fiber.StatusConflict,
+		})
+	}
+
 	user := &models.User{
 		Id:       uuid.New().String(),
-		FullName: input.Name, // Use FullName from input
+		Username: input.Username,
+		FullName: input.Name,
 		Email:    input.Email,
 		Password: input.Password, // Will be hashed in the repository
 		Role:     input.Role,
@@ -200,7 +225,7 @@ func (h *UserHandler) Register(c *fiber.Ctx) error {
 // @Router /users/login [post]
 func (h *UserHandler) Login(c *fiber.Ctx) error {
 	var input struct {
-		Email    string `json:"email"`
+		Username string `json:"username"`
 		Password string `json:"password"`
 	}
 
@@ -212,18 +237,17 @@ func (h *UserHandler) Login(c *fiber.Ctx) error {
 	}
 
 	// Validate input
-	if input.Email == "" || input.Password == "" {
+	if input.Username == "" || input.Password == "" {
 		return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
-			Error:      "Email and password are required",
+			Error:      "Username or Email and password are required",
 			StatusCode: fiber.StatusBadRequest,
 		})
 	}
 
-	// First, get the user by email to check if they exist and if they're active
-	user, err := h.userRepo.GetByEmail(input.Email)
+	// Find user by email or username using the constant time function
+	user, err := h.userRepo.FindByEmailOrUsernameConstantTime(input.Username)
 	if err != nil {
-		log.Printf("Login failed - user not found for email %s: %v", input.Email, err)
-		// Return a generic error message to avoid revealing which part failed (email or password)
+		log.Printf("Login failed - user not found for identifier %s: %v", input.Username, err)
 		return c.Status(fiber.StatusUnauthorized).JSON(ErrorResponse{
 			Error:      "Invalid credentials",
 			StatusCode: fiber.StatusUnauthorized,
@@ -232,17 +256,19 @@ func (h *UserHandler) Login(c *fiber.Ctx) error {
 
 	// Check if user is active before verifying password
 	if !user.IsActive {
-		log.Printf("Login attempt for inactive user: %s", input.Email)
+		log.Printf("Login attempt for inactive user: %s", input.Username)
 		return c.Status(fiber.StatusForbidden).JSON(ErrorResponse{
 			Error:      "Account is inactive",
 			StatusCode: fiber.StatusForbidden,
 		})
 	}
 
-	// Verify password
-	user, err = h.userRepo.VerifyPassword(input.Email, input.Password)
+	// Verify password using the identifier (email or username)
+	// The FindByEmailOrUsernameConstantTime already returned the user if found,
+	// so we just need to verify the password against the retrieved user.
+	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password))
 	if err != nil {
-		log.Printf("Login failed - invalid password for email %s: %v", input.Email, err)
+		log.Printf("Login failed - invalid password for identifier %s: %v", input.Username, err)
 		// Return a generic error message to avoid revealing which part failed
 		return c.Status(fiber.StatusUnauthorized).JSON(ErrorResponse{
 			Error:      "Invalid credentials",
diff --git a/Backend/internal/handlers/user_handlers_test.go b/Backend/internal/handlers/user_handlers_test.go
index 77e8035..d0ee556 100644
--- a/Backend/internal/handlers/user_handlers_test.go
+++ b/Backend/internal/handlers/user_handlers_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
+	"golang.org/x/crypto/bcrypt"
 )
 
 // MockUserRepository is a mock implementation of the UserRepository
@@ -65,8 +66,21 @@ func (m *MockUserRepository) GetAll() ([]*models.User, error) {
 	return args.Get(0).([]*models.User), args.Error(1)
 }
 
-func (m *MockUserRepository) VerifyPassword(email, password string) (*models.User, error) {
-	args := m.Called(email, password)
+func (m *MockUserRepository) GetByUsername(username string) (*models.User, error) {
+	args := m.Called(username)
+	if args.Get(0) == nil {
+		return nil, args.Error(1)
+	}
+	return args.Get(0).(*models.User), args.Error(1)
+}
+
+func (m *MockUserRepository) UsernameExists(username string) (bool, error) {
+	args := m.Called(username)
+	return args.Bool(0), args.Error(1)
+}
+
+func (m *MockUserRepository) VerifyPassword(identifier, password string) (*models.User, error) {
+	args := m.Called(identifier, password)
 	if args.Get(0) == nil {
 		return nil, args.Error(1)
 	}
@@ -88,6 +102,14 @@ func (m *MockUserRepository) EmailExists(email string) (bool, error) {
 	return args.Bool(0), args.Error(1)
 }
 
+func (m *MockUserRepository) FindByEmailOrUsernameConstantTime(identifier string) (*models.User, error) {
+	args := m.Called(identifier)
+	if args.Get(0) == nil {
+		return nil, args.Error(1)
+	}
+	return args.Get(0).(*models.User), args.Error(1)
+}
+
 // Helper function to create a test app and handler
 func setupTest() (*fiber.App, *UserHandler, *MockUserRepository) {
 	app := fiber.New()
@@ -96,13 +118,15 @@ func setupTest() (*fiber.App, *UserHandler, *MockUserRepository) {
 	return app, handler, mockRepo
 }
 
-// Helper function to create a test user
-func createTestUser() *models.User {
+// Helper function to create a test user with a hashed password
+func createTestUser(password string) *models.User {
+	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 	return &models.User{
 		Id:        uuid.New().String(),
+		Username:  "testuser",
 		FullName:  "Test User",
 		Email:     "test@example.com",
-		Password:  "hashed_password",
+		Password:  string(hashedPassword), // Store the hashed password
 		Role:      "user",
 		CreatedAt: time.Now(),
 		UpdatedAt: time.Now(),
@@ -119,16 +143,21 @@ func TestUserHandler_Register_Success(t *testing.T) {
 
 	// Setup expectations
 	mockRepo.On("EmailExists", "test@example.com").Return(false, nil)
+	mockRepo.On("UsernameExists", "testuser").Return(false, nil)
 	mockRepo.On("Create", mock.AnythingOfType("*models.User")).Return(nil)
 
 	// Create request body
 	reqBody := map[string]string{
+		"username": "testuser",
 		"fullName": "Test User",
 		"email":    "test@example.com",
 		"password": "password123",
 		"role":     "user",
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPost, "/register", bytes.NewReader(jsonBody))
@@ -161,15 +190,22 @@ func TestUserHandler_Register_EmailExists(t *testing.T) {
 	app.Post("/register", handler.Register)
 
 	// Setup expectations
+	// The handler will first validate input, then check username, then email.
+	// For this test, username should not exist, but email should.
 	mockRepo.On("EmailExists", "test@example.com").Return(true, nil)
 
 	// Create request body
 	reqBody := map[string]string{
+		"username": "testuser_email_exists", // Added
 		"fullName": "Test User",
 		"email":    "test@example.com",
 		"password": "password123",
+		"role":     "user", // Added
+	}
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
 	}
-	jsonBody, _ := json.Marshal(reqBody)
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPost, "/register", bytes.NewReader(jsonBody))
@@ -193,7 +229,7 @@ func TestUserHandler_Register_EmailExists(t *testing.T) {
 	mockRepo.AssertExpectations(t)
 }
 
-func TestUserHandler_Login_Success(t *testing.T) {
+func TestUserHandler_Login_Success_WithEmail(t *testing.T) {
 	// Setup
 	app, handler, mockRepo := setupTest()
 
@@ -201,18 +237,66 @@ func TestUserHandler_Login_Success(t *testing.T) {
 	app.Post("/login", handler.Login)
 
 	// Create test user
-	user := createTestUser()
+	user := createTestUser("password123")
 
 	// Setup expectations
-	mockRepo.On("GetByEmail", "test@example.com").Return(user, nil)
-	mockRepo.On("VerifyPassword", "test@example.com", "password123").Return(user, nil)
+	mockRepo.On("FindByEmailOrUsernameConstantTime", "test@example.com").Return(user, nil)
 
 	// Create request body
 	reqBody := map[string]string{
-		"email":    "test@example.com",
+		"username": "test@example.com",
+		"password": "password123",
+	}
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
+
+	// Create request
+	req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(jsonBody))
+	req.Header.Set("Content-Type", "application/json")
+
+	// Perform request
+	resp, err := app.Test(req)
+
+	// Assert
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+	// Verify response body
+	var result map[string]interface{}
+	error := json.NewDecoder(resp.Body).Decode(&result)
+	assert.NoError(t, error)
+
+	assert.Equal(t, "Login successful", result["message"])
+	assert.NotNil(t, result["user"])
+
+	// Verify expectations
+	mockRepo.AssertExpectations(t)
+}
+
+func TestUserHandler_Login_Success_WithUsername(t *testing.T) {
+	// Setup
+	app, handler, mockRepo := setupTest()
+
+	// Setup route
+	app.Post("/login", handler.Login)
+
+	// Create test user
+	// Setup expectations
+	// Create a test user with the correct password and active status
+	user := createTestUser("password123")
+	mockRepo.On("FindByEmailOrUsernameConstantTime", "testuser").Return(user, nil)
+
+	// Create request body
+	reqBody := map[string]string{
+		"username": "testuser",
 		"password": "password123",
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(jsonBody))
@@ -245,18 +329,65 @@ func TestUserHandler_Login_InvalidCredentials(t *testing.T) {
 	app.Post("/login", handler.Login)
 
 	// Create test user
-	user := createTestUser()
+	user := createTestUser("password123")
 
 	// Setup expectations
-	mockRepo.On("GetByEmail", "test@example.com").Return(user, nil)
-	mockRepo.On("VerifyPassword", "test@example.com", "wrong_password").Return(nil, errors.New("invalid password"))
+	mockRepo.On("FindByEmailOrUsernameConstantTime", "test@example.com").Return(user, nil)
 
 	// Create request body
 	reqBody := map[string]string{
-		"email":    "test@example.com",
+		"username": "test@example.com",
 		"password": "wrong_password",
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
+
+	// Create request
+	req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(jsonBody))
+	req.Header.Set("Content-Type", "application/json")
+
+	// Perform request
+	resp, err := app.Test(req)
+
+	// Assert
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+
+	// Verify response body
+	var result map[string]interface{}
+	err = json.NewDecoder(resp.Body).Decode(&result)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "Invalid credentials", result["error"])
+
+	// Verify expectations
+	mockRepo.AssertExpectations(t)
+}
+
+func TestUserHandler_Login_InvalidCredentials_WithUsername(t *testing.T) {
+	// Setup
+	app, handler, mockRepo := setupTest()
+
+	// Setup route
+	app.Post("/login", handler.Login)
+
+	// Create test user
+	user := createTestUser("password123") // User exists
+
+	// Setup expectations
+	mockRepo.On("FindByEmailOrUsernameConstantTime", "testuser").Return(user, nil)
+
+	// Create request body
+	reqBody := map[string]string{
+		"username": "testuser",
+		"password": "wrong_password",
+	}
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(jsonBody))
@@ -288,18 +419,70 @@ func TestUserHandler_Login_InactiveUser(t *testing.T) {
 	app.Post("/login", handler.Login)
 
 	// Create inactive test user
-	inactiveUser := createTestUser()
+	inactiveUser := createTestUser("password123")
 	inactiveUser.IsActive = false
 
 	// Setup expectations
-	mockRepo.On("GetByEmail", "test@example.com").Return(inactiveUser, nil)
+	mockRepo.On("FindByEmailOrUsernameConstantTime", "test@example.com").Return(inactiveUser, nil)
 
 	// Create request body
 	reqBody := map[string]string{
-		"email":    "test@example.com",
+		"username": "test@example.com",
 		"password": "password123",
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
+
+	// Create request
+	req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(jsonBody))
+	req.Header.Set("Content-Type", "application/json")
+
+	// Perform request
+	resp, err := app.Test(req)
+
+	// Assert
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
+
+	// Verify response body
+	var result map[string]interface{}
+	err = json.NewDecoder(resp.Body).Decode(&result)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "Account is inactive", result["error"])
+
+	// Verify expectations
+	mockRepo.AssertExpectations(t)
+}
+
+func TestUserHandler_Login_InactiveUser_WithUsername(t *testing.T) {
+	// Setup
+	app, handler, mockRepo := setupTest()
+
+	// Setup route
+	app.Post("/login", handler.Login)
+
+	// Create inactive test user with a specific username
+	inactiveUser := createTestUser("password123")
+	inactiveUser.IsActive = false
+	inactiveUser.Username = "inactive_username" // Ensure this user has the target username
+	// inactiveUser.Email = "inactive_user_specific_email@example.com" // Optional: make email distinct
+
+	// Setup expectations
+	mockRepo.On("FindByEmailOrUsernameConstantTime", "inactive_username").Return(inactiveUser, nil)
+	// VerifyPassword should NOT be called
+
+	// Create request body
+	reqBody := map[string]string{
+		"username": "inactive_username",
+		"password": "password123",
+	}
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(jsonBody))
@@ -387,7 +570,7 @@ func TestUserHandler_GetUser(t *testing.T) {
 	app.Get("/users/:id", handler.GetUser)
 
 	// Create test user
-	user := createTestUser()
+	user := createTestUser("password123")
 
 	// Setup expectations
 	mockRepo.On("GetByID", user.Id).Return(user, nil)
@@ -458,7 +641,7 @@ func TestUserHandler_UpdateUser(t *testing.T) {
 	app.Put("/users/:id", handler.UpdateUser)
 
 	// Create test user
-	user := createTestUser()
+	user := createTestUser("password123")
 
 	// Setup expectations
 	mockRepo.On("GetByID", user.Id).Return(user, nil)
@@ -475,7 +658,10 @@ func TestUserHandler_UpdateUser(t *testing.T) {
 		"role":     "staff",
 		"isActive": false,
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPut, "/users/"+user.Id, bytes.NewReader(jsonBody))
@@ -518,7 +704,7 @@ func TestUserHandler_UpdateUser_InactiveUser(t *testing.T) {
 	app.Put("/users/:id", handler.UpdateUser)
 
 	// Create inactive test user
-	inactiveUser := createTestUser()
+	inactiveUser := createTestUser("password123")
 	inactiveUser.IsActive = false
 
 	// Setup expectations
@@ -544,7 +730,10 @@ func TestUserHandler_UpdateUser_InactiveUser(t *testing.T) {
 		"role":     "user",
 		"isActive": true, // Try to activate the user
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPut, "/users/"+inactiveUser.Id, bytes.NewReader(jsonBody))
@@ -673,7 +862,7 @@ func TestUserHandler_UpdatePassword(t *testing.T) {
 	app, handler, mockRepo := setupTest()
 
 	// Create test user
-	user := createTestUser()
+	user := createTestUser("password123")
 
 	// Setup middleware to simulate authenticated user
 	app.Use(func(c *fiber.Ctx) error {
@@ -698,7 +887,10 @@ func TestUserHandler_UpdatePassword(t *testing.T) {
 		"current_password": "current_password",
 		"new_password":     "new_password",
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPut, "/users/"+user.Id+"/password", bytes.NewReader(jsonBody))
@@ -727,7 +919,7 @@ func TestUserHandler_UpdatePassword_InactiveUser(t *testing.T) {
 	app, handler, mockRepo := setupTest()
 
 	// Create inactive test user
-	inactiveUser := createTestUser()
+	inactiveUser := createTestUser("password123")
 	inactiveUser.IsActive = false
 
 	// Setup middleware to simulate authenticated user
@@ -761,7 +953,10 @@ func TestUserHandler_UpdatePassword_InactiveUser(t *testing.T) {
 		"current_password": "current_password",
 		"new_password":     "new_password",
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPut, "/users/"+inactiveUser.Id+"/password", bytes.NewReader(jsonBody))
@@ -779,7 +974,7 @@ func TestUserHandler_UpdatePassword_InactiveUser(t *testing.T) {
 	err = json.NewDecoder(resp.Body).Decode(&result)
 	assert.NoError(t, err)
 
-	assert.Equal(t, "Account is inactive", result["error"]) // Expect inactive account error
+	assert.Equal(t, "Account is inactive", result["error"])
 
 	// Verify expectations
 	mockRepo.AssertExpectations(t)
@@ -790,7 +985,7 @@ func TestUserHandler_UpdatePassword_IncorrectCurrentPassword(t *testing.T) {
 	app, handler, mockRepo := setupTest()
 
 	// Create test user (defaults to active)
-	user := createTestUser()
+	user := createTestUser("password123")
 
 	// Setup middleware to simulate authenticated user
 	app.Use(func(c *fiber.Ctx) error {
@@ -816,7 +1011,10 @@ func TestUserHandler_UpdatePassword_IncorrectCurrentPassword(t *testing.T) {
 		"current_password": "wrong_password",
 		"new_password":     "new_password",
 	}
-	jsonBody, _ := json.Marshal(reqBody)
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		t.Fatalf("Failed to marshal request body: %v", err)
+	}
 
 	// Create request
 	req := httptest.NewRequest(http.MethodPut, "/users/"+user.Id+"/password", bytes.NewReader(jsonBody))
diff --git a/Backend/internal/models/models.go b/Backend/internal/models/models.go
index 2a2317a..1c8605d 100644
--- a/Backend/internal/models/models.go
+++ b/Backend/internal/models/models.go
@@ -6,6 +6,7 @@ import (
 
 type User struct {
 	Id        string    `json:"id"`
+	Username  string    `json:"username"`
 	FullName  string    `json:"fullName"`
 	Email     string    `json:"email"`
 	Password  string    `json:"-"`
diff --git a/Backend/internal/models/user_api_models.go b/Backend/internal/models/user_api_models.go
index 0b6c45b..102a246 100644
--- a/Backend/internal/models/user_api_models.go
+++ b/Backend/internal/models/user_api_models.go
@@ -5,6 +5,7 @@ import "time"
 // UserCreateRequest defines the shape of the request body for user registration and admin/staff creation.
 // It's used when a new user is being added to the system.
 type UserCreateRequest struct {
+	Username string `json:"username" example:"johndoe"`
 	FullName string `json:"fullName" example:"John Doe"`
 	Email    string `json:"email" example:"john.doe@example.com"`
 	Password string `json:"password" example:"securepassword123"`
@@ -13,8 +14,9 @@ type UserCreateRequest struct {
 
 // UserLoginRequest defines the shape of the request body for user login.
 // It contains the credentials required for a user to authenticate.
+// Username field can be either username or email
 type UserLoginRequest struct {
-	Email    string `json:"email" example:"john.doe@example.com"`
+	Username string `json:"username" example:"johndoe or john.doe@example.com"`
 	Password string `json:"password" example:"securepassword123"`
 }
 
@@ -34,6 +36,7 @@ type UserResponse struct {
 // Fields are optional (omitempty), and a pointer is used for boolean 'IsActive'
 // to differentiate between explicitly setting it to false and not providing the field.
 type UserUpdateRequest struct {
+	Username string `json:"username,omitempty" example:"johndoe"`
 	FullName string `json:"fullName,omitempty" example:"Johnathan Doe"`
 	Email    string `json:"email,omitempty" example:"johnathan.doe@example.com"`
 	Role     string `json:"role,omitempty" example:"admin" enums:"staff,admin"`
diff --git a/Backend/internal/repositories/user_repository.go b/Backend/internal/repositories/user_repository.go
index 95e70bf..5fef2f7 100644
--- a/Backend/internal/repositories/user_repository.go
+++ b/Backend/internal/repositories/user_repository.go
@@ -51,12 +51,13 @@ func (r *UserRepository) Create(user *models.User) error {
 
 	// Insert the user into the database
 	query := `
-		INSERT INTO users (id, full_name, email, password_hash, role, created_at, updated_at, is_active)
-		VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+		INSERT INTO users (id, username, full_name, email, password_hash, role, created_at, updated_at, is_active)
+		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
 	`
 	_, err = r.dbClient.DB.Exec(
 		query,
 		user.Id,
+		user.Username,
 		user.FullName,
 		user.Email,
 		string(hashedPassword),
@@ -76,7 +77,7 @@ func (r *UserRepository) Create(user *models.User) error {
 // GetByID retrieves a user by their ID
 func (r *UserRepository) GetByID(id string) (*models.User, error) {
 	query := `
-		SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active
+		SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active
 		FROM users
 		WHERE id = ?
 	`
@@ -85,6 +86,7 @@ func (r *UserRepository) GetByID(id string) (*models.User, error) {
 	var user models.User
 	err := row.Scan(
 		&user.Id,
+		&user.Username,
 		&user.FullName,
 		&user.Email,
 		&user.Password,
@@ -107,7 +109,7 @@ func (r *UserRepository) GetByID(id string) (*models.User, error) {
 // GetByEmail retrieves a user by their email address
 func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
 	query := `
-		SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active
+		SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active
 		FROM users
 		WHERE email = ?
 	`
@@ -116,6 +118,7 @@ func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
 	var user models.User
 	err := row.Scan(
 		&user.Id,
+		&user.Username,
 		&user.FullName,
 		&user.Email,
 		&user.Password,
@@ -142,11 +145,12 @@ func (r *UserRepository) Update(user *models.User) error {
 
 	query := `
 		UPDATE users
-		SET full_name = ?, email = ?, role = ?, updated_at = ?, is_active = ?
+		SET username = ?, full_name = ?, email = ?, role = ?, updated_at = ?, is_active = ?
 		WHERE id = ?
 	`
 	result, err := r.dbClient.DB.Exec(
 		query,
+		user.Username,
 		user.FullName,
 		user.Email,
 		user.Role,
@@ -233,7 +237,7 @@ func (r *UserRepository) Delete(id string) error {
 // GetAll retrieves all users from the database
 func (r *UserRepository) GetAll() ([]*models.User, error) {
 	query := `
-		SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active
+		SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active
 		FROM users
 		ORDER BY created_at DESC
 	`
@@ -248,6 +252,7 @@ func (r *UserRepository) GetAll() ([]*models.User, error) {
 		var user models.User
 		err := rows.Scan(
 			&user.Id,
+			&user.Username,
 			&user.FullName,
 			&user.Email,
 			&user.Password,
@@ -269,10 +274,61 @@ func (r *UserRepository) GetAll() ([]*models.User, error) {
 	return users, nil
 }
 
+// GetByUsername retrieves a user by their username
+func (r *UserRepository) GetByUsername(username string) (*models.User, error) {
+	query := `
+		SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active
+		FROM users
+		WHERE username = ?
+	`
+	row := r.dbClient.DB.QueryRow(query, username)
+
+	var user models.User
+	err := row.Scan(
+		&user.Id,
+		&user.Username,
+		&user.FullName,
+		&user.Email,
+		&user.Password,
+		&user.Role,
+		&user.CreatedAt,
+		&user.UpdatedAt,
+		&user.IsActive,
+	)
+
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, fmt.Errorf("user not found: %w", err)
+		}
+		return nil, fmt.Errorf("failed to get user by username: %w", err)
+	}
+
+	return &user, nil
+}
+
+// UsernameExists checks if a username already exists in the database
+func (r *UserRepository) UsernameExists(username string) (bool, error) {
+	var exists bool
+	query := `SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)`
+	err := r.dbClient.DB.QueryRow(query, username).Scan(&exists)
+	if err != nil {
+		return false, fmt.Errorf("failed to check if username exists: %w", err)
+	}
+	return exists, nil
+}
+
 // VerifyPassword checks if the provided password matches the stored hash
 // Note: This method does not check if the user is active - that's the responsibility of the handler
-func (r *UserRepository) VerifyPassword(email, password string) (*models.User, error) {
-	user, err := r.GetByEmail(email)
+func (r *UserRepository) VerifyPassword(identifier, password string) (*models.User, error) {
+	// Try to find user by email first, then by username
+	user, err := r.GetByEmail(identifier)
+	if err != nil {
+		// If not found by email, try username
+		user, err = r.GetByUsername(identifier)
+		if err != nil {
+			return nil, err
+		}
+	}
 	if err != nil {
 		return nil, err
 	}
@@ -318,3 +374,42 @@ func (r *UserRepository) EmailExists(email string) (bool, error) {
 	}
 	return exists, nil
 }
+
+// FindByEmailOrUsernameConstantTime attempts to find a user by email or username
+// in a way that takes a consistent amount of time.
+func (r *UserRepository) FindByEmailOrUsernameConstantTime(identifier string) (*models.User, error) {
+	// Always query for both email and username to maintain consistent timing.
+	// We use UNION ALL to ensure both parts of the query are executed.
+	query := `
+		SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active
+		FROM users
+		WHERE email = ? OR username = ?
+	`
+
+	var user models.User
+	err := r.dbClient.DB.QueryRow(query, identifier, identifier).Scan(
+		&user.Id,
+		&user.Username,
+		&user.FullName,
+		&user.Email,
+		&user.Password,
+		&user.Role,
+		&user.CreatedAt,
+		&user.UpdatedAt,
+		&user.IsActive,
+	)
+
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			// User not found by either email or username.
+			// To maintain constant time, we might add a small delay here in a real-world scenario,
+			// but for this example, the constant query execution is sufficient.
+			return nil, fmt.Errorf("user not found")
+		}
+		// Other database errors
+		return nil, fmt.Errorf("failed to find user by email or username: %w", err)
+	}
+
+	// If a user is found, return it.
+	return &user, nil
+}
diff --git a/Backend/internal/repositories/user_repository_test.go b/Backend/internal/repositories/user_repository_test.go
index 0ef6e4e..a49db9b 100644
--- a/Backend/internal/repositories/user_repository_test.go
+++ b/Backend/internal/repositories/user_repository_test.go
@@ -34,9 +34,10 @@ func TestUserRepository_Create(t *testing.T) {
 	}
 
 	// Set up the expected SQL query and result
-	mock.ExpectExec("INSERT INTO users (id, full_name, email, password_hash, role, created_at, updated_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").
+	mock.ExpectExec("INSERT INTO users (id, username, full_name, email, password_hash, role, created_at, updated_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").
 		WithArgs(
 			user.Id,
+			user.Username,
 			user.FullName,
 			user.Email,
 			sqlmock.AnyArg(), // Password will be hashed
@@ -87,10 +88,10 @@ func TestUserRepository_GetByID(t *testing.T) {
 	}
 
 	// Set up the expected SQL query and result
-	rows := sqlmock.NewRows([]string{"id", "full_name", "email", "password", "role", "created_at", "updated_at", "is_active"}).
-		AddRow(expectedUser.Id, expectedUser.FullName, expectedUser.Email, expectedUser.Password, expectedUser.Role, expectedUser.CreatedAt, expectedUser.UpdatedAt, expectedUser.IsActive)
+	rows := sqlmock.NewRows([]string{"id", "username", "full_name", "email", "password_hash", "role", "created_at", "updated_at", "is_active"}).
+		AddRow(expectedUser.Id, expectedUser.Username, expectedUser.FullName, expectedUser.Email, expectedUser.Password, expectedUser.Role, expectedUser.CreatedAt, expectedUser.UpdatedAt, expectedUser.IsActive)
 
-	mock.ExpectQuery("SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE id = ?").
+	mock.ExpectQuery("SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE id = ?").
 		WithArgs(userID).
 		WillReturnRows(rows)
 
@@ -135,10 +136,10 @@ func TestUserRepository_GetByEmail(t *testing.T) {
 	}
 
 	// Set up the expected SQL query and result
-	rows := sqlmock.NewRows([]string{"id", "full_name", "email", "password", "role", "created_at", "updated_at", "is_active"}).
-		AddRow(expectedUser.Id, expectedUser.FullName, expectedUser.Email, expectedUser.Password, expectedUser.Role, expectedUser.CreatedAt, expectedUser.UpdatedAt, expectedUser.IsActive)
+	rows := sqlmock.NewRows([]string{"id", "username", "full_name", "email", "password_hash", "role", "created_at", "updated_at", "is_active"}).
+		AddRow(expectedUser.Id, expectedUser.Username, expectedUser.FullName, expectedUser.Email, expectedUser.Password, expectedUser.Role, expectedUser.CreatedAt, expectedUser.UpdatedAt, expectedUser.IsActive)
 
-	mock.ExpectQuery("SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE email = ?").
+	mock.ExpectQuery("SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE email = ?").
 		WithArgs(email).
 		WillReturnRows(rows)
 
@@ -178,8 +179,8 @@ func TestUserRepository_Update(t *testing.T) {
 	}
 
 	// Set up the expected SQL query and result
-	mock.ExpectExec("UPDATE users SET full_name = ?, email = ?, role = ?, updated_at = ?, is_active = ? WHERE id = ?").
-		WithArgs(user.FullName, user.Email, user.Role, sqlmock.AnyArg(), user.IsActive, user.Id).
+	mock.ExpectExec("UPDATE users SET username = ?, full_name = ?, email = ?, role = ?, updated_at = ?, is_active = ? WHERE id = ?").
+		WithArgs(user.Username, user.FullName, user.Email, user.Role, sqlmock.AnyArg(), user.IsActive, user.Id).
 		WillReturnResult(sqlmock.NewResult(0, 1))
 
 	// Call the function being tested
@@ -303,11 +304,11 @@ func TestUserRepository_GetAll(t *testing.T) {
 	expectedUsers := []*models.User{user1, user2}
 
 	// Set up the expected SQL query and result
-	rows := sqlmock.NewRows([]string{"id", "full_name", "email", "password", "role", "created_at", "updated_at", "is_active"}).
-		AddRow(user1.Id, user1.FullName, user1.Email, user1.Password, user1.Role, user1.CreatedAt, user1.UpdatedAt, user1.IsActive).
-		AddRow(user2.Id, user2.FullName, user2.Email, user2.Password, user2.Role, user2.CreatedAt, user2.UpdatedAt, user2.IsActive)
+	rows := sqlmock.NewRows([]string{"id", "username", "full_name", "email", "password_hash", "role", "created_at", "updated_at", "is_active"}).
+		AddRow(user1.Id, user1.Username, user1.FullName, user1.Email, user1.Password, user1.Role, user1.CreatedAt, user1.UpdatedAt, user1.IsActive).
+		AddRow(user2.Id, user2.Username, user2.FullName, user2.Email, user2.Password, user2.Role, user2.CreatedAt, user2.UpdatedAt, user2.IsActive)
 
-	mock.ExpectQuery("SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users ORDER BY created_at DESC").
+	mock.ExpectQuery("SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users ORDER BY created_at DESC").
 		WillReturnRows(rows)
 
 	// Call the function being tested
@@ -324,84 +325,161 @@ func TestUserRepository_GetAll(t *testing.T) {
 }
 
 func TestUserRepository_VerifyPassword(t *testing.T) {
-	// Create a new mock database with exact query matching
-	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+	emailIdentifier := "test@example.com"
+	usernameIdentifier := "testuser"
+	correctPassword := "password123"
+	incorrectPassword := "wrongpassword"
+	now := time.Now()
+
+	hashedCorrectPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(correctPassword), bcrypt.DefaultCost)
 	if err != nil {
-		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+		t.Fatalf("Failed to generate password hash for testing: %v", err)
 	}
-	defer db.Close()
+	hashedCorrectPassword := string(hashedCorrectPasswordBytes)
 
-	// Create a new repository with the mock database
-	repo := &UserRepository{
-		dbClient: &DatabaseClient{DB: db},
-	}
+	userColumnNames := []string{"id", "username", "full_name", "email", "password_hash", "role", "created_at", "updated_at", "is_active"}
+	queryByEmail := "SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE email = ?"
+	queryByUsername := "SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE username = ?"
 
-	// Set up test data
-	email := "test@example.com"
-	password := "password123"
-	now := time.Now()
-	hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
-	if err != nil {
-		t.Fatalf("Failed to generate password hash: %v", err)
-	}
-	hashedPassword := string(hashedBytes)
-	expectedUser := &models.User{
-		Id:        "test-id",
-		FullName:  "Test User",
-		Email:     email,
-		Password:  hashedPassword,
+	baseUser := &models.User{
+		Id:        "test-id-common",
+		FullName:  "Test User Common",
+		Password:  hashedCorrectPassword,
 		Role:      "user",
 		CreatedAt: now,
 		UpdatedAt: now,
 		IsActive:  true,
 	}
 
-	// Set up the expected SQL query and result
-	rows := sqlmock.NewRows([]string{"id", "full_name", "email", "password", "role", "created_at", "updated_at", "is_active"}).
-		AddRow(expectedUser.Id, expectedUser.FullName, expectedUser.Email, expectedUser.Password, expectedUser.Role, expectedUser.CreatedAt, expectedUser.UpdatedAt, expectedUser.IsActive)
-
-	mock.ExpectQuery("SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE email = ?").
-		WithArgs(email).
-		WillReturnRows(rows)
-
-	// Call the function being tested
-	user, err := repo.VerifyPassword(email, password)
-
-	// Assert that no errors occurred and the user matches the expected user
-	assert.NoError(t, err)
-	assert.Equal(t, expectedUser.Id, user.Id)
-	assert.Equal(t, expectedUser.Email, user.Email)
-
-	// Verify that all expectations were met
-	if err := mock.ExpectationsWereMet(); err != nil {
-		t.Errorf("there were unfulfilled expectations: %s", err)
-	}
-
-	// Test with incorrect password
-	db2, mock2, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
-	if err != nil {
-		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
-	}
-	defer db2.Close()
-
-	// Create a new repository with the new mock database
-	repo2 := &UserRepository{
-		dbClient: &DatabaseClient{DB: db2},
-	}
-
-	rows2 := sqlmock.NewRows([]string{"id", "full_name", "email", "password", "role", "created_at", "updated_at", "is_active"}).
-		AddRow(expectedUser.Id, expectedUser.FullName, expectedUser.Email, expectedUser.Password, expectedUser.Role, expectedUser.CreatedAt, expectedUser.UpdatedAt, expectedUser.IsActive)
-
-	mock2.ExpectQuery("SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE email = ?").
-		WithArgs(email).
-		WillReturnRows(rows2)
-
-	// Call the function being tested with wrong password
-	_, err = repo2.VerifyPassword(email, "wrong_password")
-
-	// Assert that an error occurred
-	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "invalid password")
+	t.Run("FoundByEmail_CorrectPassword", func(t *testing.T) {
+		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+		if err != nil {
+			t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+		}
+		defer db.Close()
+		repo := &UserRepository{dbClient: &DatabaseClient{DB: db}}
+
+		userToReturn := *baseUser // copy
+		userToReturn.Email = emailIdentifier
+		userToReturn.Username = "someusername" // Ensure all fields are plausible
+
+		rows := sqlmock.NewRows(userColumnNames).
+			AddRow(userToReturn.Id, userToReturn.Username, userToReturn.FullName, userToReturn.Email, userToReturn.Password, userToReturn.Role, userToReturn.CreatedAt, userToReturn.UpdatedAt, userToReturn.IsActive)
+		mock.ExpectQuery(queryByEmail).WithArgs(emailIdentifier).WillReturnRows(rows)
+
+		user, err := repo.VerifyPassword(emailIdentifier, correctPassword)
+
+		assert.NoError(t, err)
+		assert.NotNil(t, user)
+		assert.Equal(t, userToReturn.Id, user.Id)
+		assert.Equal(t, userToReturn.Email, user.Email)
+		if err := mock.ExpectationsWereMet(); err != nil {
+			t.Errorf("there were unfulfilled expectations: %s", err)
+		}
+	})
+
+	t.Run("FoundByEmail_IncorrectPassword", func(t *testing.T) {
+		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+		if err != nil {
+			t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+		}
+		defer db.Close()
+		repo := &UserRepository{dbClient: &DatabaseClient{DB: db}}
+
+		userToReturn := *baseUser // copy
+		userToReturn.Email = emailIdentifier
+		userToReturn.Username = "someusername"
+
+		rows := sqlmock.NewRows(userColumnNames).
+			AddRow(userToReturn.Id, userToReturn.Username, userToReturn.FullName, userToReturn.Email, userToReturn.Password, userToReturn.Role, userToReturn.CreatedAt, userToReturn.UpdatedAt, userToReturn.IsActive)
+		mock.ExpectQuery(queryByEmail).WithArgs(emailIdentifier).WillReturnRows(rows)
+
+		user, err := repo.VerifyPassword(emailIdentifier, incorrectPassword)
+
+		assert.Error(t, err)
+		assert.Nil(t, user)
+		assert.Contains(t, err.Error(), "invalid password")
+		if err := mock.ExpectationsWereMet(); err != nil {
+			t.Errorf("there were unfulfilled expectations: %s", err)
+		}
+	})
+
+	t.Run("NotFoundByEmail_FoundByUsername_CorrectPassword", func(t *testing.T) {
+		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+		if err != nil {
+			t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+		}
+		defer db.Close()
+		repo := &UserRepository{dbClient: &DatabaseClient{DB: db}}
+
+		userToReturn := *baseUser // copy
+		userToReturn.Username = usernameIdentifier
+		userToReturn.Email = "someother@example.com" // Ensure all fields are plausible
+
+		mock.ExpectQuery(queryByEmail).WithArgs(usernameIdentifier).WillReturnError(sql.ErrNoRows)
+		rows := sqlmock.NewRows(userColumnNames).
+			AddRow(userToReturn.Id, userToReturn.Username, userToReturn.FullName, userToReturn.Email, userToReturn.Password, userToReturn.Role, userToReturn.CreatedAt, userToReturn.UpdatedAt, userToReturn.IsActive)
+		mock.ExpectQuery(queryByUsername).WithArgs(usernameIdentifier).WillReturnRows(rows)
+
+		user, err := repo.VerifyPassword(usernameIdentifier, correctPassword)
+
+		assert.NoError(t, err)
+		assert.NotNil(t, user)
+		assert.Equal(t, userToReturn.Id, user.Id)
+		assert.Equal(t, userToReturn.Username, user.Username)
+		if err := mock.ExpectationsWereMet(); err != nil {
+			t.Errorf("there were unfulfilled expectations: %s", err)
+		}
+	})
+
+	t.Run("NotFoundByEmail_FoundByUsername_IncorrectPassword", func(t *testing.T) {
+		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+		if err != nil {
+			t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+		}
+		defer db.Close()
+		repo := &UserRepository{dbClient: &DatabaseClient{DB: db}}
+
+		userToReturn := *baseUser // copy
+		userToReturn.Username = usernameIdentifier
+		userToReturn.Email = "someother@example.com"
+
+		mock.ExpectQuery(queryByEmail).WithArgs(usernameIdentifier).WillReturnError(sql.ErrNoRows)
+		rows := sqlmock.NewRows(userColumnNames).
+			AddRow(userToReturn.Id, userToReturn.Username, userToReturn.FullName, userToReturn.Email, userToReturn.Password, userToReturn.Role, userToReturn.CreatedAt, userToReturn.UpdatedAt, userToReturn.IsActive)
+		mock.ExpectQuery(queryByUsername).WithArgs(usernameIdentifier).WillReturnRows(rows)
+
+		user, err := repo.VerifyPassword(usernameIdentifier, incorrectPassword)
+
+		assert.Error(t, err)
+		assert.Nil(t, user)
+		assert.Contains(t, err.Error(), "invalid password")
+		if err := mock.ExpectationsWereMet(); err != nil {
+			t.Errorf("there were unfulfilled expectations: %s", err)
+		}
+	})
+
+	t.Run("NotFoundByEmailOrUsername", func(t *testing.T) {
+		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
+		if err != nil {
+			t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+		}
+		defer db.Close()
+		repo := &UserRepository{dbClient: &DatabaseClient{DB: db}}
+
+		nonExistentIdentifier := "nonexistentuser"
+		mock.ExpectQuery(queryByEmail).WithArgs(nonExistentIdentifier).WillReturnError(sql.ErrNoRows)
+		mock.ExpectQuery(queryByUsername).WithArgs(nonExistentIdentifier).WillReturnError(sql.ErrNoRows)
+
+		user, err := repo.VerifyPassword(nonExistentIdentifier, correctPassword)
+
+		assert.Error(t, err)
+		assert.Nil(t, user)
+		assert.Contains(t, err.Error(), "user not found") // This error comes from GetByUsername after GetByEmail fails
+		if err := mock.ExpectationsWereMet(); err != nil {
+			t.Errorf("there were unfulfilled expectations: %s", err)
+		}
+	})
 }
 
 func TestUserRepository_ActivateUser(t *testing.T) {
@@ -549,7 +627,7 @@ func TestUserRepository_GetByID_NotFound(t *testing.T) {
 	userID := "non-existent-id"
 
 	// Set up the expected SQL query to return no rows
-	mock.ExpectQuery("SELECT id, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE id = ?").
+	mock.ExpectQuery("SELECT id, username, full_name, email, password_hash, role, created_at, updated_at, is_active FROM users WHERE id = ?").
 		WithArgs(userID).
 		WillReturnError(sql.ErrNoRows)
 
diff --git a/Frontend/quasar.config.ts b/Frontend/quasar.config.ts
index 7913011..0153035 100644
--- a/Frontend/quasar.config.ts
+++ b/Frontend/quasar.config.ts
@@ -118,7 +118,8 @@ export default defineConfig((/* ctx */) => {
 
       // Quasar plugins
       plugins: [
-        'Notify'
+        'Notify',
+        'Loading'
       ]
     },
 
diff --git a/Frontend/src/pages/LoginPage.vue b/Frontend/src/pages/LoginPage.vue
index a9b0282..d0a932d 100644
--- a/Frontend/src/pages/LoginPage.vue
+++ b/Frontend/src/pages/LoginPage.vue
@@ -3,6 +3,7 @@ import { computed, ref, reactive, onMounted, watch } from 'vue'
 import { useRouter } from 'vue-router'
 import { useAuthStore } from '../stores/auth'
 import * as TurnstileCaptchaComponent from '../components/Global/TurnstileCaptcha.vue'
+import { useQuasar } from 'quasar'
 
 // Define Turnstile interface for TypeScript
 interface TurnstileOptions {
@@ -29,7 +30,7 @@ const showModal = ref(false)
 const recoveryEmail = ref('')
 const recoveryPhone = ref('')
 const showCaptchaDialog = ref(false)
-const captchaToken = ref<string|null>(null)
+const captchaToken = ref<string | null>(null)
 
 const isSendingRecovery = ref(false)
 const recoverySent = ref(false)
@@ -66,14 +67,17 @@ const isRecoveryInputValid = computed(() => {
 
 const router = useRouter()
 const authStore = useAuthStore()
+const $q = useQuasar()
 
 const form = reactive({
-  email: '',
+  username: '',
   password: ''
 })
 
+const isPageLoading = ref(true)
+
 const errors = reactive({
-  email: '',
+  username: '',
   password: '',
 })
 
@@ -83,27 +87,45 @@ const loginError = ref('')
 const isCaptchaLoading = ref(false)
 const isLoginValidated = ref(false)
 
-onMounted( async () => {
-  if (authStore.isAuthenticated) await router.push('/')
+onMounted(async () => {
+  // Show page loader
+  isPageLoading.value = true
+
+  try {
+    if (authStore.isAuthenticated) await router.push('/')
+
+    // Load Turnstile script if not already loaded
+    if (!window.turnstile) {
+      const script = document.createElement('script')
+      script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
+      script.async = true
+      script.defer = true
+      document.head.appendChild(script)
+    }
 
-  // Load Turnstile script if not already loaded
-  if (!window.turnstile) {
-    const script = document.createElement('script')
-    script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
-    script.async = true
-    script.defer = true
-    document.head.appendChild(script)
+    // Simulate a minimum loading time for better UX
+    await new Promise(resolve => setTimeout(resolve, 800))
+  } catch (error) {
+    console.error('Error during page initialization:', error)
+  } finally {
+    // Hide page loader
+    isPageLoading.value = false
   }
 })
 
-const validateEmail = () => {
-  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
-  if (!form.email) {
-    errors.email = 'Email is required'
-  } else if (!emailRegex.test(form.email)) {
-    errors.email = 'Please enter a valid email'
+const validateUsername = () => {
+  if (!form.username) {
+    errors.username = 'Username or email is required'
   } else {
-    errors.email = ''
+    // Allow either email or username format
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+    const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/
+
+    if (!emailRegex.test(form.username) && !usernameRegex.test(form.username)) {
+      errors.username = 'Please enter a valid username or email'
+    } else {
+      errors.username = ''
+    }
   }
 }
 
@@ -130,6 +152,13 @@ const handleCaptchaError = () => {
 
 const proceedLogin = async () => {
   isSubmitting.value = true
+  // Show loading overlay during login
+  $q.loading.show({
+    message: 'Signing in...',
+    spinnerColor: 'primary',
+    spinnerSize: 80
+  })
+
   try {
     const result = await authStore.login(form)
     if (result.success) {
@@ -139,9 +168,9 @@ const proceedLogin = async () => {
     } else {
       captchaToken.value = null
       form.password = ''
-      let msg = result.message || 'Invalid email or password. Please try again.'
+      let msg = result.message || 'Invalid username/email or password. Please try again.'
       if (msg === 'Invalid credentials') {
-        msg = 'Invalid email or password.'
+        msg = 'Invalid username/email or password.'
       }
       loginError.value = msg
       showShake.value = true
@@ -154,15 +183,17 @@ const proceedLogin = async () => {
     setTimeout(() => showShake.value = false, 500)
   } finally {
     isSubmitting.value = false
+    // Hide loading overlay
+    $q.loading.hide()
   }
 }
 
 const handleSubmit = () => {
-  validateEmail()
+  validateUsername()
   validatePassword()
   loginError.value = ''
 
-  if (errors.email || errors.password) {
+  if (errors.username || errors.password) {
     showShake.value = true
     setTimeout(() => showShake.value = false, 500)
     return
@@ -174,13 +205,13 @@ const handleSubmit = () => {
 
 // Watch for when both email and password are filled to set captcha loading
 watch(
-  () => [form.email, form.password],
-  ([email, password], [oldEmail, oldPassword]) => {
-    if (email && password && (!oldEmail || !oldPassword)) {
+  () => [form.username, form.password],
+  ([username, password], [oldUsername, oldPassword]) => {
+    if (username && password && (!oldUsername || !oldPassword)) {
       isCaptchaLoading.value = true
     }
     // Optionally, if either is cleared, reset loading state
-    if (!email || !password) {
+    if (!username || !password) {
       isCaptchaLoading.value = false
     }
   }
@@ -189,7 +220,13 @@ watch(
 
 <template>
   <div class="login-page">
-    <div class="login-container" :class="{ 'shake-animation': showShake }">
+      <div class="login-container" :class="{ 'shake-animation': showShake }">
+      <!-- Spinner overlay when page is loading -->
+      <div v-if="isPageLoading" class="spinner-overlay">
+        <q-spinner-dots color="primary" size="80px" />
+      </div>
+
+      <!-- Actual content -->
       <div class="logo">
         <q-icon name="account_circle" size="48px" />
       </div>
@@ -203,29 +240,18 @@ watch(
         </div>
 
         <div class="input-group">
-          <input
-            v-model="form.email"
-            id="email"
-            placeholder="Enter your email"
-            @blur="validateEmail"
-            :class="{ 'error': errors.email }"
-          >
-          <span class="error-message" v-if="errors.email">{{ errors.email }}</span>
+          <input v-model="form.username" id="username" placeholder="Enter your username or email"
+            @blur="validateUsername" :class="{ 'error': errors.username }">
+          <span class="error-message" v-if="errors.username">{{ errors.username }}</span>
         </div>
 
         <div class="input-group">
-          <input
-            v-model="form.password"
-            type="password"
-            id="password"
-            placeholder="Enter your password"
-            @blur="validatePassword"
-            :class="{ 'error': errors.password }"
-          >
+          <input v-model="form.password" type="password" id="password" placeholder="Enter your password"
+            @blur="validatePassword" :class="{ 'error': errors.password }">
           <span class="error-message" v-if="errors.password">{{ errors.password }}</span>
           <div class="col flex content-center justify-end">
-                <a href="#" class="forgot-password" @click.prevent="showModal = true">Forgot password?</a>
-              </div>
+            <a href="#" class="forgot-password" @click.prevent="showModal = true">Forgot password?</a>
+          </div>
         </div>
 
         <button type="submit" :disabled="isSubmitting">
@@ -241,21 +267,11 @@ watch(
       <q-card class="recovery-card">
         <q-card-section class="header-section">
           <div class="text-h6">Password Recovery</div>
-          <q-icon
-            name="close"
-            class="close-icon"
-            @click="showModal = false"
-          />
+          <q-icon name="close" class="close-icon" @click="showModal = false" />
         </q-card-section>
 
-        <q-tabs
-          v-model="tab"
-          dense
-          class="bg-grey-1 text-dark"
-          active-color="primary"
-          indicator-color="primary"
-          align="justify"
-        >
+        <q-tabs v-model="tab" dense class="bg-grey-1 text-dark" active-color="primary" indicator-color="primary"
+          align="justify">
           <q-tab name="email" icon="mail" label="Email" />
           <q-tab name="phone" icon="phone" label="Phone" />
         </q-tabs>
@@ -264,11 +280,8 @@ watch(
 
         <q-card-section class="content-section">
           <div class="illustration-container">
-            <q-img
-              src="https://cdn-icons-png.flaticon.com/512/6195/6195699.png"
-              alt="Password Recovery"
-              style="width: 120px; height: 120px"
-            />
+            <q-img src="https://cdn-icons-png.flaticon.com/512/6195/6195699.png" alt="Password Recovery"
+              style="width: 120px; height: 120px" />
           </div>
 
           <div class="text-center text-body1 q-mb-md">
@@ -280,46 +293,24 @@ watch(
             </template>
           </div>
 
-          <q-input
-            v-if="tab === 'email'"
-            v-model="recoveryEmail"
-            type="email"
-            label="Email Address"
-            outlined
-            dense
+          <q-input v-if="tab === 'email'" v-model="recoveryEmail" type="email" label="Email Address" outlined dense
             class="q-mb-md"
-            :rules="[val => !!val || 'Email is required', val => /.+@.+\..+/.test(val) || 'Invalid email']"
-          >
+            :rules="[val => !!val || 'Email is required', val => /.+@.+\..+/.test(val) || 'Invalid email']">
             <template v-slot:prepend>
               <q-icon name="mail" />
             </template>
           </q-input>
 
-          <q-input
-            v-else
-            v-model="recoveryPhone"
-            type="tel"
-            label="Phone Number"
-            mask="(###) ###-####"
-            fill-mask
-            outlined
-            dense
-            class="q-mb-md"
-            :rules="[val => val.replace(/\D/g,'').length === 10 || 'Valid phone number required']"
-          >
+          <q-input v-else v-model="recoveryPhone" type="tel" label="Phone Number" mask="(###) ###-####" fill-mask
+            outlined dense class="q-mb-md"
+            :rules="[val => val.replace(/\D/g, '').length === 10 || 'Valid phone number required']">
             <template v-slot:prepend>
               <q-icon name="phone" />
             </template>
           </q-input>
 
-          <q-btn
-            label="Send Recovery Link"
-            color="primary"
-            class="full-width q-mt-md"
-            :loading="isSendingRecovery"
-            :disable="!isRecoveryInputValid"
-            @click="handleRecoveryRequest"
-          />
+          <q-btn label="Send Recovery Link" color="primary" class="full-width q-mt-md" :loading="isSendingRecovery"
+            :disable="!isRecoveryInputValid" @click="handleRecoveryRequest" />
 
           <div class="text-center text-caption q-mt-md">
             Remember your password?
@@ -347,11 +338,8 @@ watch(
           <div class="text-h6">CAPTCHA Verification</div>
         </q-card-section>
         <q-card-section>
-          <TurnstileCaptchaComponent.default
-            @verify="handleCaptchaVerified"
-            @error="handleCaptchaError"
-            @expired="handleCaptchaError"
-          />
+          <TurnstileCaptchaComponent.default @verify="handleCaptchaVerified" @error="handleCaptchaError"
+            @expired="handleCaptchaError" />
         </q-card-section>
       </q-card>
     </q-dialog>
@@ -364,14 +352,40 @@ watch(
 @import '../assets/styles/LoginPage.dark.css';
 
 /* Keep only animation-related styles here */
+
+/* Spinner overlay styles */
+.spinner-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.26);
+  z-index: 10;
+}
+
 .shake-animation {
   animation: shake 0.5s;
 }
 
 @keyframes shake {
-  0%, 100% { transform: translateX(0); }
-  20%, 60% { transform: translateX(-5px); }
-  40%, 80% { transform: translateX(5px); }
+  0%,
+  100% {
+    transform: translateX(0);
+  }
+
+  20%,
+  60% {
+    transform: translateX(-5px);
+  }
+
+  40%,
+  80% {
+    transform: translateX(5px);
+  }
 }
 
 .spinner {
@@ -415,7 +429,9 @@ watch(
 }
 
 @keyframes spin {
-  to { transform: rotate(360deg); }
+  to {
+    transform: rotate(360deg);
+  }
 }
 
 .captcha-placeholder {
diff --git a/Frontend/src/stores/auth.ts b/Frontend/src/stores/auth.ts
index 9830f04..8bf55af 100644
--- a/Frontend/src/stores/auth.ts
+++ b/Frontend/src/stores/auth.ts
@@ -37,9 +37,9 @@ export const useAuthStore = defineStore('auth', () => {
     localStorage.removeItem('user')
   }
 
-  async function login(credentials: { email: string; password: string }): Promise<{ success: boolean; message?: string }> {
+  async function login(credentials: { username: string; password: string }): Promise<{ success: boolean; message?: string }> {
     clearAuth();
-    console.log('Attempting login with:', credentials.email);
+    console.log('Attempting login with:', credentials.username);
     try {
       const response = await axios.post<LoginResponse>('/api/users/login', credentials);
 
@@ -50,8 +50,6 @@ export const useAuthStore = defineStore('auth', () => {
         if (response.data.user) {
           setUser(response.data.user);
         }
-        // TODO: Alternatively, fetch user details in a separate request using the token
-
         console.log('Login successful');
         return { success: true };
       } else {


Reviews:
user: login: github-actions[bot]
body: Gemini AI Code Reviewer Comments
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-10T16:24:53Z

Review comments:
diff_hunk: @@ -110,6 +112,7 @@ func generateToken() (string, error) {
 // @Router /users/register [post]
 func (h *UserHandler) Register(c *fiber.Ctx) error {
 	var input struct {
path: Backend/internal/handlers/user_handlers.go
user: login: github-actions[bot]
body: Consider standardizing the error message.  It currently says "Username/Email".  Pick one, or reword it to be more generic, like "Username or Email" or "Identifier".
created_at: 2025-05-10T16:24:53Z
author_association: NONE
diff_hunk: @@ -124,9 +127,9 @@
 	}
 
 	// Validate input
path: Backend/internal/handlers/user_handlers.go
user: login: github-actions[bot]
body: There's a potential timing attack vulnerability here. If `GetByEmail` takes significantly longer than `GetByUsername`, an attacker could potentially infer whether a given email exists in the system.  Consider using a single function in the repository layer that tries both email and username, but always takes the same amount of time, regardless of whether the user exists or not.
created_at: 2025-05-10T16:24:53Z
author_association: NONE
diff_hunk: @@ -159,9 +162,27 @@
 	}
 
 	// Create user
+	// Check if username already exists
+	exists, err = h.userRepo.UsernameExists(input.Username)
+	if err != nil {
+		log.Printf("Error checking username existence: %v", err)
+		return c.Status(fiber.StatusInternalServerError).JSON(ErrorResponse{
+			Error:      "Internal server error",
+			StatusCode: fiber.StatusInternalServerError,
+		})
+	}
+
+	if exists {
+		return c.Status(fiber.StatusConflict).JSON(ErrorResponse{
+			Error:      "Username already in use",
+			StatusCode: fiber.StatusConflict,
path: Backend/internal/handlers/user_handlers.go
user: login: github-actions[bot]
body: Since the `VerifyPassword` function now accepts a username or email, it's crucial to ensure that it correctly handles both cases to prevent potential security vulnerabilities. Verify that the underlying implementation correctly identifies the user based on the provided identifier (email or username) before comparing the password. Ensure consistent hashing algorithms are used for both email and username lookups.
created_at: 2025-05-10T16:24:53Z
author_association: NONE
diff_hunk: @@ -193,7 +209,7 @@ func TestUserHandler_Register_EmailExists(t *testing.T) {
path: Backend/internal/handlers/user_handlers_test.go
user: login: github-actions[bot]
body: Consider using `t.Fatalf` or `t.Errorf` to immediately stop the test if `json.Marshal` fails, as the test cannot proceed without the request body.
created_at: 2025-05-10T16:24:53Z
author_association: NONE
diff_hunk: @@ -65,8 +65,21 @@
 	return args.Get(0).([]*models.User), args.Error(1)
 }
 
-func (m *MockUserRepository) VerifyPassword(email, password string) (*models.User, error) {
path: Backend/internal/handlers/user_handlers_test.go
user: login: github-actions[bot]
body: The value for the `username` field in the test request body is set to `test@example.com`, which looks like an email. While it might work if usernames are allowed to be email addresses, it's better to use a dedicated username for testing purposes to avoid confusion and potential issues if the username format is restricted later on. Consider changing it to something like `testuser`.
created_at: 2025-05-10T16:24:53Z
author_association: NONE

PR comments:
user: login: netlify[bot]
created_at: 2025-05-10T16:24:09Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *cortes-surplus* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | fb5c2d28a369f56ef22dec37779f98ce35b50159 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/cortes-surplus/deploys/681fd64e613b0e0008953836 |
author_association: NONE
user: login: Neil-urk12
created_at: 2025-05-10T16:24:17Z
body: /gemini-review
author_association: OWNER
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-10T22:42:19Z
body: ## Deploying surplus-sales-management-system with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>fb5c2d2</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://6f224c03.surplus-sales-management-system.pages.dev'>https://6f224c03.surplus-sales-management-system.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://oop-65.surplus-sales-management-system.pages.dev'>https://oop-65.surplus-sales-management-system.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/46d4dc27915004623364d5865e5bbe65/pages/view/surplus-sales-management-system/6f224c03-4b38-48fa-bd15-143fd4a3f805)

author_association: NONE