krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
main: internal/control/token.go · raw
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "strconv"
8 "strings"
9 "time"
10
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15func init() {
16 register(Command{Path: []string{"token", "create"},
17 Summary: "mint an API token (shown once): token create --name <n> [--scope full|read] [--ttl 30d|720h]",
18 SSHOnly: true, Run: runTokenCreate})
19 register(Command{Path: []string{"token", "list"},
20 Summary: "list API tokens", ReadOnly: true, SSHOnly: true, Run: runTokenList})
21 register(Command{Path: []string{"token", "revoke"},
22 Summary: "revoke an API token by name: token revoke <name>",
23 SSHOnly: true, Run: runTokenRevoke})
24}
25
26// parseTTL accepts Go durations plus a day suffix ("30d").
27func parseTTL(s string) (time.Duration, error) {
28 if days, ok := strings.CutSuffix(s, "d"); ok {
29 n, err := strconv.Atoi(days)
30 if err != nil || n < 1 {
31 return 0, fmt.Errorf("bad ttl %q", s)
32 }
33 return time.Duration(n) * 24 * time.Hour, nil
34 }
35 return time.ParseDuration(s)
36}
37
38func runTokenCreate(c *Ctx, args []string) int {
39 name, scope, ttl := "", "full", ""
40 for i := 0; i < len(args); i++ {
41 switch args[i] {
42 case "--name", "--scope", "--ttl":
43 if i+1 >= len(args) {
44 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
45 }
46 switch args[i] {
47 case "--name":
48 name = args[i+1]
49 case "--scope":
50 scope = args[i+1]
51 case "--ttl":
52 ttl = args[i+1]
53 }
54 i++
55 default:
56 return c.fail(protocol.ExitUsage, "usage: token create --name <n> [--scope full|read] [--ttl 30d]")
57 }
58 }
59 if name == "" || (scope != "full" && scope != "read") {
60 return c.fail(protocol.ExitUsage, "usage: token create --name <n> [--scope full|read] [--ttl 30d]")
61 }
62 var expires *time.Time
63 if ttl != "" {
64 d, err := parseTTL(ttl)
65 if err != nil {
66 return c.fail(protocol.ExitUsage, "%v", err)
67 }
68 t := time.Now().Add(d)
69 expires = &t
70 }
71 raw, _, err := store.NewToken()
72 if err != nil {
73 return c.fail(protocol.ExitFailure, "%v", err)
74 }
75 // The gb_ prefix makes leaked tokens findable by secret scanners.
76 token := "gb_" + raw
77 if err := c.Store.CreateAPIToken(c.User.ID, name, store.HashToken(token), scope, expires); err != nil {
78 return c.fail(protocol.ExitUsage, "%v", err)
79 }
80 type out struct {
81 Name string `json:"name"`
82 Scope string `json:"scope"`
83 Token string `json:"token"`
84 }
85 d := out{name, scope, token}
86 return c.emit(d, func(w io.Writer) {
87 fmt.Fprintf(w, "token %q (%s) — shown once, store it now:\n%s\n", d.Name, d.Scope, d.Token)
88 })
89}
90
91func runTokenList(c *Ctx, args []string) int {
92 tokens, err := c.Store.ListAPITokens(c.User.ID)
93 if err != nil {
94 return c.fail(protocol.ExitFailure, "%v", err)
95 }
96 type out struct {
97 Name string `json:"name"`
98 Scope string `json:"scope"`
99 CreatedAt string `json:"created_at"`
100 ExpiresAt *time.Time `json:"expires_at,omitempty"`
101 LastUsedAt *time.Time `json:"last_used_at,omitempty"`
102 }
103 var ds []out
104 for _, t := range tokens {
105 ds = append(ds, out{t.Name, t.Scope, t.CreatedAt, t.ExpiresAt, t.LastUsedAt})
106 }
107 return c.emit(ds, func(w io.Writer) {
108 for _, d := range ds {
109 exp := "never expires"
110 if d.ExpiresAt != nil {
111 exp = "expires " + d.ExpiresAt.UTC().Format(time.RFC3339)
112 }
113 fmt.Fprintf(w, "%s\t%s\t%s\n", d.Name, d.Scope, exp)
114 }
115 })
116}
117
118func runTokenRevoke(c *Ctx, args []string) int {
119 if len(args) != 1 {
120 return c.fail(protocol.ExitUsage, "usage: token revoke <name>")
121 }
122 if err := c.Store.RevokeAPIToken(c.User.ID, args[0]); err != nil {
123 if errors.Is(err, store.ErrNotFound) {
124 return c.fail(protocol.ExitNotFound, "no token named %q", args[0])
125 }
126 return c.fail(protocol.ExitFailure, "%v", err)
127 }
128 return c.emit(map[string]string{"revoked": args[0]}, func(w io.Writer) {
129 fmt.Fprintf(w, "revoked %s\n", args[0])
130 })
131}