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
- Variables
- type Request
- type Store
- func (s *Store) ApproveWithToken(ctx context.Context, id, decidedBy uuid.UUID, ttl time.Duration) (Request, error)
- func (s *Store) ConsumeToken(ctx context.Context, token string) (Request, error)
- func (s *Store) CountSince(ctx context.Context, email string, t time.Time) (int, error)
- func (s *Store) Create(ctx context.Context, email, message string) (Request, error)
- func (s *Store) Get(ctx context.Context, id uuid.UUID) (Request, error)
- func (s *Store) List(ctx context.Context, status string) ([]Request, error)
- func (s *Store) LookupToken(ctx context.Context, token string) (Request, error)
- func (s *Store) PendingID(ctx context.Context, email string) (uuid.UUID, error)
- func (s *Store) PurgeStale(ctx context.Context) (int64, error)
- func (s *Store) Reject(ctx context.Context, id, decidedBy uuid.UUID) (Request, error)
Constants ¶
const ( StatusPending = "pending" StatusApproved = "approved" StatusRejected = "rejected" )
Status values stored in signup_requests.status. The DB CHECK constraint enforces this set.
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.
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 ¶
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.
var ErrNotFound = errors.New("signuprequests: not found")
ErrNotFound is returned for lookups against a missing id.
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).
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 (*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
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
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 ¶
Create inserts a new pending request. Returns ErrAlreadyPending if the same email already has a pending row.
func (*Store) List ¶
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
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
PendingID returns the id of the email's pending request, or ErrNotFound.
func (*Store) PurgeStale ¶ added in v0.36.10
PurgeStale deletes the requests past retention and returns how many.