初步实现鉴权

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-23 20:21:35 +08:00
parent 847ce0c6a8
commit 62e076111f
10 changed files with 1212 additions and 87 deletions
+198
View File
@@ -0,0 +1,198 @@
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 <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>]`)
}