Files
ufile/cmd/ufile-cli/auth.go
T

176 lines
5.4 KiB
Go

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"
}