internal/control/web.go
106 lines · 3483 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "time"
8
9 "gitbay.org/gitbay/internal/protocol"
10 "gitbay.org/gitbay/internal/store"
11)
12
13func newStoredToken() (token, hash string, err error) { return store.NewToken() }
14
15func init() {
16 register(Command{Path: []string{"web", "login"},
17 Summary: "mint a one-time browser login URL",
18 Usage: "web login",
19 MintsCredential: true, NeedsRecentSignIn: true,
20 Examples: []string{"web login"}, Run: runWebLogin})
21 register(Command{Path: []string{"web", "sessions", "list"},
22 Summary: "list your browser sessions",
23 Usage: "web sessions list",
24 Examples: []string{"web sessions list"}, ReadOnly: true, Run: runWebSessionsList})
25 register(Command{Path: []string{"web", "sessions", "revoke"},
26 Summary: "end a browser session, or all of them",
27 Usage: "web sessions revoke <id>|--all",
28 Flags: []Flag{
29 {"--all", "", "revoke every browser session", ""},
30 },
31 Examples: []string{"web sessions revoke --all"}, Run: runWebSessionsRevoke})
32}
33
34func runWebSessionsList(c *Ctx, args []string) int {
35 if len(args) != 0 {
36 return c.usage()
37 }
38 sessions, err := c.Store.ListWebSessions(c.User.ID)
39 if err != nil {
40 return c.fail(protocol.ExitFailure, "%v", err)
41 }
42 return c.emit(sessions, func(w io.Writer) {
43 tb := c.table(w, "ID", "SINCE", "UNTIL", "USED")
44 for _, s := range sessions {
45 if c.Term.Cols > 0 {
46 tb.row(cRef(s.ID), cAge(s.CreatedAt), cAge(s.ExpiresAt), cText(c.usedText(s.LastUsedAt)))
47 continue
48 }
49 tb.row(cRef(s.ID), cText("since "+stamp(s.CreatedAt)), cText("until "+stamp(s.ExpiresAt)), cText(c.usedText(s.LastUsedAt)))
50 }
51 tb.flush()
52 })
53}
54
55func runWebSessionsRevoke(c *Ctx, args []string) int {
56 if len(args) != 1 {
57 return c.usage()
58 }
59 if args[0] == "--all" {
60 n, err := c.Store.RevokeAllWebSessions(c.User.ID)
61 if err != nil {
62 return c.fail(protocol.ExitFailure, "%v", err)
63 }
64 return c.emit(map[string]any{"revoked": n}, func(w io.Writer) {
65 fmt.Fprintf(w, "revoked %d browser sessions\n", n)
66 })
67 }
68 if err := c.Store.RevokeWebSession(c.User.ID, args[0]); err != nil {
69 if errors.Is(err, store.ErrNotFound) {
70 return c.fail(protocol.ExitNotFound, "no browser session %s on your account", args[0])
71 }
72 return c.fail(protocol.ExitFailure, "%v", err)
73 }
74 return c.emit(map[string]any{"revoked": args[0]}, func(w io.Writer) {
75 fmt.Fprintf(w, "revoked browser session %s\n", args[0])
76 })
77}
78
79func runWebLogin(c *Ctx, args []string) int {
80 if len(args) != 0 {
81 return c.usage()
82 }
83 if c.Cfg.Web.Mode != "accounts" {
84 return c.fail(protocol.ExitDenied,
85 "this instance runs the web in view-only mode (web.mode = %q); there is nothing to log in to", c.Cfg.Web.Mode)
86 }
87 n, err := c.Store.CountLoginTokensSince(c.User.ID, time.Now().Add(-time.Hour))
88 if err != nil {
89 return c.fail(protocol.ExitFailure, "%v", err)
90 }
91 if n >= maxLoginLinksPerHour {
92 return c.fail(protocol.ExitDenied,
93 "%d login links in the last hour is the most an account gets; use one of those, or wait", maxLoginLinksPerHour)
94 }
95 token, hash, err := newStoredToken()
96 if err != nil {
97 return c.fail(protocol.ExitFailure, "%v", err)
98 }
99 if err := c.Store.CreateLoginToken(c.User.ID, hash, 5*time.Minute); err != nil {
100 return c.fail(protocol.ExitFailure, "%v", err)
101 }
102 url := c.Cfg.Server.SiteURL + "/login?token=" + token
103 return c.emit(map[string]string{"url": url, "expires_in": "5m"}, func(w io.Writer) {
104 fmt.Fprintf(w, "open within 5 minutes (single use):\n%s\n", url)
105 })
106}