repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

3.4 KB · 104 lines · Go Raw History
  1package main
  2
  3import (
  4	"database/sql"
  5	"errors"
  6	"fmt"
  7)
  8
  9// runInit seeds the account and prints the first set of recovery codes, which
 10// are what the first login uses. Nothing here needs ntfy, so a fresh machine can
 11// get in before the phone is enrolled and mint everything else from inside.
 12//
 13// It is idempotent the same way `ntfy up` is: an account that already exists
 14// keeps what it has and nothing is printed, because handing back a set of codes
 15// that were never applied is worse than printing nothing.
 16//
 17// This runs as `docker exec orchard-auth /app -init`, which works against a
 18// scratch image because exec wants a binary path rather than a shell. It must
 19// never generate on first boot and print to stdout, since container stdout ships
 20// to logging.bythewood.me and that would write recovery codes into a store with
 21// a long retention on it.
 22func runInit(db *sql.DB) error {
 23	created, err := createUser(db, seedUsername)
 24	if err != nil {
 25		return err
 26	}
 27	if !created {
 28		user, err := loadUser(db)
 29		if err != nil {
 30			return err
 31		}
 32		remaining, err := countRecoveryCodes(db)
 33		if err != nil {
 34			return err
 35		}
 36		fmt.Printf("already initialized as %q, with %d recovery codes left, and nothing was changed.\n",
 37			user.Username, remaining)
 38		fmt.Println("to replace the codes, sign in and use the security page.")
 39		return nil
 40	}
 41
 42	codes, err := regenerateRecoveryCodes(db)
 43	if err != nil {
 44		return err
 45	}
 46
 47	fmt.Printf("\nthe account is %q, and these are its recovery codes:\n\n", seedUsername)
 48	for _, code := range codes {
 49		fmt.Printf("  %s\n", code)
 50	}
 51	fmt.Printf("\nthese are the only copies, so put them in 1Password now. each one\n")
 52	fmt.Printf("works once. sign in with one at %s/recovery, then\n", baseURL)
 53	fmt.Printf("subscribe the phone to the %q topic and regenerate them from the\n", ntfyTopic)
 54	fmt.Printf("security page, so the set you keep was never on a terminal.\n")
 55	return nil
 56}
 57
 58// runCheck reports what exists without changing anything, which is what
 59// `make doctor` calls. It has to be its own flag rather than a second run of
 60// -init, since that would create the account on a machine that has none.
 61func runCheck(db *sql.DB) error {
 62	user, err := loadUser(db)
 63	if errors.Is(err, errNoUser) {
 64		fmt.Println("not initialized")
 65		return nil
 66	}
 67	if err != nil {
 68		return err
 69	}
 70	remaining, err := countRecoveryCodes(db)
 71	if err != nil {
 72		return err
 73	}
 74	fmt.Printf("initialized as %s, %d recovery codes left\n", user.Username, remaining)
 75	return nil
 76}
 77
 78// runRecovery replaces the recovery codes from the command line, for the case
 79// that has no other way out: no codes left and ntfy or the tunnel down, so the
 80// browser cannot reach a sign in and the security page is unreachable.
 81//
 82// It needs host access to the Docker socket, which is the point. That is the
 83// bottom of the ladder and it is why nothing about this account has to be kept
 84// anywhere: every credential here can be replaced from the machine.
 85func runRecovery(db *sql.DB) error {
 86	if _, err := loadUser(db); errors.Is(err, errNoUser) {
 87		return fmt.Errorf("not initialized yet, run -init instead")
 88	} else if err != nil {
 89		return err
 90	}
 91
 92	codes, err := regenerateRecoveryCodes(db)
 93	if err != nil {
 94		return err
 95	}
 96
 97	fmt.Printf("\nevery previous recovery code stopped working just now. the new set:\n\n")
 98	for _, code := range codes {
 99		fmt.Printf("  %s\n", code)
100	}
101	fmt.Printf("\neach one works once, at %s/recovery.\n", baseURL)
102	return nil
103}