Mints a single-use invitation and prints the absolute claim URL for the operator to send. The URL base is TAPIR_PUBLIC_URL (default https://tapir.d-ma.be). runInvite is factored from config/store wiring so it's unit-tested against a fake inviter — no Postgres. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
1.9 KiB
Go
62 lines
1.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
|
"gitea.d-ma.be/mathias/tapir/internal/config"
|
|
)
|
|
|
|
// inviteTTL is how long a minted invite stays claimable. A week is generous for a
|
|
// human to act on an emailed link without leaving a stale capability around.
|
|
const inviteTTL = 7 * 24 * time.Hour
|
|
|
|
// inviter is the narrow store capability cmdInvite needs — minting an invitation.
|
|
// Defined here (not store) so runInvite is testable with a fake, no Postgres.
|
|
type inviter interface {
|
|
CreateInvitation(ctx context.Context, email string, ttl time.Duration) (string, error)
|
|
}
|
|
|
|
// cmdInvite mints an invitation for an email and prints the claim URL. Host-side
|
|
// only (no Dex session): the operator runs it, copies the link, and sends it.
|
|
// Usage: tapir invite <email>.
|
|
func cmdInvite(ctx context.Context, args []string) error {
|
|
if len(args) < 1 || strings.TrimSpace(args[0]) == "" {
|
|
return fmt.Errorf("usage: tapir invite <email>")
|
|
}
|
|
email := strings.TrimSpace(args[0])
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(cfg.DBDSN) == "" {
|
|
return fmt.Errorf("missing required config: TAPIR_DB_DSN")
|
|
}
|
|
|
|
st, err := store.New(ctx, cfg.DBDSN)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer st.Close()
|
|
|
|
return runInvite(ctx, st, os.Stdout, cfg.PublicURL, email)
|
|
}
|
|
|
|
// runInvite is the testable core: mint the token and print the absolute claim URL
|
|
// to w. Pure of config/store construction so a fake inviter exercises it.
|
|
func runInvite(ctx context.Context, inv inviter, w io.Writer, publicURL, email string) error {
|
|
token, err := inv.CreateInvitation(ctx, email, inviteTTL)
|
|
if err != nil {
|
|
return fmt.Errorf("create invitation: %w", err)
|
|
}
|
|
base := strings.TrimRight(strings.TrimSpace(publicURL), "/")
|
|
_, err = fmt.Fprintf(w, "Invite URL (valid 7 days):\n%s/invite/%s\n", base, token)
|
|
return err
|
|
}
|