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}