package authService import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strings" "time" "git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/utils/logger" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/view" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) const googleUserInfoURL = "https://www.googleapis.com/oauth2/v2/userinfo" // googleOAuthConfig returns the OAuth2 config for Google func googleOAuthConfig() *oauth2.Config { cfg := config.Get().OAuth.Google scopes := cfg.Scopes if len(scopes) == 0 { scopes = []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", } } return &oauth2.Config{ ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, RedirectURL: cfg.RedirectURL, Scopes: scopes, Endpoint: google.Endpoint, } } // GenerateOAuthState generates a random state token for CSRF protection func (s *AuthService) GenerateOAuthState() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil } // GetGoogleAuthURL returns the Google OAuth2 authorization URL with a state token func (s *AuthService) GetGoogleAuthURL(state string) string { return googleOAuthConfig().AuthCodeURL(state, oauth2.AccessTypeOnline) } // HandleGoogleCallback exchanges the code for a token, fetches user info, // and either logs in or registers the user, returning an AuthResponse and raw refresh token. func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, string, error) { oauthCfg := googleOAuthConfig() // Exchange authorization code for token token, err := oauthCfg.Exchange(context.Background(), code) if err != nil { return nil, "", fmt.Errorf("failed to exchange code: %w", err) } // Fetch user info from Google userInfo, err := fetchGoogleUserInfo(oauthCfg.Client(context.Background(), token)) if err != nil { return nil, "", fmt.Errorf("failed to fetch user info: %w", err) } if !userInfo.VerifiedEmail { return nil, "", responseErrors.ErrEmailNotVerified } // Find or create user user, err := s.findOrCreateGoogleUser(userInfo) if err != nil { if strings.Contains(err.Error(), "database") { logger.Error("google oauth callback failed - database error", "service", "AuthService.HandleGoogleCallback", "email", userInfo.Email, "error", err.Error(), ) } return nil, "", err } // Update last login now := time.Now() user.LastLoginAt = &now s.db.Save(user) // Generate access token (JWT) accessToken, err := s.generateAccessToken(user) if err != nil { return nil, "", fmt.Errorf("failed to generate access token: %w", err) } // Generate opaque refresh token and store in DB rawRefreshToken, err := s.createRefreshToken(user.ID) if err != nil { return nil, "", fmt.Errorf("failed to create refresh token: %w", err) } return &model.AuthResponse{ AccessToken: accessToken, TokenType: "Bearer", ExpiresIn: s.config.JWTExpiration, User: user.ToSession(), }, rawRefreshToken, nil } // findOrCreateGoogleUser finds an existing user by Google provider ID or email, // or creates a new one. func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.Customer, error) { var user *model.Customer // Try to find by provider + provider_id user, err := s.customerRepo.GetByExternalProviderId(model.ProviderGoogle, info.ID) if err == nil { // Update avatar in case it changed user.AvatarURL = info.Picture err = s.customerRepo.Save(user) if err != nil { return nil, err } return user, nil } // Try to find by email (user may have registered locally before) user, err = s.customerRepo.GetByEmail(info.Email) if err == nil { // Link Google provider to existing account user.Provider = model.ProviderGoogle user.ProviderID = info.ID user.AvatarURL = info.Picture user.IsActive = true err = s.customerRepo.Save(user) if err != nil { return nil, err } // If email has not been verified yet, send email to admin. if !user.EmailVerified { baseURL := config.Get().App.BaseURL if err := s.email.SendNewUserAdminNotification(info.Email, info.Name, baseURL); err != nil { // Log error but don't fail registration _ = err } } user.EmailVerified = true return user, nil } // Create new user newUser := model.Customer{ Email: info.Email, FirstName: info.GivenName, LastName: info.FamilyName, Provider: model.ProviderGoogle, RoleID: 1, // user ProviderID: info.ID, AvatarURL: info.Picture, IsActive: true, EmailVerified: true, LangID: 2, // default is english CountryID: 2, // default is England } if err := s.customerRepo.Create(&newUser); err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } // If everything succeeded, send email to admin. if !user.EmailVerified { baseURL := config.Get().App.BaseURL if err := s.email.SendNewUserAdminNotification(info.Email, info.Name, baseURL); err != nil { // Log error but don't fail registration _ = err } } var role *model.Role role, err = s.roleRepo.Get(newUser.RoleID) if err != nil { return nil, err } newUser.Role = role return &newUser, nil } // fetchGoogleUserInfo fetches user info from Google using the provided HTTP client func fetchGoogleUserInfo(client *http.Client) (*view.GoogleUserInfo, error) { resp, err := client.Get(googleUserInfoURL) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var userInfo view.GoogleUserInfo if err := json.Unmarshal(body, &userInfo); err != nil { return nil, err } return &userInfo, nil }