feat: Implement browser-based OAuth authentication using Chromedp and Rod, add an upload page, and introduce team processing API.
This commit is contained in:
122
backend/internal/auth/auth_log.go
Normal file
122
backend/internal/auth/auth_log.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuthStep 授权步骤
|
||||
type AuthStep string
|
||||
|
||||
const (
|
||||
StepBrowserStart AuthStep = "browser_start" // 启动浏览器
|
||||
StepNavigate AuthStep = "navigate" // 访问授权页面
|
||||
StepInputEmail AuthStep = "input_email" // 输入邮箱
|
||||
StepSubmitEmail AuthStep = "submit_email" // 提交邮箱
|
||||
StepInputPassword AuthStep = "input_password" // 输入密码
|
||||
StepSubmitPassword AuthStep = "submit_password" // 提交密码
|
||||
StepSelectWorkspace AuthStep = "select_workspace" // 选择工作区
|
||||
StepConsent AuthStep = "consent" // 授权同意
|
||||
StepWaitCallback AuthStep = "wait_callback" // 等待回调
|
||||
StepExtractCode AuthStep = "extract_code" // 提取授权码
|
||||
StepComplete AuthStep = "complete" // 完成授权
|
||||
)
|
||||
|
||||
// AuthLogEntry 授权日志条目
|
||||
type AuthLogEntry struct {
|
||||
Step AuthStep
|
||||
Message string
|
||||
Duration time.Duration
|
||||
Timestamp time.Time
|
||||
IsError bool
|
||||
}
|
||||
|
||||
// AuthLogger 授权日志记录器
|
||||
type AuthLogger struct {
|
||||
email string
|
||||
teamPrefix string
|
||||
memberIdx int
|
||||
callback func(entry AuthLogEntry)
|
||||
startTime time.Time
|
||||
stepStart time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewAuthLogger 创建授权日志记录器
|
||||
func NewAuthLogger(email, teamPrefix string, memberIdx int, callback func(entry AuthLogEntry)) *AuthLogger {
|
||||
return &AuthLogger{
|
||||
email: email,
|
||||
teamPrefix: teamPrefix,
|
||||
memberIdx: memberIdx,
|
||||
callback: callback,
|
||||
startTime: time.Now(),
|
||||
stepStart: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// LogStep 记录步骤
|
||||
func (l *AuthLogger) LogStep(step AuthStep, format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
entry := AuthLogEntry{
|
||||
Step: step,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Duration: now.Sub(l.stepStart),
|
||||
Timestamp: now,
|
||||
IsError: false,
|
||||
}
|
||||
l.stepStart = now
|
||||
|
||||
if l.callback != nil {
|
||||
l.callback(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// LogError 记录错误
|
||||
func (l *AuthLogger) LogError(step AuthStep, format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
entry := AuthLogEntry{
|
||||
Step: step,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Duration: now.Sub(l.stepStart),
|
||||
Timestamp: now,
|
||||
IsError: true,
|
||||
}
|
||||
l.stepStart = now
|
||||
|
||||
if l.callback != nil {
|
||||
l.callback(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// TotalDuration 获取总耗时
|
||||
func (l *AuthLogger) TotalDuration() time.Duration {
|
||||
return time.Since(l.startTime)
|
||||
}
|
||||
|
||||
// StepName 获取步骤中文名称
|
||||
func StepName(step AuthStep) string {
|
||||
names := map[AuthStep]string{
|
||||
StepBrowserStart: "启动浏览器",
|
||||
StepNavigate: "访问授权页",
|
||||
StepInputEmail: "输入邮箱",
|
||||
StepSubmitEmail: "提交邮箱",
|
||||
StepInputPassword: "输入密码",
|
||||
StepSubmitPassword: "提交密码",
|
||||
StepSelectWorkspace: "选择工作区",
|
||||
StepConsent: "授权同意",
|
||||
StepWaitCallback: "等待回调",
|
||||
StepExtractCode: "提取授权码",
|
||||
StepComplete: "完成授权",
|
||||
}
|
||||
if name, ok := names[step]; ok {
|
||||
return name
|
||||
}
|
||||
return string(step)
|
||||
}
|
||||
@@ -16,6 +16,24 @@ import (
|
||||
|
||||
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
|
||||
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
return CompleteWithChromedpLogged(authURL, email, password, teamID, headless, proxy, nil)
|
||||
}
|
||||
|
||||
// CompleteWithChromedpLogged 使用 chromedp 完成 S2A OAuth 授权(带日志回调)
|
||||
func CompleteWithChromedpLogged(authURL, email, password, teamID string, headless bool, proxy string, logger *AuthLogger) (string, error) {
|
||||
// 日志辅助函数
|
||||
logStep := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogStep(step, format, args...)
|
||||
}
|
||||
}
|
||||
logError := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogError(step, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
logStep(StepBrowserStart, "正在启动 Chromedp 浏览器...")
|
||||
// 获取随机浏览器配置
|
||||
profile := GetRandomBrowserProfile()
|
||||
|
||||
@@ -132,14 +150,17 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
tasks = append([]chromedp.Action{fetch.Enable().WithHandleAuthRequests(true)}, tasks...)
|
||||
}
|
||||
|
||||
logStep(StepNavigate, "正在访问授权页面...")
|
||||
err := chromedp.Run(ctx, tasks...)
|
||||
if err != nil {
|
||||
logError(StepNavigate, "访问失败: %v", err)
|
||||
return "", fmt.Errorf("访问失败: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if callbackURL != "" {
|
||||
logStep(StepExtractCode, "已捕获授权码回调")
|
||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||
}
|
||||
|
||||
@@ -158,6 +179,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
`input[name="username"]`,
|
||||
}
|
||||
|
||||
logStep(StepInputEmail, "正在查找邮箱输入框...")
|
||||
var emailFilled bool
|
||||
for _, sel := range emailSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
|
||||
@@ -168,12 +190,14 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
)
|
||||
if err == nil {
|
||||
emailFilled = true
|
||||
logStep(StepInputEmail, "已输入邮箱")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !emailFilled {
|
||||
logError(StepInputEmail, "未找到邮箱输入框")
|
||||
return "", fmt.Errorf("未找到邮箱输入框")
|
||||
}
|
||||
|
||||
@@ -186,6 +210,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
`input[type="submit"]`,
|
||||
}
|
||||
|
||||
logStep(StepSubmitEmail, "正在提交邮箱...")
|
||||
for _, sel := range buttonSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
@@ -196,14 +221,17 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
if callbackURL != "" {
|
||||
logStep(StepExtractCode, "已捕获授权码回调")
|
||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||
}
|
||||
|
||||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||||
if strings.Contains(currentURL, "code=") {
|
||||
logStep(StepExtractCode, "已获取授权码")
|
||||
return ExtractCodeFromCallbackURL(currentURL), nil
|
||||
}
|
||||
|
||||
logStep(StepInputPassword, "正在查找密码输入框...")
|
||||
passwordSelectors := []string{
|
||||
`input[name="current-password"]`,
|
||||
`input[name="password"]`,
|
||||
@@ -220,17 +248,20 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
)
|
||||
if err == nil {
|
||||
passwordFilled = true
|
||||
logStep(StepInputPassword, "已输入密码")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !passwordFilled {
|
||||
logError(StepInputPassword, "未找到密码输入框")
|
||||
return "", fmt.Errorf("未找到密码输入框")
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
logStep(StepSubmitPassword, "正在提交密码...")
|
||||
for _, sel := range buttonSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
@@ -238,6 +269,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
}
|
||||
}
|
||||
|
||||
logStep(StepWaitCallback, "等待授权回调...")
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
@@ -248,10 +280,12 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
var url string
|
||||
if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil {
|
||||
if strings.Contains(url, "code=") {
|
||||
logStep(StepExtractCode, "已获取授权码")
|
||||
return ExtractCodeFromCallbackURL(url), nil
|
||||
}
|
||||
|
||||
if strings.Contains(url, "consent") {
|
||||
logStep(StepConsent, "正在处理授权同意页面...")
|
||||
for _, sel := range buttonSelectors {
|
||||
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
|
||||
if err == nil {
|
||||
@@ -262,6 +296,7 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
}
|
||||
|
||||
if strings.Contains(url, "authorize") && teamID != "" {
|
||||
logStep(StepSelectWorkspace, "正在选择工作区: %s", teamID)
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery),
|
||||
)
|
||||
@@ -270,8 +305,10 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
}
|
||||
|
||||
if callbackURL != "" {
|
||||
logStep(StepComplete, "授权完成")
|
||||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||||
}
|
||||
|
||||
logError(StepWaitCallback, "授权超时")
|
||||
return "", fmt.Errorf("授权超时")
|
||||
}
|
||||
|
||||
@@ -134,6 +134,23 @@ func (r *RodAuth) Close() {
|
||||
|
||||
// CompleteOAuth 完成 OAuth 授权
|
||||
func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) {
|
||||
return r.CompleteOAuthLogged(authURL, email, password, teamID, nil)
|
||||
}
|
||||
|
||||
// CompleteOAuthLogged 完成 OAuth 授权(带日志回调)
|
||||
func (r *RodAuth) CompleteOAuthLogged(authURL, email, password, teamID string, logger *AuthLogger) (string, error) {
|
||||
// 日志辅助函数
|
||||
logStep := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogStep(step, format, args...)
|
||||
}
|
||||
}
|
||||
logError := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogError(step, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle proxy auth (407) in headless mode.
|
||||
// When Fetch domain is enabled without patterns, requests will be paused and must be continued.
|
||||
// 只在代理需要认证时才启用 Fetch 域
|
||||
@@ -194,24 +211,31 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
||||
// 增加超时时间到 90 秒
|
||||
page = page.Timeout(90 * time.Second)
|
||||
|
||||
logStep(StepNavigate, "正在访问授权页面...")
|
||||
if err := page.Navigate(authURL); err != nil {
|
||||
logError(StepNavigate, "访问失败: %v", err)
|
||||
return "", fmt.Errorf("访问授权URL失败: %v", err)
|
||||
}
|
||||
|
||||
page.MustWaitDOMStable()
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
logStep(StepExtractCode, "已捕获授权码回调")
|
||||
return code, nil
|
||||
}
|
||||
|
||||
logStep(StepInputEmail, "正在查找邮箱输入框...")
|
||||
emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']")
|
||||
if err != nil {
|
||||
logError(StepInputEmail, "未找到邮箱输入框")
|
||||
return "", fmt.Errorf("未找到邮箱输入框")
|
||||
}
|
||||
|
||||
emailInput.MustSelectAllText().MustInput(email)
|
||||
logStep(StepInputEmail, "已输入邮箱")
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
logStep(StepSubmitEmail, "正在提交邮箱...")
|
||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
||||
btn.MustClick()
|
||||
}
|
||||
@@ -219,25 +243,32 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
logStep(StepExtractCode, "已获取授权码")
|
||||
return code, nil
|
||||
}
|
||||
|
||||
logStep(StepInputPassword, "正在查找密码输入框...")
|
||||
passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']")
|
||||
if err != nil {
|
||||
logError(StepInputPassword, "未找到密码输入框")
|
||||
return "", fmt.Errorf("未找到密码输入框")
|
||||
}
|
||||
|
||||
passwordInput.MustSelectAllText().MustInput(password)
|
||||
logStep(StepInputPassword, "已输入密码")
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
logStep(StepSubmitPassword, "正在提交密码...")
|
||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
|
||||
btn.MustClick()
|
||||
}
|
||||
|
||||
logStep(StepWaitCallback, "等待授权回调...")
|
||||
for i := 0; i < 66; i++ {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
logStep(StepComplete, "授权完成")
|
||||
return code, nil
|
||||
}
|
||||
|
||||
@@ -245,12 +276,14 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
||||
currentURL := info.URL
|
||||
|
||||
if strings.Contains(currentURL, "consent") {
|
||||
logStep(StepConsent, "正在处理授权同意页面...")
|
||||
if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil {
|
||||
btn.Click(proto.InputMouseButtonLeft, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(currentURL, "authorize") && teamID != "" {
|
||||
logStep(StepSelectWorkspace, "正在选择工作区: %s", teamID)
|
||||
wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID)
|
||||
if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil {
|
||||
wsBtn.Click(proto.InputMouseButtonLeft, 1)
|
||||
@@ -258,6 +291,7 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
||||
}
|
||||
}
|
||||
|
||||
logError(StepWaitCallback, "授权超时")
|
||||
return "", fmt.Errorf("授权超时")
|
||||
}
|
||||
|
||||
@@ -275,13 +309,33 @@ func (r *RodAuth) checkForCode(page *rod.Page) string {
|
||||
|
||||
// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权
|
||||
func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
return CompleteWithRodLogged(authURL, email, password, teamID, headless, proxy, nil)
|
||||
}
|
||||
|
||||
// CompleteWithRodLogged 使用 Rod + Stealth 完成 S2A 授权(带日志回调)
|
||||
func CompleteWithRodLogged(authURL, email, password, teamID string, headless bool, proxy string, logger *AuthLogger) (string, error) {
|
||||
// 日志辅助函数
|
||||
logStep := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogStep(step, format, args...)
|
||||
}
|
||||
}
|
||||
logError := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogError(step, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
logStep(StepBrowserStart, "正在启动 Rod 浏览器...")
|
||||
auth, err := NewRodAuth(headless, proxy)
|
||||
if err != nil {
|
||||
logError(StepBrowserStart, "启动失败: %v", err)
|
||||
return "", err
|
||||
}
|
||||
defer auth.Close()
|
||||
|
||||
return auth.CompleteOAuth(authURL, email, password, teamID)
|
||||
logStep(StepBrowserStart, "浏览器启动成功")
|
||||
return auth.CompleteOAuthLogged(authURL, email, password, teamID, logger)
|
||||
}
|
||||
|
||||
// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名)
|
||||
|
||||
Reference in New Issue
Block a user