mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-11 18:10:23 +08:00
124 lines
4.2 KiB
Go
124 lines
4.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
"google.golang.org/api/drive/v3"
|
|
|
|
"backupx/server/internal/model"
|
|
"backupx/server/internal/repository"
|
|
"backupx/server/internal/security"
|
|
"backupx/server/internal/storage"
|
|
"backupx/server/internal/storage/codec"
|
|
)
|
|
|
|
type GoogleDriveOAuthResult struct {
|
|
TargetID *uint
|
|
Config storage.GoogleDriveConfig
|
|
State string
|
|
}
|
|
|
|
type GoogleDriveOAuthService struct {
|
|
sessions repository.OAuthSessionRepository
|
|
cipher *codec.Cipher
|
|
now func() time.Time
|
|
generateState func() (string, error)
|
|
exchangeCode func(context.Context, *oauth2.Config, string) (*oauth2.Token, error)
|
|
ttl time.Duration
|
|
}
|
|
|
|
type googleDriveOAuthPayload struct {
|
|
TargetID *uint `json:"targetId,omitempty"`
|
|
Config storage.GoogleDriveConfig `json:"config"`
|
|
}
|
|
|
|
func NewGoogleDriveOAuthService(sessions repository.OAuthSessionRepository, cipher *codec.Cipher) *GoogleDriveOAuthService {
|
|
return &GoogleDriveOAuthService{
|
|
sessions: sessions,
|
|
cipher: cipher,
|
|
now: func() time.Time { return time.Now().UTC() },
|
|
generateState: func() (string, error) {
|
|
return security.GenerateSecret(24)
|
|
},
|
|
exchangeCode: func(ctx context.Context, config *oauth2.Config, code string) (*oauth2.Token, error) {
|
|
return config.Exchange(ctx, code)
|
|
},
|
|
ttl: 10 * time.Minute,
|
|
}
|
|
}
|
|
|
|
func (s *GoogleDriveOAuthService) Start(ctx context.Context, targetID *uint, cfg storage.GoogleDriveConfig) (string, string, error) {
|
|
if strings.TrimSpace(cfg.ClientID) == "" || strings.TrimSpace(cfg.ClientSecret) == "" {
|
|
return "", "", fmt.Errorf("google drive client credentials are required")
|
|
}
|
|
if strings.TrimSpace(cfg.RedirectURL) == "" {
|
|
return "", "", fmt.Errorf("google drive redirect url is required")
|
|
}
|
|
state, err := s.generateState()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("generate oauth state: %w", err)
|
|
}
|
|
payload := googleDriveOAuthPayload{TargetID: targetID, Config: cfg}
|
|
ciphertext, err := s.cipher.EncryptValue(payload)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("encrypt oauth payload: %w", err)
|
|
}
|
|
now := s.now()
|
|
session := &model.OAuthSession{ProviderType: string(storage.ProviderTypeGoogleDrive), State: state, PayloadCiphertext: ciphertext, TargetID: targetID, ExpiresAt: now.Add(s.ttl)}
|
|
if err := s.sessions.Create(ctx, session); err != nil {
|
|
return "", "", fmt.Errorf("create oauth session: %w", err)
|
|
}
|
|
oauthConfig := s.oauthConfig(cfg)
|
|
url := oauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
|
|
return url, state, nil
|
|
}
|
|
|
|
func (s *GoogleDriveOAuthService) Complete(ctx context.Context, state string, code string) (*GoogleDriveOAuthResult, error) {
|
|
session, err := s.sessions.FindByState(ctx, state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("find oauth session: %w", err)
|
|
}
|
|
if session == nil {
|
|
return nil, fmt.Errorf("google drive oauth state not found")
|
|
}
|
|
now := s.now()
|
|
if session.UsedAt != nil {
|
|
return nil, fmt.Errorf("google drive oauth state already used")
|
|
}
|
|
if now.After(session.ExpiresAt) {
|
|
return nil, fmt.Errorf("google drive oauth state expired")
|
|
}
|
|
var payload googleDriveOAuthPayload
|
|
if err := s.cipher.DecryptValue(session.PayloadCiphertext, &payload); err != nil {
|
|
return nil, fmt.Errorf("decrypt oauth session payload: %w", err)
|
|
}
|
|
token, err := s.exchangeCode(ctx, s.oauthConfig(payload.Config), code)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("exchange google drive oauth code: %w", err)
|
|
}
|
|
if strings.TrimSpace(token.RefreshToken) == "" {
|
|
return nil, fmt.Errorf("google drive oauth response missing refresh token")
|
|
}
|
|
payload.Config.RefreshToken = token.RefreshToken
|
|
session.UsedAt = &now
|
|
if err := s.sessions.Update(ctx, session); err != nil {
|
|
return nil, fmt.Errorf("mark oauth session used: %w", err)
|
|
}
|
|
return &GoogleDriveOAuthResult{TargetID: payload.TargetID, Config: payload.Config, State: state}, nil
|
|
}
|
|
|
|
func (s *GoogleDriveOAuthService) oauthConfig(cfg storage.GoogleDriveConfig) *oauth2.Config {
|
|
return &oauth2.Config{
|
|
ClientID: cfg.ClientID,
|
|
ClientSecret: cfg.ClientSecret,
|
|
RedirectURL: cfg.RedirectURL,
|
|
Endpoint: google.Endpoint,
|
|
Scopes: []string{drive.DriveScope},
|
|
}
|
|
}
|