internal/control/web.go

99 lines · 3082 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		Examples: []string{"web login"}, Run: runWebLogin})
20	register(Command{Path: []string{"web", "sessions", "list"},
21		Summary:  "list your browser sessions",
22		Usage:    "web sessions list",
23		Examples: []string{"web sessions list"}, ReadOnly: true, Run: runWebSessionsList})
24	register(Command{Path: []string{"web", "sessions", "revoke"},
25		Summary: "end a browser session, or all of them",
26		Usage:   "web sessions revoke <id>|--all",
27		Flags: []Flag{
28			{"--all", "", "revoke every browser session", ""},
29		},
30		Examples: []string{"web sessions revoke --all"}, Run: runWebSessionsRevoke})
31}
32
33func runWebSessionsList(c *Ctx, args []string) int {
34	if len(args) != 0 {
35		return c.usage()
36	}
37	sessions, err := c.Store.ListWebSessions(c.User.ID)
38	if err != nil {
39		return c.fail(protocol.ExitFailure, "%v", err)
40	}
41	return c.emit(sessions, func(w io.Writer) {
42		tb := c.table(w, "ID", "SINCE", "UNTIL")
43		for _, s := range sessions {
44			since, until := s.CreatedAt, s.ExpiresAt
45			if c.Term.Cols == 0 {
46				since, until = stamp(since), stamp(until)
47			} else {
48				since, until = relAge(since, termNow()), relAge(until, termNow())
49			}
50			tb.row(cRef(s.ID), cText("since "+since), cText("until "+until))
51		}
52		tb.flush()
53	})
54}
55
56func runWebSessionsRevoke(c *Ctx, args []string) int {
57	if len(args) != 1 {
58		return c.usage()
59	}
60	if args[0] == "--all" {
61		n, err := c.Store.RevokeAllWebSessions(c.User.ID)
62		if err != nil {
63			return c.fail(protocol.ExitFailure, "%v", err)
64		}
65		return c.emit(map[string]any{"revoked": n}, func(w io.Writer) {
66			fmt.Fprintf(w, "revoked %d browser sessions\n", n)
67		})
68	}
69	if err := c.Store.RevokeWebSession(c.User.ID, args[0]); err != nil {
70		if errors.Is(err, store.ErrNotFound) {
71			return c.fail(protocol.ExitNotFound, "no browser session %s on your account", args[0])
72		}
73		return c.fail(protocol.ExitFailure, "%v", err)
74	}
75	return c.emit(map[string]any{"revoked": args[0]}, func(w io.Writer) {
76		fmt.Fprintf(w, "revoked browser session %s\n", args[0])
77	})
78}
79
80func runWebLogin(c *Ctx, args []string) int {
81	if len(args) != 0 {
82		return c.usage()
83	}
84	if c.Cfg.Web.Mode != "accounts" {
85		return c.fail(protocol.ExitDenied,
86			"this instance runs the web in view-only mode (web.mode = %q); there is nothing to log in to", c.Cfg.Web.Mode)
87	}
88	token, hash, err := newStoredToken()
89	if err != nil {
90		return c.fail(protocol.ExitFailure, "%v", err)
91	}
92	if err := c.Store.CreateLoginToken(c.User.ID, hash, 5*time.Minute); err != nil {
93		return c.fail(protocol.ExitFailure, "%v", err)
94	}
95	url := c.Cfg.Server.SiteURL + "/login?token=" + token
96	return c.emit(map[string]string{"url": url, "expires_in": "5m"}, func(w io.Writer) {
97		fmt.Fprintf(w, "open within 5 minutes (single use):\n%s\n", url)
98	})
99}