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