internal/control/web.go

f8b976a97290a20d552056a999511f5d27d8e8ec
gitbay/internal/control/web.go history · blame · raw

108 lines · 3505 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,
 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}