package main import ( "bufio" "context" "errors" "flag" "fmt" "io" "net/http" "os" "strings" "time" "golang.org/x/term" ) func runAuth(ctx context.Context, args []string, configPath string, config configFile, client *apiClient, ioStreams streams) *commandError { if len(args) == 0 { return usageError("auth 需要子命令 login、whoami 或 logout;运行 ufile-cli help auth") } switch args[0] { case "login": return authLogin(ctx, args[1:], configPath, config, client, ioStreams) case "whoami": return authWhoami(ctx, args[1:], client, ioStreams) case "logout": return authLogout(ctx, args[1:], configPath, config, client, ioStreams) case "help", "--help": _, _ = io.WriteString(ioStreams.out, authHelp) return nil default: return usageError(fmt.Sprintf("未知 auth 子命令 %q;运行 ufile-cli help auth", args[0])) } } func authLogin(ctx context.Context, args []string, configPath string, config configFile, client *apiClient, ioStreams streams) *commandError { var username string var passwordStdin, help bool fs := flag.NewFlagSet("auth login", flag.ContinueOnError) fs.SetOutput(io.Discard) fs.StringVar(&username, "U", "", "用户名") fs.StringVar(&username, "username", "", "用户名") fs.BoolVar(&passwordStdin, "password-stdin", false, "从标准输入读取密码") fs.BoolVar(&help, "help", false, "显示帮助") if err := fs.Parse(args); err != nil { return usageError(err.Error()) } if help { _, _ = io.WriteString(ioStreams.out, authHelp) return nil } if fs.NArg() != 0 { return usageError("auth login 不接受位置参数;运行 ufile-cli help auth") } username = firstNonEmpty(username, os.Getenv("UFILE_USERNAME")) if username == "" { return usageError("缺少用户名;请使用 -U/--username 或设置 UFILE_USERNAME") } password, err := readPassword(passwordStdin, ioStreams) if err != nil { return usageError(err.Error()) } var user userResponse response, err := client.doJSON(ctx, http.MethodPost, "/api/auth/login", map[string]string{"username": username, "password": password}, &user) if err != nil { return classifyError(err) } var session string var expiresAt time.Time for _, cookie := range response.Cookies() { if cookie.Name == sessionCookieName { session = cookie.Value expiresAt = cookie.Expires break } } if session == "" { return networkError("登录成功,但服务端没有返回会话 Cookie", nil) } config.Host = client.host config.Session = session config.ExpiresAt = expiresAt if err := saveConfig(configPath, config); err != nil { return localError("保存会话配置失败", err) } result := struct { Host string `json:"host"` ExpiresAt time.Time `json:"expiresAt"` User userResponse `json:"user"` }{Host: client.host, ExpiresAt: expiresAt, User: user} writeJSON(ioStreams.out, result) return nil } func authWhoami(ctx context.Context, args []string, client *apiClient, ioStreams streams) *commandError { if isHelpOnly(args) { _, _ = io.WriteString(ioStreams.out, authHelp) return nil } if len(args) != 0 { return usageError("auth whoami 不接受参数") } if client.session == "" { return &commandError{Code: "not_logged_in", Message: "本地没有会话;请先运行 auth login", ExitCode: exitAuth} } var user userResponse if _, err := client.doJSON(ctx, http.MethodGet, "/api/auth/me", nil, &user); err != nil { return classifyError(err) } writeJSON(ioStreams.out, user) return nil } func authLogout(ctx context.Context, args []string, configPath string, config configFile, client *apiClient, ioStreams streams) *commandError { if isHelpOnly(args) { _, _ = io.WriteString(ioStreams.out, authHelp) return nil } if len(args) != 0 { return usageError("auth logout 不接受参数") } if client.session == "" { return &commandError{Code: "not_logged_in", Message: "本地没有会话", ExitCode: exitAuth} } if _, err := client.doJSON(ctx, http.MethodPost, "/api/auth/logout", nil, nil); err != nil { var remoteErr *apiError if !errors.As(err, &remoteErr) || remoteErr.Status != http.StatusUnauthorized { return classifyError(err) } } config.Host = client.host config.Session = "" config.ExpiresAt = time.Time{} if err := saveConfig(configPath, config); err != nil { return localError("清除本地会话失败", err) } writeJSON(ioStreams.out, map[string]bool{"loggedOut": true}) return nil } func readPassword(fromStdin bool, ioStreams streams) (string, error) { if fromStdin { line, err := bufio.NewReader(ioStreams.in).ReadString('\n') if err != nil && len(line) == 0 { return "", fmt.Errorf("从标准输入读取密码失败: %w", err) } password := strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r") if password == "" { return "", fmt.Errorf("密码不能为空") } return password, nil } if password := os.Getenv("UFILE_PASSWORD"); password != "" { return password, nil } if ioStreams.stdin == nil || !term.IsTerminal(int(ioStreams.stdin.Fd())) { return "", fmt.Errorf("非交互环境需要使用 --password-stdin 或 UFILE_PASSWORD") } fmt.Fprint(ioStreams.errOut, "密码: ") password, err := term.ReadPassword(int(ioStreams.stdin.Fd())) fmt.Fprintln(ioStreams.errOut) if err != nil { return "", fmt.Errorf("读取密码失败: %w", err) } if len(password) == 0 { return "", fmt.Errorf("密码不能为空") } return string(password), nil } func isHelpOnly(args []string) bool { return len(args) == 1 && args[0] == "--help" }