From 911cf6d1fbeca5f699a8f4d5499ad9fb86f95119 Mon Sep 17 00:00:00 2001 From: dpurbosakti Date: Tue, 19 Aug 2025 14:26:34 +0700 Subject: [PATCH] add auth --- .../authentication/authentication.go | 58 +++++ .../main-use-case/authentication/case.go | 218 ++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 internal/interface/main-handler/authentication/authentication.go create mode 100644 internal/use-case/main-use-case/authentication/case.go diff --git a/internal/interface/main-handler/authentication/authentication.go b/internal/interface/main-handler/authentication/authentication.go new file mode 100644 index 00000000..2b3cf437 --- /dev/null +++ b/internal/interface/main-handler/authentication/authentication.go @@ -0,0 +1,58 @@ +package authentication + +import ( + "context" + "net/http" + + d "github.com/karincake/dodol" + rw "github.com/karincake/risoles" + + m "simrs-vx/internal/domain/main-entities/user" + s "simrs-vx/internal/use-case/main-use-case/authentication" +) + +type authKey string + +const akInfo authKey = "authInfo" + +type Key struct{} + +// var Position m.Position + +func Login(w http.ResponseWriter, r *http.Request) { + var input m.LoginDto + if !(rw.ValidateStructByIOR(w, r.Body, &input)) { + return + } + + // input.Position = Position + res, err := s.GenToken(input) + if err != nil { + rw.WriteJSON(w, http.StatusUnauthorized, d.II{"errors": err}, nil) + } else { + rw.DataResponse(w, res, err) + } +} + +func Logout(w http.ResponseWriter, r *http.Request) { + authInfoContext := context.Context.Value(r.Context(), akInfo) + if authInfoContext == nil { + rw.WriteJSON(w, http.StatusUnauthorized, d.IS{"message": "logout skiped. the request is done wihtout authorization."}, nil) + return + } + authInfo := context.Context.Value(r.Context(), akInfo).(*s.AuthInfo) + s.RevokeToken(authInfo.Uuid) + rw.WriteJSON(w, http.StatusOK, d.IS{"message": "logged out"}, nil) +} + +func GuardMW(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + accessDetail, err := s.ExtractToken(r, s.AccessToken) + if err != nil { + rw.WriteJSON(w, http.StatusUnauthorized, err.(d.FieldError), nil) + return + } + ctx := context.WithValue(r.Context(), Key{}, accessDetail) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/internal/use-case/main-use-case/authentication/case.go b/internal/use-case/main-use-case/authentication/case.go new file mode 100644 index 00000000..0a4383e1 --- /dev/null +++ b/internal/use-case/main-use-case/authentication/case.go @@ -0,0 +1,218 @@ +package authentication + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + dg "github.com/karincake/apem/db-gorm-mysql" + d "github.com/karincake/dodol" + l "github.com/karincake/lepet" + + a "github.com/karincake/apem" + ms "github.com/karincake/apem/ms-redis" + + el "simrs-vx/pkg/logger" + p "simrs-vx/pkg/password" + + mu "simrs-vx/internal/domain/main-entities/user" + erc "simrs-vx/internal/domain/references/common" +) + +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 mu.LoginDto) (*d.Data, error) { + // Get User + user := &mu.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.SCBlocked { + return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-blocked", Message: el.GenMessage("auth-login-blocked")}} + } else if user.Status_Code == erc.SCNew { + 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 + + // extra + // if input.Position_Code == "doc" { + + // } + + // Creating Access Token + atClaims := jwt.MapClaims{} + atClaims["user_id"] = user.Id + atClaims["user_name"] = user.Name + // atClaims["user_email"] = user.Email + // atClaims["user_position_code"] = user.Position_Code + // atClaims["user_ref_id"] = user.Ref_Id + atClaims["exp"] = atExpires + atClaims["uuid"] = aUuid + 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")}} + } + + // 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: d.II{ + "user_id": strconv.Itoa(int(user.Id)), + "user_name": user.Name, + // "user_email": user.Email, + // "user_position_code": user.Position_Code, + // "user_ref_id": user.Ref_Id, + "accessToken": ats, + }, + }, 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 *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")} + } + user_name := fmt.Sprintf("%v", claims["user_name"]) + // user_email := "" + // if v, exist := claims["user_email"]; exist && v != nil { + // user_email = v.(string) + // } + // ref_id := 0 + // if v, exist := claims["user_ref_id"]; exist && v != nil { + // tmp := v.(float64) + // ref_id = int(tmp) + // } + // position_code := "" + // if v, exist := claims["user_position_code"]; exist && v != nil { + // position_code = v.(string) + // } + data = &AuthInfo{ + Uuid: accessUuid, + User_Id: int(user_id), + User_Name: user_name, + // User_Email: user_email, + // User_Ref_Id: ref_id, + // User_Position_Code: position_code, + } + return + } + return nil, d.FieldError{Code: "token", Message: "token-invalid"} +} + +func GetConfig() { + a.ParseCfg(&authCfg) +}