signuprequests

package
v0.36.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package signuprequests manages the pre-account waitlist: someone visits /signup without an invite, fills the "request access" form, and lands as a pending row that an admin later approves (minting a single-use magic-link signup token) or rejects.

Rows live outside the multi-tenant grain — there is no user_id and no RLS, because the row exists before the requester has an account. Admin-facing handlers run inside the existing requireAdmin gate so listing the table is safe.

Index

Constants

View Source
const (
	StatusPending  = "pending"
	StatusApproved = "approved"
	StatusRejected = "rejected"
)

Status values stored in signup_requests.status. The DB CHECK constraint enforces this set.

View Source
const (
	NeverApprovedRetention = 90 * 24 * time.Hour
	ApprovedUnusedGrace    = 30 * 24 * time.Hour
)

Retention of waitlist requests: one never approved is deleted after NeverApprovedRetention; an approved one is deleted once its link was used (the account exists) or, if never used, ApprovedUnusedGrace after the link expired.

View Source
const DefaultTokenTTL = 14 * 24 * time.Hour

DefaultTokenTTL is how long a magic-link signup token stays valid. 14 days is generous — the requester might be on vacation when the approval lands — and short enough that an intercepted token can't lurk indefinitely.

Variables

View Source
var ErrAlreadyPending = errors.New("signuprequests: email already has a pending request")

ErrAlreadyPending is returned when an email already has a pending row. Re-requests after a decision (approved/rejected) are allowed.

View Source
var ErrNotFound = errors.New("signuprequests: not found")

ErrNotFound is returned for lookups against a missing id.

View Source
var ErrNotPending = errors.New("signuprequests: not pending")

ErrNotPending is returned when an admin tries to decide on a row that is not in the pending state (race or double-click).

View Source
var ErrTokenInvalid = errors.New("signuprequests: token invalid or expired")

ErrTokenInvalid is returned by LookupToken / ConsumeToken when the token does not match an approved row, has expired, or has already been redeemed. Single sentinel rather than separate errors so the frontend can show a uniform "this link is no longer valid" without leaking which row state caused the rejection.

Functions

This section is empty.

Types

type Request

type Request struct {
	ID             uuid.UUID
	Email          string
	Message        string
	Status         string
	SignupToken    *string
	TokenExpiresAt *time.Time
	TokenUsedAt    *time.Time
	DecidedBy      *uuid.UUID
	DecidedAt      *time.Time
	RequestedAt    time.Time
}

Request is the read view of a signup_requests row.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store wraps the signup_requests table.

func New

func New(db *sql.DB) *Store

New returns a Store backed by db.

func (*Store) ApproveWithToken added in v0.18.28

func (s *Store) ApproveWithToken(ctx context.Context, id, decidedBy uuid.UUID, ttl time.Duration) (Request, error)

ApproveWithToken transitions a pending row to approved and stamps a single-use signup_token + expiry on it. The returned Request carries the token so the caller can email a sign-up link. Distinct from Approve (legacy invite_code path) so the two flows can coexist while we deprecate codes.

func (*Store) ConsumeToken added in v0.18.28

func (s *Store) ConsumeToken(ctx context.Context, token string) (Request, error)

ConsumeToken atomically marks the token as redeemed. Returns ErrTokenInvalid if the token was already used, has expired, or does not exist. The single UPDATE…WHERE checks all three so two concurrent submissions can't both succeed.

func (*Store) CountSince added in v0.36.15

func (s *Store) CountSince(ctx context.Context, email string, t time.Time) (int, error)

CountSince is how many requests the email made since t, whatever their status: the throttle that keeps a public form from mailing one address over and over.

func (*Store) Create

func (s *Store) Create(ctx context.Context, email, message string) (Request, error)

Create inserts a new pending request. Returns ErrAlreadyPending if the same email already has a pending row.

func (*Store) Get

func (s *Store) Get(ctx context.Context, id uuid.UUID) (Request, error)

Get fetches one row by id.

func (*Store) List

func (s *Store) List(ctx context.Context, status string) ([]Request, error)

List returns rows filtered by status (empty string = all), newest first. Caller should already be admin-gated.

func (*Store) LookupToken added in v0.18.28

func (s *Store) LookupToken(ctx context.Context, token string) (Request, error)

LookupToken returns the approved row whose signup_token matches. Used by the public GET /api/signup/token/{token} endpoint to prefill the email on the signup form. Returns ErrTokenInvalid for missing/expired/already-redeemed tokens.

func (*Store) PendingID added in v0.36.15

func (s *Store) PendingID(ctx context.Context, email string) (uuid.UUID, error)

PendingID returns the id of the email's pending request, or ErrNotFound.

func (*Store) PurgeStale added in v0.36.10

func (s *Store) PurgeStale(ctx context.Context) (int64, error)

PurgeStale deletes the requests past retention and returns how many.

func (*Store) Reject

func (s *Store) Reject(ctx context.Context, id, decidedBy uuid.UUID) (Request, error)

Reject transitions a pending row to rejected.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL