package authentication import ( "context" "crypto/rand" "encoding/base32" "errors" "time" "Yimaru-Backend/internal/domain" "golang.org/x/crypto/bcrypt" ) var ( ErrInvalidPassword = errors.New("incorrect password") ErrUserNotFound = errors.New("user not found") ErrExpiredToken = errors.New("token expired") ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again ErrUserSuspended = errors.New("user has been suspended") ) type LoginSuccess struct { UserId int64 Role domain.Role RfToken string CompanyID domain.ValidInt64 } func (s *Service) Login(ctx context.Context, email, phone string, password string, companyID domain.ValidInt64) (LoginSuccess, error) { user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone, companyID) if err != nil { return LoginSuccess{}, err } err = matchPassword(password, user.Password) if err != nil { return LoginSuccess{}, err } if user.Suspended { return LoginSuccess{}, ErrUserSuspended } oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID) if err != nil && err != ErrRefreshTokenNotFound { return LoginSuccess{}, err } // If old refresh token is not revoked, revoke it if err == nil && !oldRefreshToken.Revoked { err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token) if err != nil { return LoginSuccess{}, err } } refreshToken, err := generateRefreshToken() if err != nil { return LoginSuccess{}, err } err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ Token: refreshToken, UserID: user.ID, CreatedAt: time.Now(), ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), }) if err != nil { return LoginSuccess{}, err } return LoginSuccess{ UserId: user.ID, Role: user.Role, RfToken: refreshToken, CompanyID: user.OrganizationID, }, nil } func (s *Service) RefreshToken(ctx context.Context, refToken string) (domain.RefreshToken, error) { token, err := s.tokenStore.GetRefreshToken(ctx, refToken) if err != nil { return domain.RefreshToken{}, err } if token.Revoked { return domain.RefreshToken{}, ErrRefreshTokenNotFound } if token.ExpiresAt.Before(time.Now()) { return domain.RefreshToken{}, ErrExpiredToken } // newRefToken, err := generateRefreshToken() // if err != nil { // return "", err // } // err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ // Token: newRefToken, // UserID: token.UserID, // CreatedAt: time.Now(), // ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), // }) return token, nil } func (s *Service) GetLastLogin(ctx context.Context, user_id int64) (*time.Time, error) { refreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user_id) if err != nil { return nil, err } return &refreshToken.CreatedAt, nil } func (s *Service) Logout(ctx context.Context, refToken string) error { token, err := s.tokenStore.GetRefreshToken(ctx, refToken) if err != nil { return err } if token.Revoked { return ErrRefreshTokenNotFound } if token.ExpiresAt.Before(time.Now()) { return ErrExpiredToken } return s.tokenStore.RevokeRefreshToken(ctx, refToken) } func matchPassword(plaintextPassword string, hash []byte) error { err := bcrypt.CompareHashAndPassword(hash, []byte(plaintextPassword)) if err != nil { switch { case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword): return ErrInvalidPassword default: return err } } return err } func generateRefreshToken() (string, error) { randomBytes := make([]byte, 32) _, err := rand.Read(randomBytes) if err != nil { return "", err } plaintext := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes) return plaintext, nil }