package main import ( "context" "errors" "flag" "fmt" "log" "os" "strings" "time" "gitea.gangary.cn/gary/Hugs-Proxy/internal/config" "gitea.gangary.cn/gary/Hugs-Proxy/internal/db" ) func main() { cfg := config.Load() if len(os.Args) < 2 { printUsage() os.Exit(1) } sub := os.Args[1] switch sub { case "create-user": handleCreateUser(cfg, os.Args[2:]) case "issue-token": handleIssueToken(cfg, os.Args[2:]) case "disable-token": handleDisableToken(cfg, os.Args[2:]) case "list-users": handleListUsers(cfg, os.Args[2:]) case "list-tokens": handleListTokens(cfg, os.Args[2:]) default: printUsage() os.Exit(1) } } func handleCreateUser(cfg config.Config, args []string) { fs := flag.NewFlagSet("create-user", flag.ExitOnError) dbPath := fs.String("db", cfg.DBPath, "SQLite DB path") name := fs.String("name", "", "username") _ = fs.Parse(args) if strings.TrimSpace(*name) == "" { log.Fatal("--name is required") } store := openStore(*dbPath, cfg.BusyTimeoutMS) defer store.Close() u, err := store.CreateUser(context.Background(), strings.TrimSpace(*name)) if err != nil { log.Fatal(err) } fmt.Printf("created user: id=%d username=%s created_at=%s\n", u.ID, u.Username, u.CreatedAt.Format(time.RFC3339)) } func handleIssueToken(cfg config.Config, args []string) { fs := flag.NewFlagSet("issue-token", flag.ExitOnError) dbPath := fs.String("db", cfg.DBPath, "SQLite DB path") username := fs.String("user", "", "username") expires := fs.String("expires", "", "expires duration like 7d/30d or RFC3339 time") uses := fs.Int("uses", 1, "max usable times for this token, must be > 0") _ = fs.Parse(args) if strings.TrimSpace(*username) == "" { log.Fatal("--user is required") } if *uses <= 0 { log.Fatal("--uses must be > 0") } store := openStore(*dbPath, cfg.BusyTimeoutMS) defer store.Close() ctx := context.Background() u, err := store.GetUserByName(ctx, strings.TrimSpace(*username)) if err != nil { if errors.Is(err, db.ErrNotFound) { log.Fatalf("user %q not found", *username) } log.Fatal(err) } expiresAt, err := parseExpiresAt(*expires, cfg.DefaultTokenTTL) if err != nil { log.Fatal(err) } tok, err := store.IssueToken(ctx, u.ID, expiresAt, *uses) if err != nil { log.Fatal(err) } fmt.Printf("issued token: id=%d user=%s token=%s expires_at=%s max_uses=%d\n", tok.ID, u.Username, tok.Token, tok.ExpiresAt.Format(time.RFC3339), tok.MaxUses) } func handleDisableToken(cfg config.Config, args []string) { fs := flag.NewFlagSet("disable-token", flag.ExitOnError) dbPath := fs.String("db", cfg.DBPath, "SQLite DB path") token := fs.String("token", "", "token value") _ = fs.Parse(args) if strings.TrimSpace(*token) == "" { log.Fatal("--token is required") } store := openStore(*dbPath, cfg.BusyTimeoutMS) defer store.Close() err := store.DisableToken(context.Background(), strings.TrimSpace(*token)) if err != nil { if errors.Is(err, db.ErrNotFound) { log.Fatalf("token not found") } log.Fatal(err) } fmt.Println("token disabled") } func handleListUsers(cfg config.Config, args []string) { fs := flag.NewFlagSet("list-users", flag.ExitOnError) dbPath := fs.String("db", cfg.DBPath, "SQLite DB path") _ = fs.Parse(args) store := openStore(*dbPath, cfg.BusyTimeoutMS) defer store.Close() users, err := store.ListUsers(context.Background()) if err != nil { log.Fatal(err) } fmt.Println("id\tusername\tcreated_at") for _, u := range users { fmt.Printf("%d\t%s\t%s\n", u.ID, u.Username, u.CreatedAt.Format(time.RFC3339)) } } func handleListTokens(cfg config.Config, args []string) { fs := flag.NewFlagSet("list-tokens", flag.ExitOnError) dbPath := fs.String("db", cfg.DBPath, "SQLite DB path") _ = fs.Parse(args) store := openStore(*dbPath, cfg.BusyTimeoutMS) defer store.Close() tokens, err := store.ListTokens(context.Background()) if err != nil { log.Fatal(err) } fmt.Println("id\tuser\tdisabled\tused/max\texpires_at\tcreated_at\ttoken") for _, t := range tokens { maxUsesDisplay := fmt.Sprintf("%d", t.MaxUses) if t.MaxUses == 0 { maxUsesDisplay = "unlimited" } fmt.Printf("%d\t%s\t%t\t%d/%s\t%s\t%s\t%s\n", t.ID, t.Username, t.Disabled, t.UsedCount, maxUsesDisplay, t.ExpiresAt.Format(time.RFC3339), t.CreatedAt.Format(time.RFC3339), t.Token) } } func parseExpiresAt(raw string, defaultTTL time.Duration) (time.Time, error) { now := time.Now().UTC() clean := strings.TrimSpace(raw) if clean == "" { return now.Add(defaultTTL), nil } if t, err := time.Parse(time.RFC3339, clean); err == nil { return t.UTC(), nil } d, err := config.ParseExpiryDuration(clean) if err != nil { return time.Time{}, fmt.Errorf("invalid --expires value: %w", err) } return now.Add(d), nil } func openStore(dbPath string, busyTimeoutMS int) *db.Store { store, err := db.NewStore(strings.TrimSpace(dbPath), busyTimeoutMS) if err != nil { log.Fatal(err) } return store } func printUsage() { fmt.Println(`tokenctl usage: tokenctl create-user --name [--db ] tokenctl issue-token --user [--expires <7d|30d|RFC3339>] [--uses ] [--db ] tokenctl disable-token --token [--db ] tokenctl list-users [--db ] tokenctl list-tokens [--db ]`) }