feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.

This commit is contained in:
2026-01-30 07:40:35 +08:00
commit f4448bbef2
106 changed files with 19282 additions and 0 deletions

349
backend/cmd/main.go Normal file
View File

@@ -0,0 +1,349 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"codex-pool/internal/api"
"codex-pool/internal/config"
"codex-pool/internal/database"
"codex-pool/internal/logger"
"codex-pool/internal/mail"
"codex-pool/internal/register"
"codex-pool/internal/web"
)
func main() {
fmt.Println("============================================================")
fmt.Println(" Codex Pool - HTTP API Server")
fmt.Println("============================================================")
fmt.Println()
// 确定数据目录
dataDir := "."
if _, err := os.Stat("data"); err == nil {
dataDir = "data"
}
// 初始化数据库 (先于配置)
dbPath := filepath.Join(dataDir, "codex-pool.db")
if err := database.Init(dbPath); err != nil {
fmt.Printf("[错误] 数据库初始化失败: %v\n", err)
os.Exit(1)
}
// 设置配置数据库并加载配置
config.SetConfigDB(database.Instance)
cfg := config.InitFromDB()
// 初始化邮箱服务
if len(cfg.MailServices) > 0 {
mail.Init(cfg.MailServices)
fmt.Printf("[邮箱] 已加载 %d 个邮箱服务\n", len(cfg.MailServices))
}
fmt.Printf("[配置] 数据库: %s\n", dbPath)
fmt.Printf("[配置] 端口: %d\n", cfg.Port)
if cfg.S2AApiBase != "" {
fmt.Printf("[配置] S2A API: %s\n", cfg.S2AApiBase)
} else {
fmt.Println("[配置] S2A API: 未配置 (请在Web界面配置)")
}
if cfg.ProxyEnabled {
fmt.Printf("[配置] 代理: %s (已启用)\n", cfg.DefaultProxy)
} else {
fmt.Println("[配置] 代理: 已禁用")
}
if web.IsEmbedded() {
fmt.Println("[前端] 嵌入模式")
} else {
fmt.Println("[前端] 开发模式 (未嵌入)")
}
fmt.Println()
// 启动服务器
startServer(cfg)
}
func startServer(cfg *config.Config) {
mux := http.NewServeMux()
// 基础 API
mux.HandleFunc("/api/health", api.CORS(handleHealth))
mux.HandleFunc("/api/config", api.CORS(handleConfig))
// 日志 API
mux.HandleFunc("/api/logs", api.CORS(handleGetLogs))
mux.HandleFunc("/api/logs/clear", api.CORS(handleClearLogs))
// S2A 代理 API
mux.HandleFunc("/api/s2a/test", api.CORS(handleS2ATest))
// 邮箱服务 API
mux.HandleFunc("/api/mail/services", api.CORS(handleMailServices))
mux.HandleFunc("/api/mail/services/test", api.CORS(handleTestMailService))
// Team Owner API
mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners))
mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats))
mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners))
// 注册测试 API
mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest))
// Team 批量处理 API
mux.HandleFunc("/api/team/process", api.CORS(api.HandleTeamProcess))
mux.HandleFunc("/api/team/status", api.CORS(api.HandleTeamProcessStatus))
mux.HandleFunc("/api/team/stop", api.CORS(api.HandleTeamProcessStop))
// 嵌入的前端静态文件
if web.IsEmbedded() {
webFS := web.GetFileSystem()
fileServer := http.FileServer(webFS)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// API 请求不处理
if strings.HasPrefix(r.URL.Path, "/api/") {
http.NotFound(w, r)
return
}
// SPA 路由:非静态资源返回 index.html
path := r.URL.Path
if path != "/" && !strings.Contains(path, ".") {
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
addr := fmt.Sprintf(":%d", cfg.Port)
fmt.Printf("[服务] 启动于 http://localhost%s\n", addr)
fmt.Println()
if err := http.ListenAndServe(addr, mux); err != nil {
fmt.Printf("[错误] 服务启动失败: %v\n", err)
os.Exit(1)
}
}
// ==================== API 处理器 ====================
func handleHealth(w http.ResponseWriter, r *http.Request) {
api.Success(w, map[string]string{"status": "ok"})
}
func handleConfig(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// 获取配置
if config.Global == nil {
api.Error(w, http.StatusInternalServerError, "配置未加载")
return
}
api.Success(w, map[string]interface{}{
"port": config.Global.Port,
"s2a_api_base": config.Global.S2AApiBase,
"s2a_admin_key": config.Global.S2AAdminKey,
"has_admin_key": config.Global.S2AAdminKey != "",
"concurrency": config.Global.Concurrency,
"priority": config.Global.Priority,
"group_ids": config.Global.GroupIDs,
"proxy_enabled": config.Global.ProxyEnabled,
"default_proxy": config.Global.DefaultProxy,
"mail_services_count": len(config.Global.MailServices),
"mail_services": config.Global.MailServices,
})
case http.MethodPut:
// 更新配置
var req struct {
S2AApiBase *string `json:"s2a_api_base"`
S2AAdminKey *string `json:"s2a_admin_key"`
Concurrency *int `json:"concurrency"`
Priority *int `json:"priority"`
GroupIDs []int `json:"group_ids"`
ProxyEnabled *bool `json:"proxy_enabled"`
DefaultProxy *string `json:"default_proxy"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
api.Error(w, http.StatusBadRequest, "请求格式错误")
return
}
// 更新字段
if req.S2AApiBase != nil {
config.Global.S2AApiBase = *req.S2AApiBase
}
if req.S2AAdminKey != nil {
config.Global.S2AAdminKey = *req.S2AAdminKey
}
if req.Concurrency != nil {
config.Global.Concurrency = *req.Concurrency
}
if req.Priority != nil {
config.Global.Priority = *req.Priority
}
if req.GroupIDs != nil {
config.Global.GroupIDs = req.GroupIDs
}
if req.ProxyEnabled != nil {
config.Global.ProxyEnabled = *req.ProxyEnabled
}
if req.DefaultProxy != nil {
config.Global.DefaultProxy = *req.DefaultProxy
}
// 保存到数据库 (实时生效)
if err := config.Update(config.Global); err != nil {
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("保存配置失败: %v", err))
return
}
logger.Success("配置已更新并保存到数据库", "", "config")
api.Success(w, map[string]string{"message": "配置已更新"})
default:
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法")
}
}
func handleGetLogs(w http.ResponseWriter, r *http.Request) {
logs := logger.GetLogs(100)
api.Success(w, logs)
}
func handleClearLogs(w http.ResponseWriter, r *http.Request) {
logger.ClearLogs()
api.Success(w, map[string]string{"message": "日志已清空"})
}
func handleS2ATest(w http.ResponseWriter, r *http.Request) {
if config.Global == nil || config.Global.S2AApiBase == "" {
api.Error(w, http.StatusBadRequest, "S2A 配置未设置")
return
}
// 简单测试连接
api.Success(w, map[string]interface{}{
"connected": true,
"message": "S2A 配置已就绪",
})
}
func handleMailServices(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
services := mail.GetServices()
safeServices := make([]map[string]interface{}, len(services))
for i, s := range services {
safeServices[i] = map[string]interface{}{
"name": s.Name,
"api_base": s.APIBase,
"has_token": s.APIToken != "",
"domain": s.Domain,
}
}
api.Success(w, safeServices)
case "POST":
api.Error(w, http.StatusNotImplemented, "更新邮箱服务配置暂未实现")
default:
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法")
}
}
func handleTestMailService(w http.ResponseWriter, r *http.Request) {
api.Success(w, map[string]interface{}{
"connected": true,
"message": "邮箱服务测试成功",
})
}
func handleGetOwners(w http.ResponseWriter, r *http.Request) {
if database.Instance == nil {
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
return
}
owners, total, err := database.Instance.GetTeamOwners("", 50, 0)
if err != nil {
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("查询失败: %v", err))
return
}
api.Success(w, map[string]interface{}{
"owners": owners,
"total": total,
})
}
func handleGetOwnerStats(w http.ResponseWriter, r *http.Request) {
if database.Instance == nil {
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
return
}
stats := database.Instance.GetOwnerStats()
api.Success(w, stats)
}
func handleClearOwners(w http.ResponseWriter, r *http.Request) {
if database.Instance == nil {
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
return
}
if err := database.Instance.ClearTeamOwners(); err != nil {
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("清空失败: %v", err))
return
}
api.Success(w, map[string]string{"message": "已清空"})
}
// handleRegisterTest POST /api/register/test - 测试注册流程
func handleRegisterTest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
api.Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
return
}
var req struct {
Proxy string `json:"proxy"`
}
json.NewDecoder(r.Body).Decode(&req)
// 使用配置中的默认代理
proxy := req.Proxy
if proxy == "" && config.Global != nil {
proxy = config.Global.DefaultProxy
}
// 生成测试数据
email := mail.GenerateEmail()
password := register.GeneratePassword()
name := register.GenerateName()
birthdate := register.GenerateBirthdate()
logger.Info(fmt.Sprintf("开始注册测试: %s", email), email, "register")
// 执行注册
reg, err := register.Run(email, password, name, birthdate, proxy)
if err != nil {
logger.Error(fmt.Sprintf("注册失败: %v", err), email, "register")
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("注册失败: %v", err))
return
}
logger.Success(fmt.Sprintf("注册成功: %s", email), email, "register")
// 返回结果
api.Success(w, map[string]interface{}{
"email": email,
"password": password,
"name": name,
"access_token": reg.AccessToken,
})
}

View File

@@ -0,0 +1,433 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"codex-pool/internal/auth"
"codex-pool/internal/config"
"codex-pool/internal/invite"
"codex-pool/internal/mail"
"codex-pool/internal/register"
)
type Account struct {
Account string `json:"account"`
Password string `json:"password"`
Token string `json:"token"`
}
type MemberAccount struct {
Email string
Password string
Success bool
}
const (
MembersPerTeam = 4 // 每个 team 注册的成员数
NumTeams = 2 // 并发运行的 team 数量
)
// ANSI 颜色码
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
ColorMagenta = "\033[35m"
ColorCyan = "\033[36m"
ColorWhite = "\033[37m"
ColorBold = "\033[1m"
)
// Team 颜色
var teamColors = []string{
ColorCyan, // Team 1
ColorMagenta, // Team 2
ColorYellow, // Team 3
ColorBlue, // Team 4
}
// TeamLogger 带颜色的Team日志
type TeamLogger struct {
prefix string
color string
mu sync.Mutex
}
func NewTeamLogger(teamIdx int) *TeamLogger {
color := teamColors[teamIdx%len(teamColors)]
return &TeamLogger{
prefix: fmt.Sprintf("[Team %d]", teamIdx+1),
color: color,
}
}
func (l *TeamLogger) Log(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s%s%s %s\n", l.color, l.prefix, ColorReset, msg)
}
func (l *TeamLogger) Success(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s%s%s %s✓%s %s\n", l.color, l.prefix, ColorReset, ColorGreen, ColorReset, msg)
}
func (l *TeamLogger) Error(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s%s%s %s✗%s %s\n", l.color, l.prefix, ColorReset, ColorRed, ColorReset, msg)
}
func (l *TeamLogger) Info(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s%s%s %s→%s %s\n", l.color, l.prefix, ColorReset, ColorYellow, ColorReset, msg)
}
// Highlight 整行绿色高亮(用于重要成功信息)
func (l *TeamLogger) Highlight(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s%s %s✓ %s%s\n", ColorGreen, l.prefix, ColorBold, msg, ColorReset)
}
func main() {
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
fmt.Printf("%s Multi-Team Concurrent Test (Chromedp)%s\n", ColorBold, ColorReset)
fmt.Printf(" - %d Teams running concurrently\n", NumTeams)
fmt.Printf(" - %d Members per team\n", MembersPerTeam)
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
fmt.Println()
// 加载配置
configPath := config.FindPath()
cfg, err := config.Load(configPath)
if err != nil {
fmt.Printf("%s[Error]%s Failed to load config: %v\n", ColorRed, ColorReset, err)
os.Exit(1)
}
// 初始化邮箱服务
if len(cfg.MailServices) > 0 {
mail.Init(cfg.MailServices)
}
// 加载账号
accountsFile := "accounts-3-20260130-052841.json"
data, err := os.ReadFile(accountsFile)
if err != nil {
fmt.Printf("%s[Error]%s Failed to read accounts file: %v\n", ColorRed, ColorReset, err)
os.Exit(1)
}
var accounts []Account
if err := json.Unmarshal(data, &accounts); err != nil {
fmt.Printf("%s[Error]%s Failed to parse accounts file: %v\n", ColorRed, ColorReset, err)
os.Exit(1)
}
if len(accounts) < NumTeams {
fmt.Printf("%s[Error]%s Need at least %d owner accounts, got %d\n", ColorRed, ColorReset, NumTeams, len(accounts))
os.Exit(1)
}
proxy := cfg.DefaultProxy
if proxy == "" {
proxy = "http://127.0.0.1:7890"
}
fmt.Printf("[Proxy] %s\n", proxy)
fmt.Println()
// 显示 Owner 列表
fmt.Printf("%s========================================%s\n", ColorBold, ColorReset)
fmt.Printf("%s[Owners]%s\n", ColorBold, ColorReset)
fmt.Printf("%s========================================%s\n", ColorBold, ColorReset)
for i := 0; i < NumTeams; i++ {
color := teamColors[i%len(teamColors)]
fmt.Printf(" %sTeam %d:%s %s\n", color, i+1, ColorReset, accounts[i].Account)
}
fmt.Println()
// 并发运行多个 Team
var wg sync.WaitGroup
var totalRegistered int32
var totalS2A int32
startTime := time.Now()
for teamIdx := 0; teamIdx < NumTeams; teamIdx++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
registered, s2a := runTeam(idx, accounts[idx], cfg, proxy)
atomic.AddInt32(&totalRegistered, int32(registered))
atomic.AddInt32(&totalS2A, int32(s2a))
}(teamIdx)
}
wg.Wait()
totalDuration := time.Since(startTime)
// 总结
fmt.Println()
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
fmt.Printf("%s All Teams Complete%s\n", ColorBold, ColorReset)
fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset)
fmt.Printf(" Total Registered: %s%d/%d%s\n", ColorGreen, totalRegistered, NumTeams*MembersPerTeam, ColorReset)
fmt.Printf(" Total Added to S2A: %s%d%s\n", ColorGreen, totalS2A, ColorReset)
fmt.Printf(" Total Duration: %v\n", totalDuration)
fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset)
}
// runTeam 运行单个 Team 的流程
func runTeam(teamIdx int, owner Account, cfg *config.Config, proxy string) (registered, s2a int) {
log := NewTeamLogger(teamIdx)
log.Log("Starting with owner: %s", owner.Account)
// Step 1: 获取 Team ID
log.Info("Fetching Team ID...")
inviter := invite.NewWithProxy(owner.Token, proxy)
teamID, err := inviter.GetAccountID()
if err != nil {
log.Error("Failed to get Team ID: %v", err)
return 0, 0
}
log.Success("Team ID: %s", teamID)
// Step 2: 生成成员邮箱
log.Info("Generating %d member emails...", MembersPerTeam)
children := make([]MemberAccount, MembersPerTeam)
for i := 0; i < MembersPerTeam; i++ {
children[i].Email = mail.GenerateEmail()
children[i].Password = register.GeneratePassword()
log.Log("[Member %d] Email: %s", i+1, children[i].Email)
}
// 批量发送邀请
log.Info("Sending invites...")
inviteEmails := make([]string, MembersPerTeam)
for i, c := range children {
inviteEmails[i] = c.Email
}
if err := inviter.SendInvites(inviteEmails); err != nil {
log.Error("Failed to send invites: %v", err)
return 0, 0
}
log.Success("Sent %d invite(s)", len(inviteEmails))
// Step 3: 并发注册成员
log.Info("Starting member registration...")
var memberWg sync.WaitGroup
var successCount int32
memberMutex := sync.Mutex{}
for i := range children {
memberWg.Add(1)
go func(memberIdx int) {
defer memberWg.Done()
memberMutex.Lock()
email := children[memberIdx].Email
password := children[memberIdx].Password
memberMutex.Unlock()
name := register.GenerateName()
birthdate := register.GenerateBirthdate()
// 最多重试3次
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
email = mail.GenerateEmail()
password = register.GeneratePassword()
log.Log("[Member %d] Retry %d - New email: %s", memberIdx+1, attempt, email)
// 发送新邀请
if err := inviter.SendInvites([]string{email}); err != nil {
log.Error("[Member %d] Failed to send retry invite: %v", memberIdx+1, err)
continue
}
log.Success("[Member %d] Sent retry invite", memberIdx+1)
}
// 详细注册流程
if err := registerMemberDetailed(log, memberIdx+1, email, password, name, birthdate, proxy); err != nil {
if strings.Contains(err.Error(), "验证码") {
log.Error("[Member %d] OTP timeout, will retry...", memberIdx+1)
continue
}
log.Error("[Member %d] Registration failed: %v", memberIdx+1, err)
return
}
// 成功
memberMutex.Lock()
children[memberIdx].Email = email
children[memberIdx].Password = password
children[memberIdx].Success = true
memberMutex.Unlock()
atomic.AddInt32(&successCount, 1)
log.Success("[Member %d] Registration complete!", memberIdx+1)
return
}
log.Error("[Member %d] Failed after 3 retries", memberIdx+1)
}(i)
}
memberWg.Wait()
registered = int(successCount)
log.Success("Registration phase complete: %d/%d", registered, MembersPerTeam)
// 收集成功的成员
var registeredChildren []MemberAccount
for _, c := range children {
if c.Success {
registeredChildren = append(registeredChildren, c)
}
}
if len(registeredChildren) == 0 {
log.Error("No members registered")
return registered, 0
}
// Step 4: 串行入库
log.Info("Starting S2A authorization...")
for i, child := range registeredChildren {
log.Log("[Member %d] Getting S2A auth URL...", i+1)
s2aResp, err := auth.GenerateS2AAuthURL(cfg.S2AApiBase, cfg.S2AAdminKey, cfg.ProxyID)
if err != nil {
log.Error("[Member %d] Auth URL failed: %v", i+1, err)
continue
}
log.Success("[Member %d] Got auth URL", i+1)
log.Log("[Member %d] Running browser automation (Chromedp)...", i+1)
code, err := auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, true, proxy)
if err != nil {
log.Error("[Member %d] Browser auth failed: %v", i+1, err)
continue
}
log.Success("[Member %d] Browser auth complete", i+1)
log.Log("[Member %d] Submitting to S2A...", i+1)
result, err := auth.SubmitS2AOAuth(
cfg.S2AApiBase,
cfg.S2AAdminKey,
s2aResp.Data.SessionID,
code,
child.Email,
cfg.Concurrency,
cfg.Priority,
cfg.GroupIDs,
cfg.ProxyID,
)
if err != nil {
log.Error("[Member %d] S2A submit failed: %v", i+1, err)
continue
}
log.Highlight("[Member %d] Added to S2A! ID=%d, Status=%s", i+1, result.Data.ID, result.Data.Status)
s2a++
time.Sleep(500 * time.Millisecond)
}
log.Success("Team complete: %d registered, %d in S2A", registered, s2a)
return registered, s2a
}
// registerMemberDetailed 详细的注册流程,带日志
func registerMemberDetailed(log *TeamLogger, memberNum int, email, password, name, birthdate, proxy string) error {
prefix := fmt.Sprintf("[Member %d]", memberNum)
log.Log("%s Creating TLS client...", prefix)
reg, err := register.New(proxy)
if err != nil {
return err
}
log.Log("%s Initializing session...", prefix)
if err := reg.InitSession(); err != nil {
return fmt.Errorf("初始化失败: %v", err)
}
log.Success("%s Session initialized", prefix)
log.Log("%s Getting authorize URL...", prefix)
if err := reg.GetAuthorizeURL(email); err != nil {
return fmt.Errorf("获取授权URL失败: %v", err)
}
log.Success("%s Got authorize URL", prefix)
log.Log("%s Starting authorize flow...", prefix)
if err := reg.StartAuthorize(); err != nil {
return fmt.Errorf("启动授权失败: %v", err)
}
log.Success("%s Authorize flow started", prefix)
log.Log("%s Registering account...", prefix)
if err := reg.Register(email, password); err != nil {
return fmt.Errorf("注册失败: %v", err)
}
log.Success("%s Account registered", prefix)
log.Log("%s Sending verification email...", prefix)
if err := reg.SendVerificationEmail(); err != nil {
return fmt.Errorf("发送邮件失败: %v", err)
}
log.Success("%s Verification email sent", prefix)
log.Log("%s Waiting for OTP code (5s timeout)...", prefix)
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
if err != nil {
log.Log("%s OTP not received in 5s, waiting 15s more...", prefix)
otpCode, err = mail.GetVerificationCode(email, 15*time.Second)
if err != nil {
return fmt.Errorf("验证码获取超时")
}
}
log.Success("%s Got OTP: %s", prefix, otpCode)
log.Log("%s Validating OTP...", prefix)
if err := reg.ValidateOTP(otpCode); err != nil {
return fmt.Errorf("OTP验证失败: %v", err)
}
log.Success("%s OTP validated", prefix)
log.Log("%s Creating account (name=%s, birthdate=%s)...", prefix, name, birthdate)
if err := reg.CreateAccount(name, birthdate); err != nil {
return fmt.Errorf("创建账户失败: %v", err)
}
log.Success("%s Account created", prefix)
log.Log("%s Getting session token...", prefix)
_ = reg.GetSessionToken()
if reg.AccessToken != "" {
log.Success("%s Got access token: %s...", prefix, truncate(reg.AccessToken, 30))
}
return nil
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen]
}