Documentation
¶
Overview ¶
Package bodyapi is the contract between the main backend and the body service, the part that holds a user's scale readings and DEXA scans and computes their estimates.
Every call carries two headers. X-Dexafix-Body is the body secret that the two services share. X-Dexafix-Body-Token is a short-lived token that the main backend signs with an Ed25519 key for one user and one scope; the body service only holds the public key, so it can check tokens but never make one. The token also carries the settings of the user's row that the estimates depend on, so the body service does not have to trust a request body for them.
The package uses nothing but the standard library and pkg/models and pkg/estimate (pure data and maths); it imports neither a backend nor the store.
Index ¶
- Constants
- Variables
- func EncodePrivateKey(priv ed25519.PrivateKey) string
- func EncodePublicKey(pub ed25519.PublicKey) string
- func GenerateKey() (keyID string, pub ed25519.PublicKey, priv ed25519.PrivateKey)
- func HTTPClient(tlsConfig *tls.Config) *http.Client
- func KeyID(pub ed25519.PublicKey) string
- func ParsePrivateKey(text string) (ed25519.PrivateKey, error)
- func ParsePublicKeys(text string) ([]ed25519.PublicKey, error)
- func StatusFor(code string) int
- type Claims
- type Client
- func (c *Client) CreateScan(ctx context.Context, u Subject, s *models.DexaScan) error
- func (c *Client) DeleteReading(ctx context.Context, u Subject, id int64) error
- func (c *Client) DeleteScan(ctx context.Context, u Subject, id int64) error
- func (c *Client) DeleteUser(ctx context.Context, userID int64) error
- func (c *Client) Estimates(ctx context.Context, u Subject, ifNoneMatch string) (EstimatesReply, error)
- func (c *Client) EstimatesWindow(ctx context.Context, u Subject, ifNoneMatch string, days int) (EstimatesReply, error)
- func (c *Client) Export(ctx context.Context, u Subject) (Export, error)
- func (c *Client) Health(ctx context.Context) error
- func (c *Client) Readings(ctx context.Context, u Subject, p Page) (readings []models.ScaleReading, next string, err error)
- func (c *Client) Reconcile(ctx context.Context, u Subject, req ReconcileRequest) (ImportResult, error)
- func (c *Client) Scans(ctx context.Context, u Subject, p Page) (scans []models.DexaScan, next string, err error)
- func (c *Client) Stats(ctx context.Context) (Stats, error)
- func (c *Client) UpsertReadings(ctx context.Context, u Subject, rs []models.ScaleReading) (int, error)
- func (c *Client) UserIDs(ctx context.Context) ([]int64, error)
- type Error
- type EstimateParams
- type EstimatesReply
- type Export
- type ImportResult
- type Page
- type ReconcileRequest
- type Scope
- type Signer
- type Stats
- type Subject
- type Verifier
Constants ¶
const ( // Prefix is the path every endpoint lives under. Prefix = "/body/v1" // HeaderSecret carries the secret the main backend and the body service share. HeaderSecret = "X-Dexafix-Body" // HeaderToken carries the signed token for the user and scope of one call. HeaderToken = "X-Dexafix-Body-Token" // Audience is what a token must name, so a token made for another service is not accepted. Audience = "DexaFix body" // MaxTokenAge is the longest a token may live. The signer sets it; the verifier refuses more. MaxTokenAge = 60 * time.Second )
const ( CodeNotFound = "not_found" CodeInvalid = "invalid" CodeForbidden = "forbidden" CodeInternal = "internal" )
Error codes in an error answer.
Variables ¶
var ( ErrNotFound = &Error{Code: CodeNotFound} ErrInvalid = &Error{Code: CodeInvalid} ErrForbidden = &Error{Code: CodeForbidden} )
Sentinels for errors.Is.
Functions ¶
func EncodePrivateKey ¶
func EncodePrivateKey(priv ed25519.PrivateKey) string
EncodePrivateKey writes a private key as text.
func EncodePublicKey ¶
EncodePublicKey writes a public key as text.
func GenerateKey ¶
func GenerateKey() (keyID string, pub ed25519.PublicKey, priv ed25519.PrivateKey)
GenerateKey makes a signing key pair. The key id names the public key, so several can be trusted side by side while one is being replaced.
func HTTPClient ¶
HTTPClient is the client for a body service in another process, with a TLS configuration such as the mutual TLS configuration of package mtls (nil means plain HTTP). An import of a long history takes a while to answer, so the wait for a response is generous; connecting is not.
func ParsePrivateKey ¶
func ParsePrivateKey(text string) (ed25519.PrivateKey, error)
ParsePrivateKey reads the private key of a key file. A file holds exactly one.
func ParsePublicKeys ¶
ParsePublicKeys reads the public keys of a key file, one per line. At least one is required.
Types ¶
type Claims ¶
type Claims struct {
KeyID string `json:"kid"`
Subject int64 `json:"sub"` // the user; 0 for a call that is about no user in particular
Audience string `json:"aud"`
Scope Scope `json:"scope"`
Expires int64 `json:"exp"` // Unix seconds
Params *EstimateParams `json:"params,omitempty"`
}
Claims are what a token says. They are signed, so the body service can rely on all of them.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client calls the body service on behalf of the main backend. It makes a token for every call.
func New ¶
New returns a Client for the service at base. hc carries the transport (TLS, mutual TLS, or the in-process transport of apiclient); nil means http.DefaultClient.
func (*Client) CreateScan ¶
CreateScan stores a scan for the user; the scan's ID is set.
func (*Client) DeleteReading ¶
DeleteReading removes one of the user's readings.
func (*Client) DeleteScan ¶
DeleteScan removes one of the user's scans.
func (*Client) DeleteUser ¶
DeleteUser removes everything the body service holds for a user. It is not an error when there is nothing.
func (*Client) Estimates ¶
func (c *Client) Estimates(ctx context.Context, u Subject, ifNoneMatch string) (EstimatesReply, error)
Estimates returns the user's estimates. ifNoneMatch is the ETag of a copy the caller already has, or empty.
func (*Client) EstimatesWindow ¶
func (c *Client) EstimatesWindow(ctx context.Context, u Subject, ifNoneMatch string, days int) (EstimatesReply, error)
EstimatesWindow is Estimates for callers that only need the newest days: with days > 0 the answer holds the days within that many days of the newest one (estimate.WithinDays) and all calibrations. The ETag differs per window, so a validator from one window never matches another.
func (*Client) Health ¶
Health asks the service whether it is up. Its /healthz needs no secret or token, so a wrong secret or key shows up on the first real call, not here.
func (*Client) Readings ¶
func (c *Client) Readings(ctx context.Context, u Subject, p Page) (readings []models.ScaleReading, next string, err error)
Readings lists a user's scale readings, newest first. next is empty on the last page.
func (*Client) Reconcile ¶
func (c *Client) Reconcile(ctx context.Context, u Subject, req ReconcileRequest) (ImportResult, error)
Reconcile stores what a source returned for the user. The body service decides what to write.
func (*Client) Scans ¶
func (c *Client) Scans(ctx context.Context, u Subject, p Page) (scans []models.DexaScan, next string, err error)
Scans lists a user's DEXA scans, newest first. next is empty on the last page.
func (*Client) UpsertReadings ¶
func (c *Client) UpsertReadings(ctx context.Context, u Subject, rs []models.ScaleReading) (int, error)
UpsertReadings stores readings; one with an external id replaces the stored one of the same source and id. It returns how many were stored.
type Error ¶
type Error struct {
Status int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
}
Error is an error answer. Compare with errors.Is against the Err* values below.
type EstimateParams ¶
type EstimateParams struct {
Timezone string `json:"timezone"`
Ranking []string `json:"ranking"`
AthleteDefault bool `json:"athlete_default"`
HeightCm *float64 `json:"height_cm,omitempty"`
}
EstimateParams are the settings of a user's row that the estimates and the import of readings depend on. The main backend puts them in the token; the body service uses nothing else.
func (EstimateParams) Location ¶
func (p EstimateParams) Location() *time.Location
Location is the user's time zone; an empty or unknown name is UTC, as for a user.
type EstimatesReply ¶
type EstimatesReply struct {
Result estimate.Result
ETag string
NotModified bool // the caller's copy, named by ifNoneMatch, is still good; Result is empty
}
EstimatesReply is the answer to Estimates.
type Export ¶
type Export struct {
DexaScans []models.DexaScan `json:"dexa_scans"`
ScaleReadings []models.ScaleReading `json:"scale_readings"`
}
Export is all of a user's scans and readings.
type ImportResult ¶
type ImportResult struct {
Imported int `json:"imported"` // new readings stored
Updated int `json:"updated"` // existing readings whose values changed
Unchanged int `json:"unchanged"` // readings already stored with identical values
Removed int `json:"removed"` // stored readings the source no longer returns (day re-import only)
Skipped int `json:"skipped"` // days left alone because they hold manual readings
SkippedDays []string `json:"skipped_days,omitempty"` // those days as YYYY-MM-DD
}
ImportResult reports what an import did.
type Page ¶
type Page struct {
Limit int // at most this many; 0 means all
Before string // continue after the cursor an earlier page returned
Since, Until time.Time // inclusive bounds on the measurement time; zero means no bound
}
Page selects a window of scans or readings, newest first. The zero value means all of them.
type ReconcileRequest ¶
type ReconcileRequest struct {
Source string `json:"source"`
Fetched []models.ScaleReading `json:"fetched"`
Seen []string `json:"seen"`
DayMode bool `json:"day_mode"`
Day string `json:"day,omitempty"` // YYYY-MM-DD, set when DayMode is
}
ReconcileRequest is what a source (Withings, ...) returned for a user. The body service decides what to store (see the rules on plan in pkg/backend/body).
type Scope ¶
type Scope string
Scope says what a token allows. Each scope includes the ones before it.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer makes tokens. Only the main backend holds one.
func NewSigner ¶
func NewSigner(priv ed25519.PrivateKey) *Signer
NewSigner returns a Signer for a private key.
type Subject ¶
type Subject struct {
ID int64
Params EstimateParams
}
Subject is who a call is for: the user's id and the settings the call may need.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier checks tokens. The body service holds one, with public keys only.
func NewVerifier ¶
NewVerifier trusts the given public keys.