package authentication import ( "Yimaru-Backend/internal/domain" "context" "encoding/json" "errors" "fmt" "time" "cloud.google.com/go/auth/credentials/idtoken" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) var googleOAuthConfig *oauth2.Config func (s *Service) InitGoogleOAuth(clientID, clientSecret, redirectURL string) { googleOAuthConfig = &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", }, Endpoint: google.Endpoint, } } func (s *Service) GenerateGoogleLoginURL(state string) string { return googleOAuthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline) } func (s *Service) ExchangeGoogleCode( ctx context.Context, code string, ) (*oauth2.Token, error) { if code == "" { return nil, errors.New("missing google auth code") } return googleOAuthConfig.Exchange(ctx, code) } func (s *Service) FetchGoogleUser( ctx context.Context, token *oauth2.Token, ) (*domain.GoogleUser, error) { client := googleOAuthConfig.Client(ctx, token) resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") if err != nil { return nil, fmt.Errorf("failed to fetch google user: %w", err) } defer resp.Body.Close() var user domain.GoogleUser if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { return nil, fmt.Errorf("failed to decode google user: %w", err) } if user.Email == "" { return nil, errors.New("google account has no email") } return &user, nil } func (s *Service) LoginWithGoogleAndroid( ctx context.Context, idToken string, clientID string, ) (domain.LoginSuccess, error) { if idToken == "" { return domain.LoginSuccess{}, errors.New("missing id token") } // 1. Verify ID token with Google payload, err := idtoken.Validate(ctx, idToken, clientID) if err != nil { return domain.LoginSuccess{}, errors.New("invalid google id token") } // 2. Extract Google user info gUser := domain.GoogleUser{ ID: payload.Subject, Email: payload.Claims["email"].(string), VerifiedEmail: payload.Claims["email_verified"].(bool), GivenName: payload.Claims["given_name"].(string), FamilyName: payload.Claims["family_name"].(string), Picture: payload.Claims["picture"].(string), } // 3. Delegate to existing login logic return s.LoginWithGoogle(ctx, gUser) } func (s *Service) LoginWithGoogle( ctx context.Context, gUser domain.GoogleUser, ) (domain.LoginSuccess, error) { var user domain.User var err error // 1. Try login via Google ID user, err = s.userStore.GetUserByGoogleID(ctx, gUser.ID) if err != nil { // 2. Try account linking by email user, err = s.userStore.GetUserByEmailPhone(ctx, gUser.Email, "") if err != nil { // 3. First-time Google user → create user, err = s.userStore.CreateGoogleUser(ctx, gUser) if err != nil { return domain.LoginSuccess{}, err } } else { // 4. Link Google account if err := s.userStore.LinkGoogleAccount(ctx, user.ID, gUser.ID); err != nil { return domain.LoginSuccess{}, err } } } // 5. Enforce account status if user.Status == domain.UserStatusPending { return domain.LoginSuccess{}, domain.ErrUserNotVerified } if user.Status == domain.UserStatusSuspended { return domain.LoginSuccess{}, ErrUserSuspended } // 6. Revoke existing refresh token (single active session) oldToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID) if err == nil && !oldToken.Revoked { if err := s.tokenStore.RevokeRefreshToken(ctx, oldToken.Token); err != nil { return domain.LoginSuccess{}, err } } // 7. Issue new refresh token (initial issuance, not rotation) refreshToken, err := generateRefreshToken() if err != nil { return domain.LoginSuccess{}, err } if 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), }); err != nil { return domain.LoginSuccess{}, err } // 8. Return standard login response return domain.LoginSuccess{ UserId: user.ID, Role: user.Role, RfToken: refreshToken, }, nil } func (s *Service) verifyGoogleIDToken( ctx context.Context, idToken string, clientID string, ) (*idtoken.Payload, error) { payload, err := idtoken.Validate(ctx, idToken, clientID) if err != nil { return nil, err } return payload, nil }