Yimaru-BackEnd/internal/services/authentication/google.go

182 lines
4.4 KiB
Go

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
}