25e0204471
Apply changes from tsk_GXDoc3Cs07Rn (conflict rescue: original auto-merge
blocked by uncommitted changes on main; worktree was clean, no code conflicts).
Changes applied verbatim from b64c002:
- server/CONVENTIONS.md: new file — mandatory coding conventions
- server/internal/apierr: add New(), StatusFor(), ErrUnauthorized/Forbidden/
NotFound/Conflict predefined errors, chi Middleware; add apierr_test.go (8 tests)
- server/internal/idgen: implement idgen.go (UUID v7 + Crockford Base32 moved
from codes); add idgen_test.go (12 tests)
- server/internal/codes/generator.go: refactor to delegate to idgen.*
- server/go.mod: move google/uuid from indirect to direct dependency
All tests pass: go test ./internal/apierr/... ./internal/idgen/... ./internal/codes/...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
42 lines
1.6 KiB
Go
42 lines
1.6 KiB
Go
// Package codes implements the activation-code lifecycle:
|
|
// batch generation, card-store webhook ingestion, idempotent redemption,
|
|
// subscription extension, and CSV export.
|
|
//
|
|
// Security invariant: plaintext codes are NEVER written to the database or
|
|
// to any log. The database stores only SHA-256(canonical_plaintext).
|
|
// Plaintext appears exactly once: in the batch-generation response / CSV export.
|
|
package codes
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/wangjia/pangolin/server/internal/idgen"
|
|
)
|
|
|
|
// GenerateCode generates a single random activation code in canonical Crockford
|
|
// Base32 form (15 random data chars + 1 check char = 16 chars total).
|
|
// It delegates to idgen.GenerateCode which uses crypto/rand.
|
|
func GenerateCode() (string, error) {
|
|
return idgen.GenerateCode()
|
|
}
|
|
|
|
// Canonicalize converts an activation-code string into canonical form:
|
|
// uppercase, with I/L→1 and O→0 substitutions applied.
|
|
// Hyphens and spaces are stripped before validation.
|
|
// Returns an error if the string contains characters outside the normalised
|
|
// Crockford alphabet, if the length is not exactly 16, or if the check character
|
|
// is incorrect.
|
|
func Canonicalize(code string) (string, error) {
|
|
return idgen.CanonicalizeCode(code)
|
|
}
|
|
|
|
// Hash returns the hex-encoded SHA-256 of the canonical plaintext code.
|
|
// This is the value stored in the database; the plaintext is never stored.
|
|
func Hash(canonical string) string {
|
|
return idgen.HashCode(canonical)
|
|
}
|
|
|
|
// ErrDuplicate is returned by the batch generator when a generated code
|
|
// already exists in the database (hash collision). The caller should retry.
|
|
var ErrDuplicate = errors.New("codes: duplicate code hash")
|