Files
jarl/internal/api/types.go
T
abrown38andClaude Opus 5.5 0b2159b1a4 Add token rotation
jarl token rotate, a Rotate button in the web UI and
POST /api/v1/tokens/{id}/rotate give a token a new secret while it keeps
its ID, name, user and grants. The old secret stops at once or after a
grace period of up to 7 days, during which jarld logs who still uses it.
A token that expires gets its lifetime again. --save writes the new
secret where the CLI got the old one (its configuration or --token @FILE),
--write FILE to any file, and --current picks the token the CLI runs
with, so a restricted token can renew itself. A restricted token can't
rotate a wider token of its user.

Also fixes token create --save for named servers, and future times
(token expiry) shown as "just now" in the web UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:58:50 +00:00

218 lines
7.0 KiB
Go

// Package api defines the JSON types of Jarl's HTTP API, shared by the
// server and the Go client used by the CLI.
package api
import (
"encoding/json"
"time"
"jarl/internal/model"
)
// Version is the API path prefix.
const Prefix = "/api/v1"
// Zone is a zone as returned by the API.
type Zone struct {
model.Zone
Display string `json:"display_name"`
View string `json:"view,omitempty"`
Records int `json:"record_count"`
ZoneFile string `json:"zone_file,omitempty"` // the file named loads
RName string `json:"rname_email,omitempty"`
// Pending is set when listing with a changeset: add, change or delete.
Pending string `json:"pending,omitempty"`
}
// Record is a record as returned by the API.
type Record struct {
model.Record
Zone string `json:"zone"`
View string `json:"view,omitempty"`
RelativeName string `json:"relative_name"`
EffectiveTTL uint32 `json:"effective_ttl"`
// Text is the decoded value of TXT and SPF records.
Text *string `json:"text,omitempty"`
// Pending is set when listing with a changeset: add, change or delete.
Pending string `json:"pending,omitempty"`
}
// View is a view with its zone count.
type View struct {
model.View
Zones int `json:"zone_count"`
}
// ACL is an ACL with the objects that use it.
type ACL struct {
model.ACL
UsedBy []string `json:"used_by"`
}
// TSIGKey is a key; Secret is only filled when asked for and allowed.
type TSIGKey struct {
model.TSIGKey
UsedBy []string `json:"used_by"`
}
// Option is an option with display helpers.
type Option struct {
model.Option
Where string `json:"where"` // e.g. "view internal"
Description string `json:"description,omitempty"`
Status string `json:"status,omitempty"`
}
// User is a user with formatted grants.
type User struct {
model.User
GrantStrings []string `json:"grant_strings"`
}
// Token is an API token. Secret is only set when the token is created.
type Token struct {
model.Token
GrantStrings []string `json:"grant_strings,omitempty"`
Secret string `json:"token,omitempty"`
}
// Whoami describes the authenticated principal.
type Whoami struct {
User User `json:"user"`
Method string `json:"method"`
Token *Token `json:"token,omitempty"`
CSRFToken string `json:"csrf_token,omitempty"`
Permissions []string `json:"permissions"`
Admin bool `json:"admin"`
Version string `json:"version"`
}
// LoginRequest logs in.
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// ErrorBody is the error response.
type ErrorBody struct {
Error ErrorInfo `json:"error"`
}
// ErrorInfo describes an error.
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
Op int `json:"op,omitempty"`
Problems json.RawMessage `json:"problems,omitempty"`
Check json.RawMessage `json:"check,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
// ChangeRequest is a request in a batch (POST /apply or changeset ops).
type ChangeRequest struct {
Op string `json:"op"`
ID int64 `json:"id,omitempty"`
BaseVersion int `json:"base_version,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// BatchRequest applies (or stages) several requests at once.
type BatchRequest struct {
Description string `json:"description,omitempty"`
Ops []ChangeRequest `json:"ops"`
DryRun bool `json:"dry_run,omitempty"`
Force bool `json:"force,omitempty"`
// Apply (for POST /changesets) applies the new changeset at once.
Apply bool `json:"apply,omitempty"`
}
// MutationResponse is returned by endpoints that change the configuration.
type MutationResponse struct {
Applied bool `json:"applied"`
Staged bool `json:"staged,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
ChangesetID int64 `json:"changeset_id,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Changeset json.RawMessage `json:"changeset,omitempty"`
Added []model.Op `json:"added,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Problems json.RawMessage `json:"problems,omitempty"`
Object json.RawMessage `json:"object,omitempty"`
}
// Changeset is a changeset as returned by the API.
type Changeset struct {
model.Changeset
Undoable bool `json:"undoable"`
}
// StatusResponse is GET /status.
type StatusResponse struct {
Version string `json:"version"`
Engine json.RawMessage `json:"engine"`
Time time.Time `json:"time"`
}
// SearchResult is one search hit.
type SearchResult struct {
Kind string `json:"kind"` // zone, record, acl, view, key
ID int64 `json:"id"`
ZoneID int64 `json:"zone_id,omitempty"`
Label string `json:"label"`
Detail string `json:"detail,omitempty"`
}
// PasswordChange changes a password.
type PasswordChange struct {
Current string `json:"current_password,omitempty"`
Password string `json:"password"`
}
// UserInput creates or updates a user.
type UserInput struct {
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
Admin *bool `json:"admin,omitempty"`
Grants *[]model.Grant `json:"grants,omitempty"`
// GrantStrings are grants in compact form ("records:write zones=x").
GrantStrings *[]string `json:"grant_strings,omitempty"`
Role *string `json:"role,omitempty"` // a role template name
Disabled *bool `json:"disabled,omitempty"`
Comment *string `json:"comment,omitempty"`
}
// TokenInput creates a token.
type TokenInput struct {
Name string `json:"name"`
Grants *[]model.Grant `json:"grants,omitempty"`
GrantStrings *[]string `json:"grant_strings,omitempty"`
Role string `json:"role,omitempty"`
ExpiresIn string `json:"expires_in,omitempty"` // e.g. "90d", "720h"
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// TokenRotation gives a token a new secret (POST /tokens/{id}/rotate).
// Every field is optional.
type TokenRotation struct {
// Grace keeps the old secret working this long (e.g. "1h", "2d"; at
// most 7d), so clients can be updated without an outage. Default:
// the old secret stops at once.
Grace string `json:"grace,omitempty"`
// The new expiry. Default: a token that expires gets its lifetime
// again, counted from now; one that doesn't, still doesn't.
ExpiresIn string `json:"expires_in,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
NoExpiry bool `json:"no_expiry,omitempty"`
}
// ZoneImport imports zone file text into a zone.
type ZoneImport struct {
Text string `json:"text"`
// Mode "merge" adds missing records; "replace" makes the zone equal
// to the file (deleting other records).
Mode string `json:"mode"`
DryRun bool `json:"dry_run,omitempty"`
// UseSOA also takes the SOA timers and default TTL from the file.
UseSOA bool `json:"use_soa,omitempty"`
}