internal/control/web.go

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

110 lines · 3717 bytes

5 symbols in this file
  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}