diff --git a/handlers/jwt.go b/handlers/jwt.go index 2630c98..32f306d 100644 --- a/handlers/jwt.go +++ b/handlers/jwt.go @@ -183,14 +183,25 @@ func generateAccessToken(email, sessionID, userID string, roleID []int) (string, AccessTokenExpiration = "45" } + if roleID == nil { + roleID = []int{} + } + + var primaryRoleID *int + if len(roleID) > 0 { + value := roleID[0] + primaryRoleID = &value + } + expirationTime := time.Now().Add(24 * time.Hour).Unix() claims := &models.AccessToken{ - Email: email, - UsersID: userID, - RoleID: roleID, - SessionID: sessionID, - Exp: expirationTime, + Email: email, + UsersID: userID, + RoleID: primaryRoleID, + AdditionalRoleID: roleID, + SessionID: sessionID, + Exp: expirationTime, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Unix(expirationTime, 0)), }, diff --git a/models/jwt.go b/models/jwt.go index 5c07549..d9f353a 100644 --- a/models/jwt.go +++ b/models/jwt.go @@ -7,11 +7,12 @@ import ( ) type AccessToken struct { - Email string `json:"email"` - UsersID string `json:"users_id"` - RoleID []int `json:"role_id"` - SessionID string `json:"session_id"` - Exp int64 `json:"exp"` + Email string `json:"email"` + UsersID string `json:"users_id"` + RoleID *int `json:"role_id,omitempty"` + AdditionalRoleID []int `json:"additional_role_id"` + SessionID string `json:"session_id"` + Exp int64 `json:"exp"` jwt.RegisteredClaims } diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..75b4ef2 --- /dev/null +++ b/models/user.go @@ -0,0 +1,31 @@ +package models + +type User struct { + UserID string `json:"user_id"` + FirstName string `json:"first_name"` + MiddleInitial *string `json:"middle_initial"` + LastName string `json:"last_name"` + Suffix *string `json:"suffix"` + OfficeID *int `json:"office_id"` + RoleID *int `json:"role_id,omitempty"` + Projects *[]ProjectMetadata `json:"projects,omitempty"` + EmailAddress string `json:"email_address"` + MIS *int `json:"mis"` + CAPI *int `json:"capi"` + CAWI *int `json:"cawi"` + DPS *int `json:"dps"` + Sex *string `json:"sex"` + OfficeName *string `json:"office_name"` + StatusOfEmployment *string `json:"status_of_employment"` + UserType *int `json:"user_type"` + HomeAddress *string `json:"home_address"` + ContactNumber *string `json:"contact_number"` + UpdatedBy *string `json:"updated_by"` +} + +type ProjectMetadata struct { + ProjectID int `json:"project_id"` + Alias *string `json:"alias"` + RoleID []int `json:"role_id"` + OfficeID *int `json:"office_id"` +} diff --git a/services/users.go b/services/users.go index bc9a9d4..864a7b2 100644 --- a/services/users.go +++ b/services/users.go @@ -2,7 +2,13 @@ package services import ( "authentication/db" + "authentication/models" + "database/sql" + "fmt" "log" + "sort" + "strconv" + "strings" ) func GetUserID(email string) (string, error) { @@ -53,27 +59,255 @@ func GetUserIDFromEmail(email string) (string, error) { func GetRoleIDsFromEmail(email string) ([]int, error) { log.Print(email) - query := `SELECT ur.role_id - FROM uess_user_management.user_roles ur - JOIN uess_user_management.users u ON ur.users_id = u.users_id + + globalQuery := `SELECT DISTINCT ur.role_id + FROM uess_user_management.users u + JOIN uess_user_management.user_roles ur ON u.users_id = ur.users_id WHERE u.email_address = ? - AND u.is_deleted = 0` - rows, err := db.DB.Query(query, email) + AND u.is_deleted = 0 + AND ur.role_id IS NOT NULL` + + globalRows, err := db.DB.Query(globalQuery, email) if err != nil { return nil, err } - defer rows.Close() + defer globalRows.Close() - var roleIDs []int - for rows.Next() { - var roleID int - if err := rows.Scan(&roleID); err != nil { - return nil, err - } - roleIDs = append(roleIDs, roleID) - } - if err := rows.Err(); err != nil { + projectQuery := `SELECT DISTINCT pa.role_id + FROM uess_user_management.users u + JOIN uess_project_management.project_assignment pa ON u.users_id = pa.users_id + WHERE u.email_address = ? + AND u.is_deleted = 0 + AND pa.is_active = 1 + AND pa.role_id IS NOT NULL` + + projectRows, err := db.DB.Query(projectQuery, email) + if err != nil { return nil, err } + defer projectRows.Close() + + roleIDs := make([]int, 0) + seen := make(map[int]struct{}) + + for globalRows.Next() { + var roleID int + if err := globalRows.Scan(&roleID); err != nil { + return nil, err + } + if _, exists := seen[roleID]; !exists { + seen[roleID] = struct{}{} + roleIDs = append(roleIDs, roleID) + } + } + if err := globalRows.Err(); err != nil { + return nil, err + } + + for projectRows.Next() { + var roleID int + if err := projectRows.Scan(&roleID); err != nil { + return nil, err + } + if _, exists := seen[roleID]; !exists { + seen[roleID] = struct{}{} + roleIDs = append(roleIDs, roleID) + } + } + if err := projectRows.Err(); err != nil { + return nil, err + } + + sort.Ints(roleIDs) + return roleIDs, nil +} + +func FetchUserByEmail(email string) (models.User, error) { + query := `SELECT u.users_id, u.first_name, u.middle_initial, u.last_name, u.suffix, GROUP_CONCAT(DISTINCT ur.role_id ORDER BY ur.role_id) AS role_ids, u.office_id, u.email_address, MAX(ur.MIS), MAX(ur.CAPI), MAX(ur.CAWI), MAX(ur.DPS), + u.sex, u.status_of_employment, u.home_address, u.contact_number, u.user_type + FROM uess_user_management.users u + LEFT JOIN uess_user_management.user_roles ur + ON u.users_id = ur.users_id + WHERE u.email_address = ? + GROUP BY u.users_id, u.first_name, u.middle_initial, u.last_name, u.suffix, u.office_id, u.email_address, u.sex, u.status_of_employment, u.home_address, u.contact_number, u.user_type` + + var user models.User + var roleIDsCSV sql.NullString + + var middleInitial sql.NullString + var suffix sql.NullString + var officeID sql.NullInt64 + var mis sql.NullInt64 + var capi sql.NullInt64 + var cawi sql.NullInt64 + var dps sql.NullInt64 + var sex sql.NullString + var statusOfEmployment sql.NullString + var homeAddress sql.NullString + var contactNumber sql.NullString + var userType sql.NullInt64 + + err := db.DB.QueryRow(query, email).Scan( + &user.UserID, + &user.FirstName, + &middleInitial, + &user.LastName, + &suffix, + &roleIDsCSV, + &officeID, + &user.EmailAddress, + &mis, + &capi, + &cawi, + &dps, + &sex, + &statusOfEmployment, + &homeAddress, + &contactNumber, + &userType, + ) + if err != nil { + return user, err + } + + if middleInitial.Valid { + value := middleInitial.String + user.MiddleInitial = &value + } + if suffix.Valid { + value := suffix.String + user.Suffix = &value + } + if officeID.Valid { + value := int(officeID.Int64) + user.OfficeID = &value + } + if mis.Valid { + value := int(mis.Int64) + user.MIS = &value + } + if capi.Valid { + value := int(capi.Int64) + user.CAPI = &value + } + if cawi.Valid { + value := int(cawi.Int64) + user.CAWI = &value + } + if dps.Valid { + value := int(dps.Int64) + user.DPS = &value + } + if sex.Valid { + value := sex.String + user.Sex = &value + } + if statusOfEmployment.Valid { + value := statusOfEmployment.String + user.StatusOfEmployment = &value + } + if homeAddress.Valid { + value := homeAddress.String + user.HomeAddress = &value + } + if contactNumber.Valid { + value := contactNumber.String + user.ContactNumber = &value + } + if userType.Valid { + value := int(userType.Int64) + user.UserType = &value + } + + baseRoleIDs, parseErr := parseRoleIDsCSV(roleIDsCSV.String) + if parseErr != nil { + return user, parseErr + } + if len(baseRoleIDs) > 0 { + primaryRoleID := baseRoleIDs[0] + user.RoleID = &primaryRoleID + } + + projectsQuery := `SELECT pa.project_id, p.alias, GROUP_CONCAT(DISTINCT pa.role_id ORDER BY pa.role_id) AS role_ids, u.office_id + FROM uess_user_management.users u + LEFT JOIN uess_project_management.project_assignment pa + ON u.users_id = pa.users_id AND pa.is_active = 1 + LEFT JOIN uess_project_management.project p + ON pa.project_id = p.project_id + WHERE u.email_address = ? AND pa.project_id IS NOT NULL + GROUP BY pa.project_id, p.alias, u.office_id` + + rows, err := db.DB.Query(projectsQuery, email) + if err != nil { + return user, err + } + defer rows.Close() + + projects := make([]models.ProjectMetadata, 0) + for rows.Next() { + var project models.ProjectMetadata + var projectAlias sql.NullString + var projectRoleIDsCSV sql.NullString + var projectOfficeID sql.NullInt64 + + if scanErr := rows.Scan(&project.ProjectID, &projectAlias, &projectRoleIDsCSV, &projectOfficeID); scanErr != nil { + return user, scanErr + } + + if projectAlias.Valid { + alias := projectAlias.String + project.Alias = &alias + } + + if projectOfficeID.Valid { + office := int(projectOfficeID.Int64) + project.OfficeID = &office + } + + roleIDs, parseErr := parseRoleIDsCSV(projectRoleIDsCSV.String) + if parseErr != nil { + return user, parseErr + } + project.RoleID = roleIDs + projects = append(projects, project) + } + + if err := rows.Err(); err != nil { + return user, err + } + + if len(projects) > 0 { + user.Projects = &projects + if user.RoleID == nil && len(projects[0].RoleID) > 0 { + primaryRoleID := projects[0].RoleID[0] + user.RoleID = &primaryRoleID + } + } + + return user, nil +} + +func parseRoleIDsCSV(roleIDsCSV string) ([]int, error) { + trimmed := strings.TrimSpace(roleIDsCSV) + if trimmed == "" { + return make([]int, 0), nil + } + + parts := strings.Split(trimmed, ",") + roleIDs := make([]int, 0, len(parts)) + + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + + parsed, err := strconv.Atoi(value) + if err != nil { + return nil, fmt.Errorf("invalid role id %q: %w", value, err) + } + roleIDs = append(roleIDs, parsed) + } + return roleIDs, nil } diff --git a/services/users_test.go b/services/users_test.go index 6770aa7..5c06c33 100644 --- a/services/users_test.go +++ b/services/users_test.go @@ -2,6 +2,7 @@ package services import ( "database/sql" + "reflect" "testing" "authentication/db" @@ -330,3 +331,164 @@ func TestCheckEmailInDBVariousEmails(t *testing.T) { } } } + +func TestGetRoleIDsFromEmail(t *testing.T) { + mock, cleanup := setupMockDB(t) + defer cleanup() + + email := "roles@example.com" + expectedRoleIDs := []int{2, 4, 8} + + globalRows := sqlmock.NewRows([]string{"role_id"}). + AddRow(2). + AddRow(8) + + projectRows := sqlmock.NewRows([]string{"role_id"}). + AddRow(4). + AddRow(8). + AddRow(2) + + mock.ExpectQuery(`SELECT DISTINCT ur\.role_id\s+FROM uess_user_management\.users u\s+JOIN uess_user_management\.user_roles ur ON u\.users_id = ur\.users_id\s+WHERE u\.email_address = \?\s+AND u\.is_deleted = 0\s+AND ur\.role_id IS NOT NULL`). + WithArgs(email). + WillReturnRows(globalRows) + + mock.ExpectQuery(`SELECT DISTINCT pa\.role_id\s+FROM uess_user_management\.users u\s+JOIN uess_project_management\.project_assignment pa ON u\.users_id = pa\.users_id\s+WHERE u\.email_address = \?\s+AND u\.is_deleted = 0\s+AND pa\.is_active = 1\s+AND pa\.role_id IS NOT NULL`). + WithArgs(email). + WillReturnRows(projectRows) + + roleIDs, err := GetRoleIDsFromEmail(email) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !reflect.DeepEqual(roleIDs, expectedRoleIDs) { + t.Errorf("Expected role IDs %v, got %v", expectedRoleIDs, roleIDs) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unfulfilled expectations: %v", err) + } +} + +func TestGetRoleIDsFromEmailQueryError(t *testing.T) { + mock, cleanup := setupMockDB(t) + defer cleanup() + + email := "roles-error@example.com" + + mock.ExpectQuery(`SELECT DISTINCT ur\.role_id\s+FROM uess_user_management\.users u\s+JOIN uess_user_management\.user_roles ur ON u\.users_id = ur\.users_id\s+WHERE u\.email_address = \?\s+AND u\.is_deleted = 0\s+AND ur\.role_id IS NOT NULL`). + WithArgs(email). + WillReturnError(sql.ErrConnDone) + + roleIDs, err := GetRoleIDsFromEmail(email) + if err == nil { + t.Error("Expected error, got nil") + } + + if roleIDs != nil { + t.Errorf("Expected nil role IDs on error, got %v", roleIDs) + } +} + +func TestGetRoleIDsFromEmailNoRowsReturnsEmptySlice(t *testing.T) { + mock, cleanup := setupMockDB(t) + defer cleanup() + + email := "no-roles@example.com" + + globalRows := sqlmock.NewRows([]string{"role_id"}) + projectRows := sqlmock.NewRows([]string{"role_id"}) + + mock.ExpectQuery(`SELECT DISTINCT ur\.role_id\s+FROM uess_user_management\.users u\s+JOIN uess_user_management\.user_roles ur ON u\.users_id = ur\.users_id\s+WHERE u\.email_address = \?\s+AND u\.is_deleted = 0\s+AND ur\.role_id IS NOT NULL`). + WithArgs(email). + WillReturnRows(globalRows) + + mock.ExpectQuery(`SELECT DISTINCT pa\.role_id\s+FROM uess_user_management\.users u\s+JOIN uess_project_management\.project_assignment pa ON u\.users_id = pa\.users_id\s+WHERE u\.email_address = \?\s+AND u\.is_deleted = 0\s+AND pa\.is_active = 1\s+AND pa\.role_id IS NOT NULL`). + WithArgs(email). + WillReturnRows(projectRows) + + roleIDs, err := GetRoleIDsFromEmail(email) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if roleIDs == nil { + t.Error("Expected empty slice, got nil") + } + + if len(roleIDs) != 0 { + t.Errorf("Expected empty slice, got %v", roleIDs) + } +} + +func TestParseRoleIDsCSV(t *testing.T) { + roleIDs, err := parseRoleIDsCSV("1, 12,3") + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + expected := []int{1, 12, 3} + if !reflect.DeepEqual(roleIDs, expected) { + t.Fatalf("Expected %v, got %v", expected, roleIDs) + } +} + +func TestParseRoleIDsCSVEmpty(t *testing.T) { + roleIDs, err := parseRoleIDsCSV("") + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if roleIDs == nil { + t.Fatal("Expected empty slice, got nil") + } + + if len(roleIDs) != 0 { + t.Fatalf("Expected empty slice, got %v", roleIDs) + } +} + +func TestFetchUserByEmail(t *testing.T) { + mock, cleanup := setupMockDB(t) + defer cleanup() + + email := "d.israel.psa@gmail.com" + + userRows := sqlmock.NewRows([]string{ + "users_id", "first_name", "middle_initial", "last_name", "suffix", "role_ids", "office_id", "email_address", "MIS", "CAPI", "CAWI", "DPS", "sex", "status_of_employment", "home_address", "contact_number", "user_type", + }).AddRow( + "U0000000001", "AAAAAAAA", "A", "Israel", "", "1", 103, email, 1, 1, 1, 1, nil, "COSW", "Quezon City", "09171234567", nil, + ) + + mock.ExpectQuery(`SELECT u\.users_id, u\.first_name, u\.middle_initial, u\.last_name, u\.suffix, GROUP_CONCAT\(DISTINCT ur\.role_id ORDER BY ur\.role_id\) AS role_ids, u\.office_id, u\.email_address, MAX\(ur\.MIS\), MAX\(ur\.CAPI\), MAX\(ur\.CAWI\), MAX\(ur\.DPS\),\s+u\.sex, u\.status_of_employment, u\.home_address, u\.contact_number, u\.user_type\s+FROM uess_user_management\.users u\s+LEFT JOIN uess_user_management\.user_roles ur\s+ON u\.users_id = ur\.users_id\s+WHERE u\.email_address = \?\s+GROUP BY u\.users_id, u\.first_name, u\.middle_initial, u\.last_name, u\.suffix, u\.office_id, u\.email_address, u\.sex, u\.status_of_employment, u\.home_address, u\.contact_number, u\.user_type`). + WithArgs(email). + WillReturnRows(userRows) + + projectRows := sqlmock.NewRows([]string{"project_id", "alias", "role_ids", "office_id"}). + AddRow(1, "TTT", "1,12", 103) + + mock.ExpectQuery(`SELECT pa\.project_id, p\.alias, GROUP_CONCAT\(DISTINCT pa\.role_id ORDER BY pa\.role_id\) AS role_ids, u\.office_id\s+FROM uess_user_management\.users u\s+LEFT JOIN uess_project_management\.project_assignment pa\s+ON u\.users_id = pa\.users_id AND pa\.is_active = 1\s+LEFT JOIN uess_project_management\.project p\s+ON pa\.project_id = p\.project_id\s+WHERE u\.email_address = \? AND pa\.project_id IS NOT NULL\s+GROUP BY pa\.project_id, p\.alias, u\.office_id`). + WithArgs(email). + WillReturnRows(projectRows) + + user, err := FetchUserByEmail(email) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if user.RoleID == nil || *user.RoleID != 1 { + t.Fatalf("Expected RoleID=1, got %v", user.RoleID) + } + + if user.Projects == nil || len(*user.Projects) != 1 { + t.Fatalf("Expected 1 project, got %v", user.Projects) + } + + if !reflect.DeepEqual((*user.Projects)[0].RoleID, []int{1, 12}) { + t.Fatalf("Expected project roles [1 12], got %v", (*user.Projects)[0].RoleID) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("Unfulfilled expectations: %v", err) + } +}