176 lines
5.4 KiB
Go
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"
|
|
}
|