// Package service provides business logic for the application package service import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "strings" "time" "accounting-app/internal/config" "accounting-app/internal/models" "accounting-app/internal/repository" ) // Gitee OAuth errors var ( ErrGiteeOAuthFailed = errors.New("gitee oauth authentication failed") ErrGiteeUserInfoFailed = errors.New("failed to get gitee user info") ) // GiteeUser represents Gitee user information type GiteeUser struct { ID int64 `json:"id"` Login string `json:"login"` Email string `json:"email"` Name string `json:"name"` AvatarURL string `json:"avatar_url"` } // GiteeTokenResponse represents Gitee OAuth token response type GiteeTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` Scope string `json:"scope"` CreatedAt int64 `json:"created_at"` } // GiteeOAuthService handles Gitee OAuth operations type GiteeOAuthService struct { userRepo *repository.UserRepository authService *AuthService cfg *config.Config httpClient *http.Client } // NewGiteeOAuthService creates a new GiteeOAuthService instance func NewGiteeOAuthService(userRepo *repository.UserRepository, authService *AuthService, cfg *config.Config) *GiteeOAuthService { return &GiteeOAuthService{ userRepo: userRepo, authService: authService, cfg: cfg, httpClient: &http.Client{ Timeout: 60 * time.Second, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, }, }, } } // GetAuthorizationURL returns the Gitee OAuth authorization URL func (s *GiteeOAuthService) GetAuthorizationURL(state string) string { params := url.Values{} params.Set("client_id", s.cfg.GiteeClientID) params.Set("redirect_uri", s.cfg.GiteeRedirectURL) params.Set("response_type", "code") params.Set("scope", "user_info") if state != "" { params.Set("state", state) } return fmt.Sprintf("https://gitee.com/oauth/authorize?%s", params.Encode()) } // ExchangeCodeForToken exchanges authorization code for access token func (s *GiteeOAuthService) ExchangeCodeForToken(code string) (*GiteeTokenResponse, error) { data := url.Values{} data.Set("grant_type", "authorization_code") data.Set("client_id", s.cfg.GiteeClientID) data.Set("client_secret", s.cfg.GiteeClientSecret) data.Set("code", code) data.Set("redirect_uri", s.cfg.GiteeRedirectURL) req, err := http.NewRequest("POST", "https://gitee.com/oauth/token", strings.NewReader(data.Encode())) if err != nil { fmt.Printf("[Gitee] Failed to create request: %v\n", err) return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") fmt.Printf("[Gitee] Exchanging code for token...\n") resp, err := s.httpClient.Do(req) if err != nil { fmt.Printf("[Gitee] Request failed: %v\n", err) return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { fmt.Printf("[Gitee] Token exchange failed with status: %d\n", resp.StatusCode) return nil, ErrGiteeOAuthFailed } var tokenResp GiteeTokenResponse if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { fmt.Printf("[Gitee] Failed to decode response: %v\n", err) return nil, err } if tokenResp.AccessToken == "" { fmt.Printf("[Gitee] No access token in response\n") return nil, ErrGiteeOAuthFailed } return &tokenResp, nil } // GetUserInfo retrieves Gitee user information using access token func (s *GiteeOAuthService) GetUserInfo(accessToken string) (*GiteeUser, error) { reqURL := fmt.Sprintf("https://gitee.com/api/v5/user?access_token=%s", url.QueryEscape(accessToken)) req, err := http.NewRequest("GET", reqURL, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") resp, err := s.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { fmt.Printf("[Gitee] Get user info failed with status: %d\n", resp.StatusCode) return nil, ErrGiteeUserInfoFailed } var user GiteeUser if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { return nil, err } // If email is empty, try to get from emails endpoint if user.Email == "" { email, err := s.getUserEmail(accessToken) if err == nil && email != "" { user.Email = email } } return &user, nil } // getUserEmail retrieves user's primary email from Gitee func (s *GiteeOAuthService) getUserEmail(accessToken string) (string, error) { reqURL := fmt.Sprintf("https://gitee.com/api/v5/emails?access_token=%s", url.QueryEscape(accessToken)) req, err := http.NewRequest("GET", reqURL, nil) if err != nil { return "", err } req.Header.Set("Accept", "application/json") resp, err := s.httpClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", ErrGiteeUserInfoFailed } var emails []struct { Email string `json:"email"` State string `json:"state"` Scope []string `json:"scope"` } if err := json.NewDecoder(resp.Body).Decode(&emails); err != nil { return "", err } // Find primary/confirmed email for _, e := range emails { if e.State == "confirmed" { return e.Email, nil } } // Fallback to first email if len(emails) > 0 { return emails[0].Email, nil } return "", nil } // HandleCallback processes Gitee OAuth callback func (s *GiteeOAuthService) HandleCallback(code string) (*models.User, *TokenPair, error) { // Exchange code for token tokenResp, err := s.ExchangeCodeForToken(code) if err != nil { return nil, nil, err } // Get Gitee user info giteeUser, err := s.GetUserInfo(tokenResp.AccessToken) if err != nil { return nil, nil, err } // Check if user already exists with this Gitee account user, err := s.userRepo.GetByOAuthProvider("gitee", fmt.Sprintf("%d", giteeUser.ID)) if err == nil { // User exists, update token and return _ = s.userRepo.UpdateOAuthToken("gitee", fmt.Sprintf("%d", giteeUser.ID), tokenResp.AccessToken) tokens, err := s.authService.generateTokenPair(user) if err != nil { return nil, nil, err } return user, tokens, nil } // Check if user exists with same email if giteeUser.Email != "" { existingUser, err := s.userRepo.GetByEmail(giteeUser.Email) if err == nil { // Link Gitee account to existing user oauth := &models.OAuthAccount{ UserID: existingUser.ID, Provider: "gitee", ProviderID: fmt.Sprintf("%d", giteeUser.ID), AccessToken: tokenResp.AccessToken, } if err := s.userRepo.CreateOAuthAccount(oauth); err != nil { return nil, nil, err } tokens, err := s.authService.generateTokenPair(existingUser) if err != nil { return nil, nil, err } return existingUser, tokens, nil } } // Create new user username := giteeUser.Login if giteeUser.Name != "" { username = giteeUser.Name } email := giteeUser.Email if email == "" { email = fmt.Sprintf("%d@gitee.user", giteeUser.ID) } newUser := &models.User{ Email: email, Username: username, Avatar: giteeUser.AvatarURL, IsActive: true, } if err := s.userRepo.Create(newUser); err != nil { return nil, nil, err } // Create OAuth account link oauth := &models.OAuthAccount{ UserID: newUser.ID, Provider: "gitee", ProviderID: fmt.Sprintf("%d", giteeUser.ID), AccessToken: tokenResp.AccessToken, } if err := s.userRepo.CreateOAuthAccount(oauth); err != nil { return nil, nil, err } tokens, err := s.authService.generateTokenPair(newUser) if err != nil { return nil, nil, err } return newUser, tokens, nil }