182 lines
4.4 KiB
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
|
|
}
|