package authentication import ( "fmt" "net/http" "strconv" "strings" "time" "github.com/golang-jwt/jwt" "github.com/google/uuid" "simrs-vx/internal/domain/main-entities/intern" eu "simrs-vx/internal/domain/main-entities/user" pa "simrs-vx/internal/lib/auth" el "simrs-vx/pkg/logger" p "simrs-vx/pkg/password" ed "simrs-vx/internal/domain/main-entities/doctor" ee "simrs-vx/internal/domain/main-entities/employee" em "simrs-vx/internal/domain/main-entities/midwife" en "simrs-vx/internal/domain/main-entities/nurse" erc "simrs-vx/internal/domain/references/common" erg "simrs-vx/internal/domain/references/organization" a "github.com/karincake/apem" dg "github.com/karincake/apem/db-gorm-pg" ms "github.com/karincake/apem/ms-redis" d "github.com/karincake/dodol" l "github.com/karincake/lepet" ) var authCfg AuthCfg func init() { a.RegisterExtCall(GetConfig) } // Generates token and store in redis at one place // just return the error code func GenToken(input eu.LoginDto) (*d.Data, error) { // Get User user := &eu.User{Name: input.Name} // if input.Position_Code != "" { // user.Position_Code = input.Position_Code // } if errCode := getAndCheck(user, user); errCode != "" { return nil, d.FieldErrors{"authentication": d.FieldError{Code: errCode, Message: el.GenMessage(errCode)}} } if user.LoginAttemptCount > 5 { if user.LastSuccessLogin != nil { now := time.Now() lastAllowdLogin := user.LastAllowdLogin if lastAllowdLogin.After(now.Add(-time.Hour * 1)) { return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-tooMany", Message: el.GenMessage("auth-login-tooMany")}} } else { tn := time.Now() user.LastAllowdLogin = &tn user.LoginAttemptCount = 0 dg.I.Save(&user) } } else { tn := time.Now() user.LastAllowdLogin = &tn dg.I.Save(&user) return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-tooMany", Message: el.GenMessage("auth-login-tooMany")}} } } if !p.Check(input.Password, user.Password) { user.LoginAttemptCount++ dg.I.Save(&user) return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-incorrect", Message: el.GenMessage("auth-login-incorrect")}} } else if user.Status_Code == erc.USCBlocked { return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-blocked", Message: el.GenMessage("auth-login-blocked")}} } else if user.Status_Code == erc.USCNew { return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-unverified", Message: el.GenMessage("auth-login-unverified")}} } // Access token prep id, err := uuid.NewRandom() if err != nil { panic(fmt.Sprintf(l.I.Msg("uuid-gen-fail"), err)) } if input.Duration == 0 { input.Duration = 24 * 60 } duration := time.Minute * time.Duration(input.Duration) aUuid := id.String() atExpires := time.Now().Add(duration).Unix() atSecretKey := authCfg.AtSecretKey // Create Claim atClaims := jwt.MapClaims{} atClaims["user_id"] = user.Id atClaims["user_name"] = user.Name atClaims["user_contractPosition_code"] = user.ContractPosition_Code atClaims["uuid"] = aUuid atClaims["exp"] = atExpires // Create output outputData := d.II{ "user_id": strconv.Itoa(int(user.Id)), "user_name": user.Name, "user_contractPosition_code": user.ContractPosition_Code, } // extra role := []string{} switch user.ContractPosition_Code { case erg.CSCEmp: // employee employee := ee.Employee{} dg.I.Where("\"User_Id\" = ?", user.Id).First(&employee) if employee.Id == 0 { return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noEmployee", Message: el.GenMessage("auth-noEmployee")}} } atClaims["employee_id"] = employee.Id outputData["employee_id"] = employee.Id role = append(role, "emp-"+string(*employee.Position_Code)) //if employee.Division_Code != nil { // atClaims["employee_division_code"] = employee.Division_Code // outputData["employee_division_code"] = employee.Division_Code //} // employee position if employee.Id > 0 && employee.Position_Code != nil { switch *employee.Position_Code { case erg.EPCDoc: doctor := ed.Doctor{} dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&doctor) if doctor.Id == 0 { return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noDoctor", Message: el.GenMessage("auth-noDoctor")}} } atClaims["doctor_id"] = doctor.Id outputData["doctor_id"] = doctor.Id // specialist if doctor.Specialist_Id != nil { atClaims["specialist_id"] = doctor.Specialist_Id outputData["specialist_id"] = doctor.Specialist_Id } if doctor.Subspecialist_Id != nil { atClaims["subspecialist_id"] = doctor.Subspecialist_Id outputData["subspecialist_id"] = doctor.Subspecialist_Id } case erg.EPCNur: empData := en.Nurse{} dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&empData) if empData.Id == 0 { return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noNurse", Message: el.GenMessage("auth-noNurse")}} } atClaims["nurse_id"] = empData.Id outputData["nurse_id"] = empData.Id case erg.EPCMwi: empData := em.Midwife{} dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&empData) if empData.Id == 0 { return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noNurse", Message: el.GenMessage("auth-noNurse")}} } atClaims["nurse_id"] = empData.Id outputData["nurse_id"] = empData.Id } errorGetPosition := d.FieldErrors{"authentication": d.FieldError{Code: "auth-getData-failed", Message: el.GenMessage("auth-getData-failed")}} // division position divisionPositions, err := getDivisionPosition(employee.Id) if err != nil { return nil, errorGetPosition } // installation position installationPositions, err := getInstallationPosition(employee.Id) if err != nil { return nil, errorGetPosition } // unit position unitPositions, err := getUnitPosition(employee.Id) if err != nil { return nil, errorGetPosition } // specialist position specialistPositions, err := getSpecialistPosition(employee.Id) if err != nil { return nil, errorGetPosition } // subspecialist position subspecialistPositions, err := getSubspecialistPosition(employee.Id) if err != nil { return nil, errorGetPosition } role = append(role, divisionPositions...) role = append(role, installationPositions...) role = append(role, unitPositions...) role = append(role, specialistPositions...) role = append(role, subspecialistPositions...) // atClaims["division_positions"] = divsionPositions // outputData["division_positions"] = divsionPositions } case erg.CSCInt: intern := intern.Intern{} dg.I.Where("\"User_Id\" = ?", user.Id).First(&intern) role = append(role, "int-"+string(*intern.Position_Code)) case erg.CSCSys: role = append(role, "system") } atClaims["roles"] = role outputData["roles"] = role // Generate jwt at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) ats, err := at.SignedString([]byte(atSecretKey)) if err != nil { return nil, d.FieldErrors{"user": d.FieldError{Code: "token-sign-err", Message: el.GenMessage("token-sign-err")}} } outputData["accessToken"] = ats // Save to redis now := time.Now() atx := time.Unix(atExpires, 0) //converting Unix to UTC(to Time object) err = ms.I.Set(aUuid, strconv.Itoa(int(user.Id)), atx.Sub(now)).Err() if err != nil { panic(fmt.Sprintf(l.I.Msg("redis-store-fail"), err.Error())) } tn := time.Now() user.LoginAttemptCount = 0 user.LastSuccessLogin = &tn user.LastAllowdLogin = &tn dg.I.Save(&user) // Current data return &d.Data{ Meta: d.IS{ "source": "authentication", "structure": "single-data", "status": "verified", }, Data: outputData, }, nil } func RevokeToken(uuid string) { ms.I.Del(uuid) } func VerifyToken(r *http.Request, tokenType TokenType) (data *jwt.Token, errCode, errDetail string) { auth := r.Header.Get("Authorization") if auth == "" { return nil, "auth-missingHeader", "" } authArr := strings.Split(auth, " ") if len(authArr) == 2 { auth = authArr[1] } token, err := jwt.Parse(auth, func(token *jwt.Token) (any, error) { //Make sure that the token method conform to "SigningMethodHMAC" if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf(l.I.Msg("token-sign-unexcpeted"), token.Header["alg"]) } if tokenType == AccessToken { return []byte(authCfg.AtSecretKey), nil } else { return []byte(authCfg.RtSecretKey), nil } }) if err != nil { return nil, "token-parse-fail", err.Error() } return token, "", "" } func ExtractToken(r *http.Request, tokenType TokenType) (data *pa.AuthInfo, err error) { token, errCode, errDetail := VerifyToken(r, tokenType) if errCode != "" { return nil, d.FieldError{Code: errCode, Message: el.GenMessage(errCode, errDetail)} } claims, ok := token.Claims.(jwt.MapClaims) if ok && token.Valid { accessUuid, ok := claims["uuid"].(string) if !ok { return nil, d.FieldError{Code: "token-invalid", Message: el.GenMessage("token-invalid", "uuid not available")} } user_id, myErr := strconv.ParseInt(fmt.Sprintf("%.f", claims["user_id"]), 10, 64) if myErr != nil { return nil, d.FieldError{Code: "token-invalid", Message: el.GenMessage("token-invalid", "uuid is not available")} } accessUuidRedis := ms.I.Get(accessUuid) if accessUuidRedis.String() == "" { return nil, d.FieldError{Code: "token-unidentified", Message: el.GenMessage("token-unidentified")} } data = &pa.AuthInfo{ Uuid: accessUuid, User_Id: uint(user_id), User_Name: fmt.Sprintf("%v", claims["user_name"]), } data.User_ContractPosition_code = checkStrClaims(claims, "contractPosition_code") data.Employee_Position_Code = checkStrPtrClaims(claims, "employee_position_code") data.Doctor_Id = checkIntPtrClaims(claims, "doctor_id") data.Nurse_Id = checkIntPtrClaims(claims, "nurse_id") data.Midwife_Id = checkIntPtrClaims(claims, "midwife_id") data.Nutritionist_Id = checkIntPtrClaims(claims, "nutritionist_id") data.Laborant_Id = checkIntPtrClaims(claims, "laborant_id") data.Pharmachist_Id = checkIntPtrClaims(claims, "pharmachist_id") data.Intern_Position_Code = checkStrPtrClaims(claims, "intern_position_code") return } return nil, d.FieldError{Code: "token", Message: "token-invalid"} } func GetConfig() { a.ParseCfg(&authCfg) } func checkStrClaims(claim map[string]interface{}, key string) string { if v, exist := claim[key]; exist && v != nil { return v.(string) } return "" } func checkStrPtrClaims(claim map[string]interface{}, key string) *string { if v, exist := claim[key]; exist && v != nil { val := v.(string) return &val } return nil } func checkIntClaims(claim map[string]interface{}, key string) int { if v, exist := claim[key]; exist && v != nil { return v.(int) } return 0 } func checkIntPtrClaims(claim map[string]interface{}, key string) *int { if v, exist := claim[key]; exist && v != nil { val := int(v.(float64)) return &val } return nil }