adminrpc

package
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 48 Imported by: 0

Documentation

Overview

Package adminrpc implements the moth.admin.v1 connect services.

Index

Constants

View Source
const (
	ActionProjectCreate    = "project.create"
	ActionProjectUpdate    = "project.update"
	ActionProjectDelete    = "project.delete"
	ActionSecretKeyRegen   = "project.secret_key.regenerate"
	ActionSigningKeyReset  = "signing_key.reset"
	ActionSigningKeyRotate = "signing_key.rotate"
	ActionProjectExport    = "project.export"
	ActionProjectImport    = "project.import"
	ActionProviderUpdate   = "provider.update"
	ActionUserCreate       = "user.create"
	ActionUserUpdate       = "user.update"
	ActionUserDisable      = "user.disable"
	ActionUserEnable       = "user.enable"
	ActionUserDelete       = "user.delete"
	ActionUserSessionsRvk  = "user.sessions.revoke"
	ActionUserPwReset      = "user.password_reset.send"
	ActionPATCreate        = "pat.create"
	ActionPATRevoke        = "pat.revoke"
	ActionAdminInvite      = "admin.invite"
	ActionAdminAccept      = "admin.invite.accept"
	ActionAdminPassword    = "admin.password.change"
	ActionSMTPUpdate       = "smtp.update"
	ActionThemeUpdate      = "theme.update"
	ActionThemeRestore     = "theme.restore"
	ActionThemeReset       = "theme.reset"
	ActionThemeLogoUpload  = "theme.logo.upload"
	ActionThemeLogoDelete  = "theme.logo.delete"
	// Milestone 11 — subscriptions & entitlements.
	ActionEntitlementCreate  = "entitlement.create"
	ActionEntitlementUpdate  = "entitlement.update"
	ActionEntitlementDelete  = "entitlement.delete"
	ActionProductCreate      = "product.create"
	ActionProductUpdate      = "product.update"
	ActionProductDelete      = "product.delete"
	ActionGrantCreate        = "subscription.grant"
	ActionGrantRevoke        = "subscription.grant.revoke"
	ActionBillingCredsUpdate = "billing.credentials.update"
	// Milestone 12 — store catalog provisioning.
	ActionStoreCatalogSync = "billing.store_catalog.sync"
	ActionOfferingReorder  = "billing.offering.reorder"
	// Milestone 13 — themed paywall.
	ActionPaywallUpdate  = "paywall.update"
	ActionPaywallRestore = "paywall.restore"

	ActionCopyUpdate   = "copy.update"
	ActionCopyReset    = "copy.reset"
	ActionCopyRestore  = "copy.restore"
	ActionPaywallReset = "paywall.reset"

	// Milestone 20 — push device registry.
	ActionPushSettingsUpdate = "push.settings.update"
	ActionPushDeviceRevoke   = "push.device.revoke"

	// Milestone 22 — setup profile (the creation wizard's answers).
	ActionProfileUpdate = "profile.update"
)

Audit action names. Machine-readable, dotted <target>.<verb>; the human summary carries the specifics.

View Source
const CookieName = "moth_admin_session"

CookieName is the admin session cookie.

View Source
const SessionTTL = 7 * 24 * time.Hour

SessionTTL is how long an admin stays logged in.

Variables

This section is empty.

Functions

func AdminFromContext

func AdminFromContext(ctx context.Context) (store.Admin, bool)

AdminFromContext returns the admin authenticated by the auth interceptor.

func ClearCookie

func ClearCookie(secure bool) *http.Cookie

ClearCookie returns an expired session cookie.

func IssueSession

func IssueSession(ctx context.Context, st store.SessionStore, adminID string, secure bool) (*http.Cookie, error)

IssueSession creates a server-side session for adminID and returns the cookie to set. Shared with the first-run setup HTTP handler.

func NewAuthInterceptor

func NewAuthInterceptor(st Store) connect.UnaryInterceptorFunc

NewAuthInterceptor authenticates every admin RPC (except the public procedures): either the admin session cookie (the SPA) or a personal access token presented as `authorization: Bearer moth_pat_...` (the CLI). Both paths inject the same admin identity into the context, so every admin handler works unchanged over either credential.

func NewID

func NewID() string

NewID returns a UUIDv7 string (time-sortable primary keys).

func PATProto

PATProto converts a stored personal access token to its proto metadata (exported: the local `moth admin token` commands reuse it for --json).

func SessionHashFromContext

func SessionHashFromContext(ctx context.Context) (string, bool)

SessionHashFromContext returns the hash of the session token that authenticated this request, so handlers can spare the current session when ending the others. Absent on PAT-authenticated requests.

func Slugify

func Slugify(name string) string

Slugify lowercases name and reduces it to [a-z0-9-].

Types

type AccountHandler

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

AccountHandler implements moth.admin.v1.AdminAccountService.

func NewAccountHandler

func NewAccountHandler(st Store, mailer mailpkg.Mailer, baseURL string, secure bool, smtpOn func() bool, auditor *Auditor) *AccountHandler

NewAccountHandler builds the admin account service.

type AnalyticsHandler

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

func NewAnalyticsHandler

func NewAnalyticsHandler(st Store, rollup *analytics.Rollup, now func() time.Time) *AnalyticsHandler

NewAnalyticsHandler builds the analytics service. now is injectable for tests; nil means time.Now.

func (*AnalyticsHandler) GetSubscriptionStats

GetSubscriptionStats serves the subscription revenue dashboards from the pre-aggregated monthly rollup (subscription_monthly_stats / subscription_tier_stats) — it never scans raw subscription_events. Money is per currency, never blended.

type AuditHandler

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

AuditHandler implements moth.admin.v1.AuditService.

func NewAuditHandler

func NewAuditHandler(st store.AuditStore) *AuditHandler

NewAuditHandler builds the audit viewer service.

func (*AuditHandler) ListAuditLog

ListAuditLog returns audit entries newest-first, narrowed by the filters and paged with page_size / page_token. The token is the id of the last row of the previous page (audit ids are UUIDv7, so id order is time order).

type Auditor

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

Auditor records admin actions, attributing each to the credential that authenticated the request (browser session vs personal access token) and tagging it with the coarse client IP. Writes never fail the request.

func NewAuditor

func NewAuditor(sink *audit.Sink) *Auditor

NewAuditor builds an Auditor over the audit sink.

type BillingHandler

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

BillingHandler implements the four moth.admin.v1 billing services: EntitlementService, ProductService, SubscriptionService and BillingCredentialsService. One struct backs all four so they share the store, master key and audit sink; server.New registers it under each service path.

func NewBillingHandler

func NewBillingHandler(st Store, master keys.MasterKey, auditor *Auditor, now func() time.Time) *BillingHandler

NewBillingHandler builds the admin billing services. now is injectable for tests; nil means time.Now.

type CopyHandler

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

CopyHandler implements moth.admin.v1.CopyService: the per-project localization overrides on top of moth's bundled catalog (internal/i18n). It mirrors ThemeService — a curated, closed key set with bundled defaults, per-project overrides, and versioned revisions — keyed by screen × locale instead of by color. Every override save is validated against the catalog (known key, required placeholders, length) and recorded as a new revision; the store keeps the last store.CopyRevisionKeep for undo.

func NewCopyHandler

func NewCopyHandler(st Store, auditor *Auditor) *CopyHandler

NewCopyHandler builds the copy service.

type Credential

type Credential struct {
	Type CredentialType
	// PATID is the personal access token id when Type is CredentialPAT.
	PATID string
	// PATExpiresAt is that token's expiry (nil: never expires), so
	// CreatePersonalAccessToken can cap PAT-minted tokens at the creating
	// token's remaining lifetime.
	PATExpiresAt *time.Time
}

Credential records how a request was authenticated, so actions can be attributed to the exact credential (the milestone-10 audit log).

func CredentialFromContext

func CredentialFromContext(ctx context.Context) (Credential, bool)

CredentialFromContext returns which credential authenticated this request (cookie session or personal access token).

type CredentialType

type CredentialType string

CredentialType is the kind of credential that authenticated a request.

const (
	// CredentialSession is the browser session cookie (the admin SPA).
	CredentialSession CredentialType = "session"
	// CredentialPAT is a personal access token (the CLI, scripts).
	CredentialPAT CredentialType = "pat"
)

type LiveStoreSyncer

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

LiveStoreSyncer is the production storeSyncer wired by server.New. It drives the real store-catalog clients from moth's encrypted billing credentials.

Apple: the App Store Connect *catalog* API key is never persisted by moth, so this cannot create Apple subscriptions — it returns a guided ManualStep pointing at `moth setup billing` (which supplies the ASC key in-process). This is the honest boundary from the capability spike, not a silent skip.

Google: the service account is stored, so the Android Publisher subscription push runs here. The RTDN Pub/Sub topic needs a separate pubsub-scoped credential moth does not hold, so topic/subscription wiring degrades to a guided notification result.

Stripe: the secret key is stored and the Stripe API can do everything, so the catalog push (Products + recurring Prices) is fully automated here. The webhook endpoint is NOT wired from this path — Stripe reveals the signing secret exactly once at creation and `moth setup billing` persists it in the same run; the handler surfaces a guided step until the secret is stored.

func NewLiveStoreSyncer

func NewLiveStoreSyncer(master keys.MasterKey) LiveStoreSyncer

NewLiveStoreSyncer builds the production storeSyncer for server.New. The returned value drives the real Android Publisher client for Google and emits a guided CLI step for Apple (the ASC catalog key is never persisted).

func (LiveStoreSyncer) Sync

func (s LiveStoreSyncer) Sync(ctx context.Context, storeName, slug, baseURL string, creds store.BillingCredentials, cat setup.DesiredCatalog) (*setup.SyncResult, error)

type MonetizationHandler

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

MonetizationHandler implements moth.admin.v1.MonetizationService: it reads a project's catalog + offering, reports store-connection/drift status, and reconciles the catalog into App Store Connect / Google Play.

Honest automation (plan/12): the App Store Connect *catalog* API key is write-only and never persisted server-side (moth stores only the App Store Server API IAP key, for the milestone-11 engine), so the admin cannot itself create Apple subscriptions — SyncStoreCatalog computes the plan/diff and emits the exact guided steps (including the `moth setup billing` command, which holds the ASC key in-process). Google's service account IS stored, so its catalog push can run server-side through the injected syncer. Every live store call sits behind the storeSyncer interface, nil for local/guided-only mode, so the handler is testable with no network.

func NewMonetizationHandler

func NewMonetizationHandler(st Store, master keys.MasterKey, baseURL string, auditor *Auditor, syncer storeSyncer, now func() time.Time) *MonetizationHandler

NewMonetizationHandler builds the monetization service. syncer may be nil (local/guided-only reconciliation); now defaults to time.Now.

func (*MonetizationHandler) GetStoreCatalogStatus

GetStoreCatalogStatus reports each store's credential/notification wiring and a per-product sync state. It computes a *live moth-side diff*: a product recorded in_sync whose price/period/listing has since changed in moth is reported DRIFT (recomputed from productRevision), not stale parity — so an edit that has not been re-pushed is visible immediately. Drift introduced outside moth (a price edited directly in Play Console / App Store Connect) is not observable from a persisted row; it surfaces on the next live push — for Google via the admin "Sync store catalog" action (which re-reads the store), for Apple via `moth setup billing` / `moth doctor` (moth never holds the ASC catalog key server-side, the honest boundary from the capability spike).

type PaywallHandler

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

PaywallHandler implements moth.admin.v1.PaywallService: the per-project paywall configuration (plan/13). Configs are validated by internal/paywall (bounded copy, known layout, http(s) legal links) before every save, and every save is a new revision (the store keeps the last store.PaywallRevisionKeep for undo). It mirrors ThemeHandler exactly, minus the logo asset plumbing — the paywall inherits colors/typography/logo from the theme and owns no assets of its own.

func NewPaywallHandler

func NewPaywallHandler(st Store, auditor *Auditor) *PaywallHandler

NewPaywallHandler builds the paywall service.

type ProfileHandler

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

ProfileHandler implements moth.admin.v1.ProfileService: the project setup profile (the milestone-22 wizard's answers — plain config, full replacement, no revisions, like the push settings) and the derived setup checklist. The checklist is recomputed from live configuration on every call using the same predicates `moth doctor` runs (internal/setup); nothing about completeness is ever stored, so it cannot go stale.

func NewProfileHandler

func NewProfileHandler(st Store, auditor *Auditor, smtpConfigured func() bool, now func() time.Time) *ProfileHandler

NewProfileHandler builds the profile admin service. smtpConfigured is the instance settings handler's SMTPConfigured (nil: SMTP treated as not configured); now is injectable for tests (nil: time.Now).

func (*ProfileHandler) GetProfile

GetProfile returns the project's setup profile; a project created before the wizard has none (has_profile false, no profile message).

func (*ProfileHandler) GetProjectSetupStatus

GetProjectSetupStatus derives the outstanding setup checklist from live configuration plus the profile's intent. Features the profile did not choose produce no item; each item disappears the moment the underlying configuration exists, however it got there (tabs, CLI, a teammate). Projects without a profile get an empty list and has_profile false.

func (*ProfileHandler) UpdateProfile

UpdateProfile validates and installs a full replacement of the setup profile.

type ProjectHandler

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

ProjectHandler implements moth.admin.v1.ProjectService.

func NewProjectHandler

func NewProjectHandler(st Store, master keys.MasterKey, baseURL string, auditor *Auditor) *ProjectHandler

NewProjectHandler builds the project service. The master key encrypts each new project's signing key at rest.

func (*ProjectHandler) ExportProject

ExportProject returns every user of the project with password hashes and provider identities, for migration off moth.

func (*ProjectHandler) ImportProject

ImportProject bulk-creates users from a migration document, carrying their (possibly foreign) password hashes so the first sign-in verifies with the original algorithm and rehashes to argon2id. Users whose email already exists in the project are skipped.

func (*ProjectHandler) RotateSigningKey

RotateSigningKey mints a fresh active signing key and moves the current key to grace status: it stays in the JWKS until grace_expire_time so in-flight access tokens keep validating and no user is signed out. Unlike ResetSigningKey it never revokes refresh tokens.

type PushHandler

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

PushHandler implements moth.admin.v1.PushService: the project push settings (enable switch + Web Push VAPID public key — plain config, full replacement, no revisions, unlike the paywall/theme documents) and the user detail Devices panel. Registrations are metadata-only here: push tokens are credentials and appear exclusively on the secret-key surface (moth.server.v1), never in admin responses.

func NewPushHandler

func NewPushHandler(st Store, auditor *Auditor, now func() time.Time) *PushHandler

NewPushHandler builds the push admin service. now is injectable for tests; nil means time.Now.

func (*PushHandler) GetPushSettings

GetPushSettings returns the project's push settings; a project that never configured push gets the defaults (disabled, no VAPID key).

func (*PushHandler) ListPushDevices

ListPushDevices returns the project's active registrations for the Push tab, newest first with keyset pagination and an optional target filter. Each row carries the owning user's id and email so the operator can tell whose device it is; tokens never appear (adminv1.PushDevice has no token field). Project-wide per-target totals ride along on every page.

func (*PushHandler) ListUserPushDevices

ListUserPushDevices returns one user's registrations for the user detail Devices panel, most recently seen first — active and revoked (revocation is auditable, not a delete), never the tokens.

func (*PushHandler) RevokePushDevice

RevokePushDevice revokes one registration by its row id (`admin` reason, audit-logged). Idempotent: revoking an already-revoked registration succeeds, keeps the original reason and is not re-audited.

func (*PushHandler) UpdatePushSettings

UpdatePushSettings validates and installs a full replacement of the push settings.

type SessionHandler

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

SessionHandler implements moth.admin.v1.SessionService.

func NewSessionHandler

func NewSessionHandler(st Store, secure bool) *SessionHandler

NewSessionHandler builds the session service. secure controls the Secure attribute on cookies (true when the instance is served over https).

type SettingsHandler

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

SettingsHandler implements moth.admin.v1.InstanceSettingsService.

The effective SMTP configuration is resolved with this precedence: database (set through this service) > config file / environment > none (console transport). Updates swap the shared dynamic mailer in place, so no restart is needed.

func NewSettingsHandler

func NewSettingsHandler(ctx context.Context, st Store, cfg config.Config, dyn *mailpkg.Dynamic, fallback mailpkg.Mailer, master keys.MasterKey, auditor *Auditor) (*SettingsHandler, error)

NewSettingsHandler builds the instance settings service and points dyn at the effective SMTP transport.

func (*SettingsHandler) SMTPConfigured

func (h *SettingsHandler) SMTPConfigured() bool

SMTPConfigured reports whether a real SMTP transport is currently effective (used to decide whether invite emails actually go out).

type ThemeHandler

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

ThemeHandler implements moth.admin.v1.ThemeService: the per-project design system (plan/06). Token sets are validated by internal/theme — WCAG AA contrast included — before every save, and every save is a new revision (the store keeps the last store.ThemeRevisionKeep for undo).

func NewThemeHandler

func NewThemeHandler(st Store, uploadsDir string, auditor *Auditor) *ThemeHandler

NewThemeHandler builds the theme service. uploadsDir is created lazily on the first logo upload.

type UserHandler

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

UserHandler implements moth.admin.v1.UserService — the operator's user management, a cookie-authed façade over the same domain layer as moth.server.v1.

func NewUserHandler

func NewUserHandler(st Store, auth *authrpc.Handler, mailer mailpkg.Mailer, ev *events.Writer, auditor *Auditor) *UserHandler

NewUserHandler builds the admin user service. events may be nil (tests).

Jump to

Keyboard shortcuts

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