internal/control/web.go
108 lines · 3523 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 since, until := s.CreatedAt, s.ExpiresAt
46 if c.Term.Cols == 0 {
47 since, until = stamp(since), stamp(until)
48 } else {
49 since, until = relAge(since, termNow()), relAge(until, termNow())
50 }
51 tb.row(cRef(s.ID), cText("since "+since), cText("until "+until), cText(c.usedText(s.LastUsedAt)))
52 }
53 tb.flush()
54 })
55}
56
57func runWebSessionsRevoke(c *Ctx, args []string) int {
58 if len(args) != 1 {
59 return c.usage()
60 }
61 if args[0] == "--all" {
62 n, err := c.Store.RevokeAllWebSessions(c.User.ID)
63 if err != nil {
64 return c.fail(protocol.ExitFailure, "%v", err)
65 }
66 return c.emit(map[string]any{"revoked": n}, func(w io.Writer) {
67 fmt.Fprintf(w, "revoked %d browser sessions\n", n)
68 })
69 }
70 if err := c.Store.RevokeWebSession(c.User.ID, args[0]); err != nil {
71 if errors.Is(err, store.ErrNotFound) {
72 return c.fail(protocol.ExitNotFound, "no browser session %s on your account", args[0])
73 }
74 return c.fail(protocol.ExitFailure, "%v", err)
75 }
76 return c.emit(map[string]any{"revoked": args[0]}, func(w io.Writer) {
77 fmt.Fprintf(w, "revoked browser session %s\n", args[0])
78 })
79}
80
81func runWebLogin(c *Ctx, args []string) int {
82 if len(args) != 0 {
83 return c.usage()
84 }
85 if c.Cfg.Web.Mode != "accounts" {
86 return c.fail(protocol.ExitDenied,
87 "this instance runs the web in view-only mode (web.mode = %q); there is nothing to log in to", c.Cfg.Web.Mode)
88 }
89 n, err := c.Store.CountLoginTokensSince(c.User.ID, time.Now().Add(-time.Hour))
90 if err != nil {
91 return c.fail(protocol.ExitFailure, "%v", err)
92 }
93 if n >= maxLoginLinksPerHour {
94 return c.fail(protocol.ExitDenied,
95 "%d login links in the last hour is the most an account gets; use one of those, or wait", maxLoginLinksPerHour)
96 }
97 token, hash, err := newStoredToken()
98 if err != nil {
99 return c.fail(protocol.ExitFailure, "%v", err)
100 }
101 if err := c.Store.CreateLoginToken(c.User.ID, hash, 5*time.Minute); err != nil {
102 return c.fail(protocol.ExitFailure, "%v", err)
103 }
104 url := c.Cfg.Server.SiteURL + "/login?token=" + token
105 return c.emit(map[string]string{"url": url, "expires_in": "5m"}, func(w io.Writer) {
106 fmt.Fprintf(w, "open within 5 minutes (single use):\n%s\n", url)
107 })
108}