f8051ea8c9
Co-authored-by: Copilot <copilot@github.com>
239 lines
6.2 KiB
Go
239 lines
6.2 KiB
Go
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:])
|
|
case "list-usage":
|
|
handleListUsage(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.Token)
|
|
}
|
|
}
|
|
|
|
func handleListUsage(cfg config.Config, args []string) {
|
|
fs := flag.NewFlagSet("list-usage", flag.ExitOnError)
|
|
dbPath := fs.String("db", cfg.DBPath, "SQLite DB path")
|
|
limit := fs.Int("limit", 50, "max rows to show, <=0 means all")
|
|
_ = fs.Parse(args)
|
|
|
|
store := openStore(*dbPath, cfg.BusyTimeoutMS)
|
|
defer store.Close()
|
|
|
|
entries, err := store.ListUsageEntries(context.Background(), *limit)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
fmt.Println("time\trequest_ip\tuser_id\ttoken_id\thttp_status\tsuccess\terror_reason\toriginal_url")
|
|
for _, e := range entries {
|
|
userID := "-"
|
|
tokenID := "-"
|
|
if e.UserID.Valid {
|
|
userID = fmt.Sprintf("%d", e.UserID.Int64)
|
|
}
|
|
if e.TokenID.Valid {
|
|
tokenID = fmt.Sprintf("%d", e.TokenID.Int64)
|
|
}
|
|
fmt.Printf("%s\t%s\t%s\t%s\t%d\t%t\t%s\t%s\n",
|
|
e.OccurredAt.Format(time.RFC3339),
|
|
e.RequestIP,
|
|
userID,
|
|
tokenID,
|
|
e.HTTPStatus,
|
|
e.Success,
|
|
e.ErrorReason,
|
|
e.OriginalURL,
|
|
)
|
|
}
|
|
}
|
|
|
|
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 <username> [--db <path>]
|
|
tokenctl issue-token --user <username> [--expires <7d|30d|RFC3339>] [--uses <n>] [--db <path>]
|
|
tokenctl disable-token --token <token> [--db <path>]
|
|
tokenctl list-users [--db <path>]
|
|
tokenctl list-tokens [--db <path>]
|
|
tokenctl list-usage [--limit <n>] [--db <path>]`)
|
|
}
|