service

package
v1.3.3 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: AGPL-3.0 Imports: 43 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DefaultTombstoneRetention bounds how long a deletion stays reportable.
	// Ninety days is long enough that a client offline for a season still
	// resyncs incrementally, and short enough that the table stays small after
	// a bulk delete.
	DefaultTombstoneRetention = 90 * 24 * time.Hour

	// DefaultDeletionPageSize / MaxDeletionPageSize bound the feed. A
	// delete-all writes one tombstone per flight, so this endpoint must page.
	DefaultDeletionPageSize = 100
	MaxDeletionPageSize     = 500
)
View Source
const (
	DefaultDeliveryEventLimit = 100
	MaxDeliveryEventLimit     = 500
)

DefaultDeliveryEventLimit / MaxDeliveryEventLimit bound the admin listing.

View Source
const (
	// DefaultIdempotencyTTL is how long a key stays replayable. A day covers
	// the realistic offline window — a flight logged on a phone with no
	// signal, replayed once the aircraft is back on the ground — without
	// keeping response bodies around indefinitely.
	DefaultIdempotencyTTL = 24 * time.Hour

	// DefaultIdempotencyLease is how long an in-progress claim is honoured
	// before another request may take it over. It must exceed the request
	// timeout (15s) so a slow-but-alive request is never displaced, and stay
	// short enough that a crashed process does not wedge a key for the whole
	// retention window.
	DefaultIdempotencyLease = 60 * time.Second

	// DefaultIdempotencyMaxResponseBytes caps what is stored per record.
	// Mutating endpoints return a single entity or a short summary; the cap
	// exists so an outlier (a large import summary, a PDF) cannot turn this
	// table into a blob store.
	DefaultIdempotencyMaxResponseBytes = 256 << 10
)

Defaults for idempotent-write bookkeeping. All three are overridable at startup (IDEMPOTENCY_TTL, IDEMPOTENCY_LEASE, IDEMPOTENCY_MAX_RESPONSE_BYTES).

View Source
const (
	// DefaultOIDCLoginStateTTL bounds how long a pending authorization request
	// stays usable. Generous enough to type a password and pass MFA at the
	// provider, short enough that abandoned logins age out quickly.
	DefaultOIDCLoginStateTTL = 10 * time.Minute

	// DefaultOIDCHandoffTTL bounds the window between the provider redirecting
	// the browser back and the SPA exchanging its code for tokens. This is a
	// single automatic request, so it needs seconds, not minutes.
	DefaultOIDCHandoffTTL = 60 * time.Second
)

OIDC login mode.

NinerLog runs in exactly one authentication mode. Setting OIDC_ISSUER switches the deployment to OIDC and turns off every local credential path — passwords, registration, password reset, TOTP and passkeys — because in that mode the identity provider owns accounts entirely and a surviving local credential would be a way around it.

View Source
const (
	// PasswordMinLength is the shortest accepted password.
	PasswordMinLength = 12
	// PasswordMaxLength is the longest accepted password. bcrypt silently
	// truncates input beyond 72 bytes, so anything longer would hash to the
	// same digest as its 72-byte prefix.
	PasswordMaxLength = 72
)

Local password policy, shared by every flow that sets a password: registration, password reset and password change.

View Source
const (
	DefaultVerificationReminderAfter = 24 * time.Hour
	DefaultUnverifiedRetention       = 30 * 24 * time.Hour
	DefaultUnverifiedSweepInterval   = time.Hour
)

Defaults for the unverified-account lifecycle.

An account that never verifies its address cannot log in, so it holds nothing but a row and an email address. It gets one reminder a day after signup — enough time that the reminder does not race the original message into the same inbox — and that reminder starts a 30-day clock. If the address is still unverified when the clock runs out, the account is deleted.

View Source
const (
	// CleanupDisabledByOIDC is the one that is not configurable. In OIDC mode
	// the identity provider owns the account lifecycle: local registration and
	// email verification are switched off entirely, so "unverified" no longer
	// means "an abandoned signup". It means the provider did not assert
	// email_verified — for an account its owner may be using daily. Reaping on
	// that signal would delete live accounts, so the feature is refused in OIDC
	// mode outright rather than left to configuration.
	CleanupDisabledByOIDC = "oidc_mode"
	// CleanupDisabledNoSMTP: without SMTP, registration marks accounts verified
	// on the spot. There is nothing unverified to chase, and anything that is
	// unverified was never asked to confirm.
	CleanupDisabledNoSMTP = "smtp_not_configured"
	// CleanupDisabledByConfig: UNVERIFIED_CLEANUP_ENABLED=false.
	CleanupDisabledByConfig = "disabled_by_configuration"
)

Reasons the reaper is not running, reported to administrators so a disabled reaper is explained rather than merely absent.

View Source
const (
	// DefaultWebAuthnSessionTTL is used when WEBAUTHN_SESSION_TTL is unset.
	DefaultWebAuthnSessionTTL = 5 * time.Minute
	// DefaultWebAuthnMaxOpenCeremonies is used when
	// WEBAUTHN_MAX_OPEN_CEREMONIES is unset. Generous for legitimate use
	// (phone, laptop, tablet, plus retries) while bounding a compromised
	// account to a trivial footprint.
	DefaultWebAuthnMaxOpenCeremonies = 10
)

Variables

View Source
var (
	ErrAircraftNotFound      = errors.New("aircraft not found")
	ErrUnauthorizedAircraft  = errors.New("unauthorized access to aircraft")
	ErrDuplicateRegistration = errors.New("aircraft registration already exists")
)
View Source
var (
	ErrUserAlreadyExists  = errors.New("user already exists")
	ErrInvalidCredentials = errors.New("invalid credentials")
	ErrInvalidToken       = errors.New("invalid token")
	ErrTokenExpired       = errors.New("token expired")
	ErrTokenRevoked       = errors.New("token revoked")
	ErrTokenUsed          = errors.New("token already used")
	ErrAccountLocked      = errors.New("account temporarily locked due to too many failed login attempts")
	ErrAccountDisabled    = errors.New("account disabled by administrator")
	ErrEmailNotVerified   = errors.New("email address not verified")
	ErrPasswordTooShort   = errors.New("password must be at least 12 characters")
	ErrPasswordTooLong    = errors.New("password must not exceed 72 characters")
	ErrPasswordTooWeak    = errors.New("password must contain at least one lowercase letter, one uppercase letter, one digit and one special character")
	ErrEmailRequired      = errors.New("email is required")
	ErrPasswordRequired   = errors.New("password is required")
	ErrNameRequired       = errors.New("name is required")
	ErrInvalidEmail       = errors.New("invalid email format")
	ErrEmailTooLong       = errors.New("email must not exceed 255 characters")
	// ErrPasswordNotSet marks an account that has no local password — an OIDC
	// account. Password-based operations are refused outright rather than
	// compared against an empty hash.
	ErrPasswordNotSet = errors.New("account has no local password")

	// ErrTwoFactorRequired is returned by ResetPassword when the account has 2FA
	// enabled and the caller supplied no code. The reset token is left unused so
	// the caller can retry with a code.
	ErrTwoFactorRequired = errors.New("two-factor authentication code required")
	// ErrTwoFactorUnavailable is returned when a 2FA-protected reset is attempted
	// but no validator is wired. It fails the reset closed rather than falling
	// back to bypassing the second factor.
	ErrTwoFactorUnavailable = errors.New("two-factor verification is unavailable")
)
View Source
var (
	ErrClassRatingNotFound     = errors.New("class rating not found")
	ErrInvalidClassType        = errors.New("invalid class type")
	ErrUnauthorizedClassRating = errors.New("unauthorized access to class rating")
)
View Source
var (
	ErrContactNotFound     = errors.New("contact not found")
	ErrUnauthorizedContact = errors.New("unauthorized access to contact")

	// ErrContactNameExists is returned when a create or rename would give the
	// user two contacts with the same name. A contact is identified by its
	// name, so returning the pre-existing row instead would hand the caller a
	// different resource than the one it asked to create; the caller is told
	// about the collision and can look the existing one up.
	ErrContactNameExists = errors.New("a contact with this name already exists")

	// ErrContactNameRequired is returned for an empty or whitespace-only name.
	ErrContactNameRequired = errors.New("contact name is required")
)
View Source
var (
	ErrCredentialNotFound     = errors.New("credential not found")
	ErrUnauthorizedCredential = errors.New("unauthorized access to credential")
	ErrExpiryBeforeIssue      = errors.New("expiry date must be after issue date")
)
View Source
var (
	// ErrDocumentFilesDisabled is returned from every image endpoint when the
	// feature is switched off (DOCUMENT_FILES_ENABLED=false). It covers reads
	// as well as writes: serving multi-megabyte blobs is itself the bandwidth
	// half of the abuse surface an operator turns this off to close. Stored
	// rows are left untouched and become reachable again when it is turned
	// back on.
	ErrDocumentFilesDisabled = errors.New("document image uploads are disabled on this server")

	// ErrDocumentFileNotFound covers both "no such image" and "that image
	// belongs to a different document or user" — the caller cannot tell them
	// apart, so an id cannot be probed through someone else's URL.
	ErrDocumentFileNotFound = errors.New("document image not found")

	// ErrDocumentSubjectNotFound means the licence or credential the image
	// would hang off does not exist or is not the caller's.
	ErrDocumentSubjectNotFound = errors.New("document not found")

	ErrDocumentFileEmpty        = errors.New("image file is empty")
	ErrDocumentFileTooLarge     = errors.New("image exceeds the maximum size")
	ErrDocumentFileUnsupported  = errors.New("unsupported image format")
	ErrDocumentFileCorrupt      = errors.New("image could not be decoded")
	ErrDocumentFileTooManyPixel = errors.New("image resolution is too large")
	ErrDocumentFileLimitReached = errors.New("maximum number of images for this document reached")
)
View Source
var (
	ErrFlightNotFound     = errors.New("flight not found")
	ErrInvalidFlight      = errors.New("invalid flight data")
	ErrUnauthorizedFlight = errors.New("unauthorized access to flight")
	// ErrFlightLocked is returned when attempting to update or delete a
	// flight that carries a completed, non-voided instructor signature. The
	// signature must be voided (see FlightSignatureService.Void) before the
	// flight can be edited or deleted again.
	ErrFlightLocked = errors.New("flight is locked by a completed signature")
)
View Source
var (
	// ErrSignatureNotFound covers both "no signature with this ID" and, for
	// the public token flow, "no signature with this token" — the latter is
	// aliased to ErrSignatureTokenInvalid so a lookup miss and an
	// already-used token are indistinguishable to an anonymous caller.
	ErrSignatureNotFound = errors.New("signature request not found")

	// ErrSignaturePendingExists is returned when creating a new deferred
	// request while one is already pending for the flight.
	ErrSignaturePendingExists = errors.New("a pending signature request already exists for this flight")

	// ErrSignatureTokenInvalid covers an unknown, revoked, voided, or
	// already-completed token. Deliberately not distinguished from each
	// other (or from "never existed") to avoid leaking state to an
	// unauthenticated caller.
	ErrSignatureTokenInvalid = errors.New("signature link is invalid")
	// ErrSignatureTokenExpired means the token's expiry has passed.
	ErrSignatureTokenExpired = errors.New("signature link has expired")

	// ErrSignatureNotPending is returned by Resend/Revoke when the target
	// signature is not (or no longer) in the 'pending' state.
	ErrSignatureNotPending = errors.New("signature request is not pending")
	// ErrSignatureNotCompleted is returned by Void when the target
	// signature is not in the 'completed' state.
	ErrSignatureNotCompleted = errors.New("signature is not completed")

	ErrSignatureImageTooLarge  = errors.New("signature image exceeds maximum size")
	ErrSignatureImageRequired  = errors.New("a signature image is required")
	ErrSignerNameRequired      = errors.New("signer name is required")
	ErrSignatureReasonRequired = errors.New("a reason is required to void a signature")

	// ErrSignatureEmailQuota is returned once a user has triggered too many
	// signature-request emails in a day. Each request sends NinerLog-branded
	// mail, from NinerLog's domain, to an arbitrary address chosen by the
	// sender, and the landing page then collects the recipient's name,
	// credential number and handwritten signature. Without a ceiling that is a
	// ready-made bulk phishing and harvesting channel.
	ErrSignatureEmailQuota = errors.New("daily limit for signature request emails reached")
)
View Source
var (
	ErrLicenseNotFound      = errors.New("license not found")
	ErrInvalidLicense       = errors.New("invalid license data")
	ErrLicenseAlreadyExists = errors.New("license already exists")
	ErrUnauthorizedAccess   = errors.New("unauthorized access to license")
)
View Source
var (
	// NotificationCheckRunsTotal counts total background notification check runs.
	NotificationCheckRunsTotal = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "notification_check_runs_total",
			Help: "Total number of background notification check runs.",
		},
	)

	// NotificationCheckDurationSeconds tracks how long each notification check takes.
	NotificationCheckDurationSeconds = prometheus.NewHistogram(
		prometheus.HistogramOpts{
			Name:    "notification_check_duration_seconds",
			Help:    "Duration of each background notification check run in seconds.",
			Buckets: prometheus.DefBuckets,
		},
	)

	// NotificationsSentTotal counts notifications sent by type.
	NotificationsSentTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "notifications_sent_total",
			Help: "Total number of notifications sent.",
		},
		[]string{"type"},
	)

	// NotificationCheckErrorsTotal counts notification check runs that aborted
	// early due to an error (e.g. failing to load user preferences).
	NotificationCheckErrorsTotal = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "notification_check_errors_total",
			Help: "Total number of background notification check runs that failed.",
		},
	)

	// NotificationLastSuccessTimestampSeconds records the Unix timestamp of the
	// last successfully completed notification check. Use it to alert when the
	// background checker stops running (staleness).
	NotificationLastSuccessTimestampSeconds = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Name: "notification_last_success_timestamp_seconds",
			Help: "Unix timestamp of the last successfully completed notification check.",
		},
	)
)
View Source
var (
	// ErrOIDCProviderUnavailable means the issuer's discovery document could
	// not be fetched. Distinct from a misconfiguration: the deployment is set
	// up correctly and the provider is simply unreachable right now.
	ErrOIDCProviderUnavailable = errors.New("oidc provider is unavailable")

	// ErrOIDCInvalidState covers every unusable authorization response: an
	// unknown, expired or already-consumed state, and a state presented by a
	// different browser than the one that started the login. They are
	// deliberately indistinguishable.
	ErrOIDCInvalidState = errors.New("oidc login state is invalid or has expired")

	// ErrOIDCExchangeFailed means the provider rejected the authorization code
	// or returned a token response without an ID token.
	ErrOIDCExchangeFailed = errors.New("oidc token exchange failed")

	// ErrOIDCInvalidToken means the ID token failed signature, issuer,
	// audience, expiry or nonce validation.
	ErrOIDCInvalidToken = errors.New("oidc id token is invalid")

	// ErrOIDCEmailMissing means the ID token carried no usable email address.
	// NinerLog keys logbooks, signature requests and notifications off the
	// address, so an account cannot be provisioned without one.
	ErrOIDCEmailMissing = errors.New("oidc id token contains no email address")

	// ErrOIDCEmailConflict means the address belongs to an existing local
	// account that this identity is not allowed to adopt.
	ErrOIDCEmailConflict = errors.New("an account with this email address already exists")

	// ErrOIDCHandoffInvalid means the single-use handoff code was unknown,
	// expired or already redeemed.
	ErrOIDCHandoffInvalid = errors.New("oidc handoff code is invalid or has expired")
)

OIDC-specific sentinel errors. Handlers map these to status codes; none of them are ever echoed back to the browser verbatim.

View Source
var (
	ErrTwoFactorAlreadyEnabled = errors.New("two-factor authentication is already enabled")
	ErrTwoFactorNotEnabled     = errors.New("two-factor authentication is not enabled")
	ErrInvalidTOTPCode         = errors.New("invalid TOTP code")
	ErrInvalid2FAToken         = errors.New("invalid two-factor token")
)
View Source
var (
	// UnverifiedRemindersTotal counts follow-up verification emails by outcome.
	//
	// Results:
	//   sent          — the reminder was delivered; the deletion clock started.
	//   undeliverable — the address refused mail permanently; the clock started
	//                   anyway, because no later attempt would do better.
	//   deferred      — a transient failure; the account is retried next sweep.
	//   error         — the token could not be minted; nothing was sent.
	UnverifiedRemindersTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "unverified_account_reminders_total",
			Help: "Follow-up email-verification reminders by outcome.",
		},
		[]string{"result"},
	)

	// UnverifiedAccountsDeletedTotal counts accounts reaped for never having
	// verified their address. Irreversible, so it is worth alerting on an
	// unexpected jump.
	UnverifiedAccountsDeletedTotal = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "unverified_accounts_deleted_total",
			Help: "Accounts deleted after failing to verify their email address within the retention window.",
		},
	)
)
View Source
var (
	ErrWebAuthnNotConfigured = errors.New("webauthn is not configured")
	// ErrWebAuthnSessionNotFound is returned for every unusable ceremony
	// handle — expired, already consumed, wrong ceremony, scoped to another
	// user, or never issued. Keeping these indistinguishable denies an
	// attacker any signal about which handles once existed.
	ErrWebAuthnSessionNotFound   = errors.New("webauthn session not found or expired")
	ErrWebAuthnInvalidResponse   = errors.New("invalid webauthn response")
	ErrWebAuthnUnknownCredential = errors.New("unknown webauthn credential")
	ErrWebAuthnVerification      = errors.New("webauthn verification failed")
)
View Source
var (
	// WebAuthnSessionsCreatedTotal counts ceremony sessions opened, split by
	// ceremony ("registration" | "login").
	WebAuthnSessionsCreatedTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "webauthn_sessions_created_total",
			Help: "Total number of WebAuthn ceremony sessions opened.",
		},
		[]string{"ceremony"},
	)

	// WebAuthnSessionsConsumedTotal counts finish attempts by ceremony and
	// result ("ok" | "rejected"). A rejection covers every unusable handle:
	// expired, already consumed, wrong ceremony, or forged.
	WebAuthnSessionsConsumedTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "webauthn_sessions_consumed_total",
			Help: "Total number of WebAuthn ceremony finish attempts by result.",
		},
		[]string{"ceremony", "result"},
	)

	// WebAuthnSessionsEvictedTotal counts sessions dropped by the per-user open
	// ceremony cap. Sustained growth means users are abandoning ceremonies.
	WebAuthnSessionsEvictedTotal = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "webauthn_sessions_evicted_total",
			Help: "Total number of WebAuthn sessions evicted by the per-user open ceremony cap.",
		},
	)

	// WebAuthnSessionsExpiredTotal counts rows removed by the cleanup reaper.
	// Sustained growth indicates abandoned ceremonies, which is the signal that
	// matters for unauthenticated discoverable-login writes.
	WebAuthnSessionsExpiredTotal = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "webauthn_sessions_expired_total",
			Help: "Total number of expired WebAuthn sessions removed by the cleanup job.",
		},
	)
)
View Source
var ErrIdempotencyUnavailable = errors.New("idempotency store unavailable")

ErrIdempotencyUnavailable is returned when the replay store cannot be reached. Callers must fail the request rather than execute it: the client asked for an exactly-once guarantee, and silently downgrading to at-least-once is how duplicate logbook entries get created.

View Source
var ErrInvalidDeletionEntity = errors.New("unknown deletion entity type")

ErrInvalidDeletionEntity is returned for an entity filter outside the enum. Answering with an empty feed instead would read to a sync client as "nothing was deleted", which is the one answer that must never be wrong.

View Source
var ErrInvalidSessionEvent = errors.New("invalid flight session event")
View Source
var ErrOIDCNotConfigured = errors.New("oidc is not configured")

ErrOIDCNotConfigured is returned by every OIDC operation on a deployment that has not set OIDC_ISSUER.

Functions

func GetCheckInterval

func GetCheckInterval() time.Duration

GetCheckInterval reads NOTIFICATION_CHECK_INTERVAL env var, defaults to 1h

func IdempotencyStatus added in v1.3.2

func IdempotencyStatus(o IdempotencyOutcome) int

IdempotencyStatus maps an outcome to the HTTP status a client should see. Kept next to the outcomes so the transport layer does not re-derive it.

func UnverifiedCleanupDisabledReason added in v1.3.2

func UnverifiedCleanupDisabledReason(smtpConfigured, oidcEnabled bool) string

UnverifiedCleanupDisabledReason reports why the reaper must not run, or "" if it may. Deleting accounts is irreversible, so every condition that forbids it is decided in one place rather than spread across the composition root.

Types

type AircraftService

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

func NewAircraftService

func NewAircraftService(aircraftRepo repository.AircraftRepository) *AircraftService

func (*AircraftService) CountAircraft

func (s *AircraftService) CountAircraft(ctx context.Context, userID uuid.UUID) (int, error)

func (*AircraftService) CreateAircraft

func (s *AircraftService) CreateAircraft(ctx context.Context, aircraft *models.Aircraft) error

func (*AircraftService) DeleteAircraft

func (s *AircraftService) DeleteAircraft(ctx context.Context, id, userID uuid.UUID) error

func (*AircraftService) GetAircraft

func (s *AircraftService) GetAircraft(ctx context.Context, id, userID uuid.UUID) (*models.Aircraft, error)

func (*AircraftService) GetAircraftStats added in v1.3.1

func (s *AircraftService) GetAircraftStats(ctx context.Context, userID uuid.UUID) (*AircraftStatsResult, error)

GetAircraftStats returns flight statistics aggregated per registration and per aircraft type, including informational 90-day landing recency

func (*AircraftService) ListAircraft

func (s *AircraftService) ListAircraft(ctx context.Context, userID uuid.UUID) ([]*models.Aircraft, error)

func (*AircraftService) ListAircraftUpdatedSince added in v1.3.2

func (s *AircraftService) ListAircraftUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.Aircraft, error)

ListAircraftUpdatedSince returns the user's aircraft that changed strictly after the given instant, for delta-syncing clients.

func (*AircraftService) UpdateAircraft

func (s *AircraftService) UpdateAircraft(ctx context.Context, aircraft *models.Aircraft, userID uuid.UUID, renameFlights bool) (int, error)

UpdateAircraft updates an aircraft. When renameFlights is true and the registration changes, flights logged under the old registration are repointed to the new one; the returned count is the number of flights updated.

type AircraftStatsResult added in v1.3.1

type AircraftStatsResult struct {
	ByRegistration []*models.AircraftStats
	ByType         []*models.AircraftTypeStats
}

AircraftStatsResult bundles per-registration and per-type flight statistics

type AuthService

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

func NewAuthService

func NewAuthService(
	userRepo repository.UserRepository,
	refreshTokenRepo repository.RefreshTokenRepository,
	passwordResetRepo repository.PasswordResetTokenRepository,
	emailVerificationRepo repository.EmailVerificationTokenRepository,
	jwtManager *jwt.Manager,
	twoFactor TwoFactorValidator,
) *AuthService

NewAuthService constructs the service. twoFactor may be nil, in which case a password reset for an account with 2FA enabled fails with ErrTwoFactorUnavailable — the second factor is never silently skipped.

func (*AuthService) ChangePassword

func (s *AuthService) ChangePassword(ctx context.Context, userID uuid.UUID, currentPassword, newPassword string) error

ChangePassword changes the user's password after verifying the current password

func (*AuthService) CreateEmailVerificationToken added in v1.3.1

func (s *AuthService) CreateEmailVerificationToken(ctx context.Context, userID uuid.UUID) (string, error)

CreateEmailVerificationToken mints a fresh verification token for a user whose address has just changed, so the new address can be proven.

func (*AuthService) DeleteUser

func (s *AuthService) DeleteUser(ctx context.Context, userID uuid.UUID, password string) error

DeleteUser permanently deletes a user account after verifying the password

func (*AuthService) DeleteUserConfirmed added in v1.3.2

func (s *AuthService) DeleteUserConfirmed(ctx context.Context, userID uuid.UUID) error

DeleteUserConfirmed permanently deletes an account whose identity has been confirmed by means other than a password.

In OIDC mode there is no password to re-enter, so the handler confirms the destructive action by requiring the caller to type their own email address instead. The confirmation itself happens in the handler; this method is the deletion once that check has passed.

func (*AuthService) GenerateTokensForUser

func (s *AuthService) GenerateTokensForUser(ctx context.Context, userID uuid.UUID) (*TokenPair, error)

GenerateTokensForUser generates access and refresh tokens for a user (used after 2FA verification)

func (*AuthService) GetUserByID

func (s *AuthService) GetUserByID(ctx context.Context, userID uuid.UUID) (*models.User, error)

GetUserByID retrieves a user by ID

func (*AuthService) Login

func (s *AuthService) Login(ctx context.Context, input LoginInput) (*models.User, *TokenPair, error)

Login authenticates a user and returns tokens

func (*AuthService) Logout

func (s *AuthService) Logout(ctx context.Context, refreshToken string) error

Logout revokes a refresh token

func (*AuthService) MarkEmailVerified added in v1.2.6

func (s *AuthService) MarkEmailVerified(ctx context.Context, userID uuid.UUID) error

MarkEmailVerified marks a user as verified and clears any outstanding verification tokens. Used for environments where email delivery is disabled.

func (*AuthService) RecordLogin added in v1.3.2

func (s *AuthService) RecordLogin(ctx context.Context, user *models.User)

RecordLogin stamps a successful sign-in on the account and mirrors the value onto the in-memory user so the caller's response carries it.

Every path that hands a session to a user calls this: password login, the 2FA second factor, passkeys, OIDC, and the sign-up verification link — following that link signs the new account in on the spot, so it is a login like any other and the admin list should show its date.

A refresh is deliberately not a login: it renews a session that an earlier login already established.

Failure is logged and swallowed. The user is authenticated either way, and an administrative timestamp is not worth failing a login over.

func (*AuthService) RefreshToken

func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*TokenPair, error)

RefreshToken generates a new access token using a refresh token

func (*AuthService) Register

func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*models.User, string, error)

Register creates a new user account. The account is created with EmailVerified=false, an email-verification token is generated and stored, and the plaintext token is returned to the caller so the handler can deliver it via email. No JWT tokens are issued at this stage — the user must consume the verification token (see VerifyEmail) before they can log in.

func (*AuthService) RequestPasswordReset

func (s *AuthService) RequestPasswordReset(ctx context.Context, email string) (*PasswordResetRequest, error)

RequestPasswordReset creates a password reset token.

On success it returns the reset token together with the recipient details loaded from the database. The handler uses the database-sourced email (rather than the HTTP request) when sending the reset mail, which keeps untrusted input out of the SMTP message (CWE-640).

func (*AuthService) ResendVerification added in v1.2.5

func (s *AuthService) ResendVerification(ctx context.Context, email string) (token, userEmail, userName, locale string, err error)

ResendVerification issues a fresh verification token for the given email if (and only if) the address belongs to a known, not-yet-verified user. The returned values are empty when nothing should be sent — callers should not distinguish "unknown email" from "already verified" to avoid enumeration.

func (*AuthService) ResetPassword

func (s *AuthService) ResetPassword(ctx context.Context, token, newPassword, twoFactorCode string) (*PasswordResetResult, error)

ResetPassword resets a user's password using a reset token.

A reset does NOT disable two-factor authentication. It used to: control of the mailbox alone was enough to strip the second factor and take the account over, and the owner was never told. Instead, an account with 2FA enabled must prove the second factor here too — twoFactorCode is a TOTP code or one of the account's recovery codes, which is the self-service path for a lost authenticator. Only a user who has lost the authenticator AND every recovery code needs an admin (POST /admin/users/{userId}/reset-2fa).

The reset token is consumed only on success, so a wrong or missing code can be retried with the same link. Code guessing is bounded by the shared account lockout applied inside the validator.

func (*AuthService) UpdateUser

func (s *AuthService) UpdateUser(ctx context.Context, user *models.User) error

UpdateUser updates user information

func (*AuthService) VerifyEmail added in v1.2.5

func (s *AuthService) VerifyEmail(ctx context.Context, token string) (*models.User, *TokenPair, error)

VerifyEmail consumes a verification token and, on success, marks the user's email as verified and returns a fresh access/refresh token pair so the frontend can log the user in.

func (*AuthService) VerifyPassword added in v1.3.1

func (s *AuthService) VerifyPassword(ctx context.Context, userID uuid.UUID, password string) error

VerifyPassword checks a plaintext password against the stored hash for the given user. Used to re-authenticate before security-sensitive profile changes (e.g. changing the email address, which is both the account recovery channel and the basis for admin authorization).

type ClassRatingRepository

type ClassRatingRepository interface {
	Create(ctx context.Context, cr *models.ClassRating) error
	GetByID(ctx context.Context, id uuid.UUID) (*models.ClassRating, error)
	GetByLicenseID(ctx context.Context, licenseID uuid.UUID) ([]*models.ClassRating, error)
	Update(ctx context.Context, cr *models.ClassRating) error
	Delete(ctx context.Context, id uuid.UUID) error
}

ClassRatingRepository defines the interface for class rating data access

type ClassRatingService

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

func (*ClassRatingService) CreateClassRating

func (s *ClassRatingService) CreateClassRating(ctx context.Context, cr *models.ClassRating, userID uuid.UUID) error

func (*ClassRatingService) DeleteClassRating

func (s *ClassRatingService) DeleteClassRating(ctx context.Context, ratingID, userID uuid.UUID) error

func (*ClassRatingService) GetClassRating added in v1.3.2

func (s *ClassRatingService) GetClassRating(ctx context.Context, ratingID, userID uuid.UUID) (*models.ClassRating, error)

func (*ClassRatingService) ListClassRatings

func (s *ClassRatingService) ListClassRatings(ctx context.Context, licenseID, userID uuid.UUID) ([]*models.ClassRating, error)

func (*ClassRatingService) UpdateClassRating

func (s *ClassRatingService) UpdateClassRating(ctx context.Context, cr *models.ClassRating, userID uuid.UUID) error

type ContactService

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

func NewContactService

func NewContactService(contactRepo repository.ContactRepository) *ContactService

func (*ContactService) CreateContact

func (s *ContactService) CreateContact(ctx context.Context, contact *models.Contact) error

func (*ContactService) DeleteContact

func (s *ContactService) DeleteContact(ctx context.Context, id, userID uuid.UUID) error

DeleteContact removes a contact from the address book. It does not remove the person from any flight: flight_crew_members.contact_id is ON DELETE SET NULL and the crew row keeps the name it was logged with, so the logbook — the legal record — reads exactly the same afterwards. Only the link, the email and the phone number are lost. Deleting is therefore always allowed, even for a contact referenced by signed flights.

Re-logging the same person afterwards creates a fresh contact and links the new crew row to it; the older crew rows stay unlinked.

func (*ContactService) ExportVCard added in v1.3.3

func (s *ContactService) ExportVCard(ctx context.Context, userID uuid.UUID) ([]byte, error)

ExportVCard renders the user's contacts as a single vCard 3.0 stream, in the same name order as the contact list. Contacts the pilot has flown with carry their crew roles as CATEGORIES, so an instructor stays recognisable as one after the file leaves NinerLog.

func (*ContactService) FindOrCreateContact

func (s *ContactService) FindOrCreateContact(ctx context.Context, userID uuid.UUID, name string) (*models.Contact, bool, error)

FindOrCreateContact finds an existing contact by exact name (case-insensitive) or creates a new one. Returns the contact and whether it was newly created.

func (*ContactService) GetContact

func (s *ContactService) GetContact(ctx context.Context, id, userID uuid.UUID) (*models.Contact, error)

func (*ContactService) LinkCrewMembers added in v1.3.3

func (s *ContactService) LinkCrewMembers(ctx context.Context, userID uuid.UUID, members []models.FlightCrewMember) (int, error)

LinkCrewMembers links a single flight's crew and reports how many contacts it created. Callers processing many flights should hold a CrewLinker instead so the name cache survives between them.

func (*ContactService) ListContacts

func (s *ContactService) ListContacts(ctx context.Context, userID uuid.UUID) ([]*models.Contact, error)

func (*ContactService) ListContactsUpdatedSince added in v1.3.2

func (s *ContactService) ListContactsUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.Contact, error)

ListContactsUpdatedSince returns the user's contacts that changed strictly after the given instant, for delta-syncing clients.

func (*ContactService) NewCrewLinker added in v1.3.3

func (s *ContactService) NewCrewLinker(userID uuid.UUID) *CrewLinker

NewCrewLinker returns a linker for one user.

func (*ContactService) SearchContacts

func (s *ContactService) SearchContacts(ctx context.Context, userID uuid.UUID, query string, limit int) ([]*models.Contact, error)

func (*ContactService) UpdateContact

func (s *ContactService) UpdateContact(ctx context.Context, contact *models.Contact, userID uuid.UUID) (int, error)

UpdateContact updates a contact and returns the number of logbook crew entries whose name was rewritten to match.

Renaming a contact is a correction to the address book, and the crew entries that reference it carry a copy of the name, so the rename has to reach them or fixing a typo would leave the logbook still showing it. Entries on flights carrying a completed instructor signature are excluded: those are attested records, and the repository enforces that boundary in SQL.

Contacts a crew entry is not linked to are unaffected, which is why the pre-000060 rows that were written with a NULL contact_id keep their text until something links them.

type CredentialService

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

func NewCredentialService

func NewCredentialService(credentialRepo repository.CredentialRepository) *CredentialService

func (*CredentialService) CreateCredential

func (s *CredentialService) CreateCredential(ctx context.Context, credential *models.Credential) error

func (*CredentialService) DeleteCredential

func (s *CredentialService) DeleteCredential(ctx context.Context, id, userID uuid.UUID) error

func (*CredentialService) GetCredential

func (s *CredentialService) GetCredential(ctx context.Context, id, userID uuid.UUID) (*models.Credential, error)

func (*CredentialService) ListCredentials

func (s *CredentialService) ListCredentials(ctx context.Context, userID uuid.UUID) ([]*models.Credential, error)

func (*CredentialService) ListCredentialsUpdatedSince added in v1.3.2

func (s *CredentialService) ListCredentialsUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.Credential, error)

ListCredentialsUpdatedSince returns the user's credentials that changed strictly after the given instant, for delta-syncing clients.

func (*CredentialService) UpdateCredential

func (s *CredentialService) UpdateCredential(ctx context.Context, credential *models.Credential, userID uuid.UUID) error

type CrewLinker added in v1.3.3

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

CrewLinker resolves crew-member names to a single user's contacts across any number of flights, remembering the names it has already looked up.

Every path that writes crew rows goes through it — flight create, flight update, spreadsheet import and backup restore — so the address book is a function of the logbook rather than of which entry point the pilot happened to use. Before this existed, only import created contacts, and a pilot who logged flights in the UI ended up with an empty contact list.

Not safe for concurrent use; take one per request.

func (*CrewLinker) Created added in v1.3.3

func (l *CrewLinker) Created() int

Created reports how many contacts this linker has had to create.

func (l *CrewLinker) Link(ctx context.Context, members []models.FlightCrewMember) error

Link resolves one flight's crew members to contacts, updating them in place.

A caller-supplied ContactID is honoured only if the caller owns that contact; otherwise it is discarded and the name is resolved like any other. Contacts are per-user, and a crew row is the only place a client can propose a contact id, so an unverified one would let a flight reference another user's address book.

A member with a blank name is left unlinked: there is nobody to link. So is one whose name is too long to be a contact — the crew row is still written with the name as logged, and one unusable name does not cost the rest of the crew their links. Only infrastructure failures abort.

type CurrencyResult

type CurrencyResult struct {
	IsCurrent     bool
	DaysCurrent   bool
	NightsCurrent bool
	Flights90Days int
	TotalLandings int
	DayLandings   int
	NightLandings int
	RequiredDay   int
	RequiredNight int
}

CurrencyResult holds the calculated currency status

type DeletionPage added in v1.3.2

type DeletionPage struct {
	Deletions []*models.Deletion
	Total     int

	// WatermarkExpired is true when `since` predates the retention horizon.
	// Tombstones older than the horizon have been swept, so the feed is no
	// longer a complete account of what was deleted and the client must fall
	// back to a full ID-set reconciliation.
	WatermarkExpired bool
}

DeletionPage is one page of the feed plus the two things a client needs in order to trust it: how many deletions match, and whether its watermark is old enough that the answer may be incomplete.

type DeletionService added in v1.3.2

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

DeletionService serves the tombstone feed and sweeps it.

func NewDeletionService added in v1.3.2

func NewDeletionService(repo repository.DeletionRepository, retention time.Duration) *DeletionService

func (*DeletionService) ListDeletions added in v1.3.2

func (s *DeletionService) ListDeletions(
	ctx context.Context, userID uuid.UUID, since time.Time,
	entity *models.DeletionEntityType, page, pageSize int,
) (*DeletionPage, error)

ListDeletions returns the deletions recorded for the user after `since`.

The result is always user-scoped: a tombstone carries only an id and a type, but leaking another pilot's deletion timings would still be a disclosure, so the filter is applied in SQL rather than after the fact.

func (*DeletionService) Retention added in v1.3.2

func (s *DeletionService) Retention() time.Duration

Retention is how long a tombstone is kept, reported to clients so they can schedule their fallback reconciliation.

func (*DeletionService) StartReaper added in v1.3.2

func (s *DeletionService) StartReaper(ctx context.Context, interval time.Duration)

StartReaper sweeps tombstones past the retention horizon. Unlike the idempotency reaper this is not purely hygiene: retention is part of the contract, and a client is told its watermark expired based on the same horizon the sweeper enforces.

type DocumentFileService added in v1.3.2

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

DocumentFileService owns reference photos attached to licences and credentials: the feature switch, upload validation, ownership checks and the per-document cap.

Every method resolves the subject through the licence/credential repository first. That both proves ownership and means an image can never be addressed except through the document it belongs to.

func NewDocumentFileService added in v1.3.2

func NewDocumentFileService(
	imageRepo repository.DocumentFileRepository,
	licenseRepo repository.LicenseRepository,
	credentialRepo repository.CredentialRepository,
	enabled bool,
) *DocumentFileService

func (*DocumentFileService) Delete added in v1.3.2

func (s *DocumentFileService) Delete(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, subjectID, imageID uuid.UUID) error

Delete removes one image from a document.

func (*DocumentFileService) Enabled added in v1.3.2

func (s *DocumentFileService) Enabled() bool

Enabled reports whether the feature is switched on. Handlers use it to answer GET /features so a client can hide the UI instead of discovering the 403 by uploading.

func (*DocumentFileService) Get added in v1.3.2

func (s *DocumentFileService) Get(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, subjectID, imageID uuid.UUID) (*models.DocumentFile, error)

Get returns a single image including its bytes, for the authenticated download endpoint.

func (*DocumentFileService) List added in v1.3.2

func (s *DocumentFileService) List(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, subjectID uuid.UUID) ([]*models.DocumentFile, error)

List returns the metadata for a document's images, oldest first.

func (*DocumentFileService) Upload added in v1.3.2

func (s *DocumentFileService) Upload(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, subjectID uuid.UUID, in UploadInput) (*models.DocumentFile, error)

Upload validates and stores one image against a licence or credential.

type EmailDeliveryService added in v1.3.2

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

EmailDeliveryService records the outcome of every send and maintains the suppression list. It implements pkg/email.DeliveryRecorder, which is how the SMTP sender reaches the database without pkg/email depending on it.

func NewEmailDeliveryService added in v1.3.2

func NewEmailDeliveryService(repo repository.EmailDeliveryRepository) *EmailDeliveryService

func (*EmailDeliveryService) CountSuppressions added in v1.3.2

func (s *EmailDeliveryService) CountSuppressions(ctx context.Context) (int, error)

CountSuppressions reports how many addresses are currently suppressed.

func (*EmailDeliveryService) IsSuppressed added in v1.3.2

func (s *EmailDeliveryService) IsSuppressed(ctx context.Context, recipient string) bool

IsSuppressed reports whether the address has hard-bounced before.

It fails open: if the lookup errors we return false and let the send proceed. One wasted delivery attempt is a far smaller problem than silently dropping every email because the database hiccuped.

func (*EmailDeliveryService) ListEvents added in v1.3.2

func (s *EmailDeliveryService) ListEvents(ctx context.Context, recipient string, limit int) ([]*models.EmailDeliveryEvent, error)

ListEvents returns recent delivery events, newest first. An empty recipient means all recipients.

func (*EmailDeliveryService) ListSuppressions added in v1.3.2

func (s *EmailDeliveryService) ListSuppressions(ctx context.Context, limit int) ([]*models.EmailSuppression, error)

ListSuppressions returns suppressed addresses, most recently bounced first.

func (*EmailDeliveryService) RecordAttempt added in v1.3.2

func (s *EmailDeliveryService) RecordAttempt(ctx context.Context, a emailpkg.Attempt)

RecordAttempt appends the attempt to the delivery log and, for a permanent recipient-level refusal, adds the address to the suppression list.

It swallows its own storage errors by design: this runs on the send path, and a delivery that succeeded must not be reported as failed because an audit row could not be written.

func (*EmailDeliveryService) RefreshSuppressionGauge added in v1.3.2

func (s *EmailDeliveryService) RefreshSuppressionGauge(ctx context.Context)

RefreshSuppressionGauge re-reads the suppression count into the metric. Called at startup so the gauge is right after a restart, not only after the next bounce.

func (*EmailDeliveryService) Unsuppress added in v1.3.2

func (s *EmailDeliveryService) Unsuppress(ctx context.Context, email string) error

Unsuppress lifts a suppression so mail to the address is attempted again. Used when an operator knows the mailbox is back — a domain that was misconfigured, a mailbox that was over quota when it was last tried.

func (*EmailDeliveryService) UserIDForEmail added in v1.3.2

func (s *EmailDeliveryService) UserIDForEmail(ctx context.Context, email string) (uuid.UUID, error)

UserIDForEmail is exposed for callers that want attribution outside the send path.

type FlightService

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

func NewFlightService

func NewFlightService(flightRepo repository.FlightRepository, baselineRepo repository.FlightBaselineRepository) *FlightService

func (*FlightService) CountFlights

func (s *FlightService) CountFlights(ctx context.Context, userID uuid.UUID, opts *repository.FlightQueryOptions) (int, error)

CountFlights counts flights for a user with optional filters

func (*FlightService) CreateFlight

func (s *FlightService) CreateFlight(ctx context.Context, flight *models.Flight) error

CreateFlight creates a new flight log entry

func (*FlightService) DeleteAllFlights

func (s *FlightService) DeleteAllFlights(ctx context.Context, userID uuid.UUID) (int64, error)

DeleteAllFlights deletes all flights for a user

func (*FlightService) DeleteBaseline added in v1.2.3

func (s *FlightService) DeleteBaseline(ctx context.Context, userID uuid.UUID) error

DeleteBaseline removes the user's initial-hours snapshot.

func (*FlightService) DeleteFlight

func (s *FlightService) DeleteFlight(ctx context.Context, flightID, userID uuid.UUID) error

DeleteFlight deletes a flight and verifies user ownership

func (*FlightService) GetBaseline added in v1.2.3

func (s *FlightService) GetBaseline(ctx context.Context, userID uuid.UUID) (*models.FlightBaseline, error)

GetBaseline returns the user's initial-hours snapshot, or repository.ErrNotFound.

func (*FlightService) GetCurrency

func (s *FlightService) GetCurrency(ctx context.Context, userID uuid.UUID) (*CurrencyResult, error)

GetCurrency calculates currency status for a user based on EASA/FAA rules. FAA 14 CFR 61.57: 3 takeoffs and landings in the preceding 90 days EASA FCL.060: Recent experience — 3 takeoffs and landings in the preceding 90 days (simplified)

func (*FlightService) GetFlight

func (s *FlightService) GetFlight(ctx context.Context, flightID, userID uuid.UUID) (*models.Flight, error)

GetFlight retrieves a flight by ID and verifies user ownership

func (*FlightService) GetStatsByUserID

func (s *FlightService) GetStatsByUserID(ctx context.Context, userID uuid.UUID, startDate, endDate *time.Time, applyBaseline bool) (*models.FlightStatistics, *models.FlightBaseline, error)

GetStatsByUserID returns aggregated statistics for a user. The optional initial-hours snapshot (baseline) is added on top when:

  • the user has a baseline, AND
  • applyBaseline is true, AND
  • startDate is nil OR startDate <= baseline_date AND endDate is nil OR endDate >= baseline_date.

When the baseline is applied, it is also returned so callers can surface a breakdown to the client. baseline is nil otherwise.

func (*FlightService) ListFlights

func (s *FlightService) ListFlights(ctx context.Context, userID uuid.UUID, opts *repository.FlightQueryOptions) ([]*models.Flight, error)

ListFlights retrieves flights for a user with optional filters

func (*FlightService) UpdateFlight

func (s *FlightService) UpdateFlight(ctx context.Context, flight *models.Flight, userID uuid.UUID) error

UpdateFlight updates a flight and verifies user ownership

func (*FlightService) UpsertBaseline added in v1.2.3

func (s *FlightService) UpsertBaseline(ctx context.Context, baseline *models.FlightBaseline) error

UpsertBaseline validates and stores the user's initial-hours snapshot.

type FlightSessionEventInput added in v1.2.8

type FlightSessionEventInput struct {
	Type        string
	OccurredAt  *time.Time
	AircraftReg *string
	ICAO        *string
	Lat         *float64
	Lon         *float64

	// UserName is the authenticated user's display name, forwarded to
	// flight auto-calculations when the session is converted into a flight.
	UserName string
}

FlightSessionEventInput carries one tap-to-log event.

type FlightSessionService added in v1.2.8

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

FlightSessionService manages live tap-to-log flight sessions: opening a session on off-block, stamping takeoff/landing, and converting the session into a regular flight log entry when the pilot goes on blocks.

func NewFlightSessionService added in v1.2.8

func NewFlightSessionService(
	sessionRepo repository.FlightSessionRepository,
	aircraftRepo repository.AircraftRepository,
	flightService *FlightService,
) *FlightSessionService

func (*FlightSessionService) Discard added in v1.2.8

func (s *FlightSessionService) Discard(ctx context.Context, userID uuid.UUID) error

Discard marks the user's open session as discarded without creating a flight. Returns models.ErrNoOpenFlightSession when there is none.

func (*FlightSessionService) GetCurrent added in v1.2.8

func (s *FlightSessionService) GetCurrent(ctx context.Context, userID uuid.UUID) (*models.FlightSession, error)

GetCurrent returns the user's open session or models.ErrNoOpenFlightSession.

func (*FlightSessionService) RecordEvent added in v1.2.8

RecordEvent applies one tap-to-log event. It returns the resulting session and whether a new session was opened (offblock on a user with no open session). Repeating an event type already recorded on the session is a no-op returning the unchanged session, which makes offline retries and double-taps safe. An onblock event completes the session and creates the flight log entry.

type FlightSignatureService added in v1.2.9

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

FlightSignatureService implements the instructor sign-off workflow: live (in-person, synchronous), deferred-by-email, and deferred-by-shareable-link (all deferred requests share the same token mechanism; email is just an optional delivery channel for it). See models.FlightSignature for the lifecycle and internal/service/flight.go's ErrFlightLocked for the edit-lock this feature imposes on signed flights.

func NewFlightSignatureService added in v1.2.9

func (*FlightSignatureService) CompleteFromToken added in v1.2.9

func (s *FlightSignatureService) CompleteFromToken(ctx context.Context, token, signerName string, credentialNo *string, image []byte, signerIP, signerUA string) (sig *models.FlightSignature, ownerEmail, ownerName string, err error)

CompleteFromToken records the instructor's signature against a valid pending token. Returns the completed signature plus the flight owner's DB-sourced email/name so the caller can send a confirmation without ever trusting client-supplied contact details (CWE-640).

func (*FlightSignatureService) CreateRequest added in v1.2.9

func (s *FlightSignatureService) CreateRequest(ctx context.Context, flightID, userID uuid.UUID, instructorEmail *string, expiresInHours *int) (sig *models.FlightSignature, rawToken string, err error)

CreateRequest starts a deferred (token-based) signature request. If instructorEmail is supplied the caller should send the request email immediately (see APIHandler.sendSignatureRequestEmail); if omitted, the request is created "pending, unsent" so the owner can send it whenever they have the instructor's address, or share the link/QR directly.

func (*FlightSignatureService) ExpirePendingRequests added in v1.2.9

func (s *FlightSignatureService) ExpirePendingRequests(ctx context.Context) (int64, error)

ExpirePendingRequests soft-expires any pending request whose token has passed its expiry, returning the number of rows affected. Called from the admin cleanup-tokens maintenance sweep.

func (*FlightSignatureService) Get added in v1.2.9

func (s *FlightSignatureService) Get(ctx context.Context, flightID, userID, signatureID uuid.UUID) (*models.FlightSignature, error)

Get returns a single signature the caller owns.

func (*FlightSignatureService) List added in v1.2.9

func (s *FlightSignatureService) List(ctx context.Context, flightID, userID uuid.UUID) ([]*models.FlightSignature, error)

List returns the full signature history for a flight, newest first.

func (*FlightSignatureService) MarkEmailSent added in v1.2.9

func (s *FlightSignatureService) MarkEmailSent(ctx context.Context, sig *models.FlightSignature) error

MarkEmailSent records that a request email was (re)sent. Call after a successful (or best-effort) Sender.Send from the handler layer.

func (*FlightSignatureService) Resend added in v1.2.9

func (s *FlightSignatureService) Resend(ctx context.Context, flightID, userID, signatureID uuid.UUID, instructorEmail *string) (sig *models.FlightSignature, rawToken string, err error)

Resend rotates the token (and, if a new address is supplied, updates the delivery email) for a still-pending request. This single action covers both "send to a different email" and "regenerate the shareable link/QR" — the caller decides whether to also send an email with the result.

func (*FlightSignatureService) ResolveToken added in v1.2.9

ResolveToken validates a public signing token and returns the pending signature plus its flight, for display on the public /sign/{token} page. Never returns information that would let a caller distinguish "token never existed" from "token already used/revoked/voided".

func (*FlightSignatureService) Revoke added in v1.2.9

func (s *FlightSignatureService) Revoke(ctx context.Context, flightID, userID, signatureID uuid.UUID) error

Revoke cancels a still-pending request; nobody has signed it yet.

func (*FlightSignatureService) SignLive added in v1.2.9

func (s *FlightSignatureService) SignLive(ctx context.Context, flightID, userID uuid.UUID, signerName string, credentialNo *string, image []byte, signerIP, signerUA string) (*models.FlightSignature, error)

SignLive records an in-person signature captured on the flight owner's own device. Completes synchronously; no token is involved.

func (*FlightSignatureService) Void added in v1.2.9

func (s *FlightSignatureService) Void(ctx context.Context, flightID, userID, signatureID uuid.UUID, reason string) error

Void invalidates an already-completed signature (a reason is required for the audit trail) and unlocks the flight for editing again. Editing and re-signing after a void is the "revalidation" workflow.

type IdempotencyClaim added in v1.3.2

type IdempotencyClaim struct {
	Outcome IdempotencyOutcome

	// ClaimedAt identifies this claim for Finish/Abandon. Set when the
	// outcome is IdempotencyClaimed.
	ClaimedAt time.Time

	// Response is set when the outcome is IdempotencyReplay.
	Response *IdempotentResponse
}

IdempotencyClaim is the result of Begin.

type IdempotencyOutcome added in v1.3.2

type IdempotencyOutcome int

IdempotencyOutcome is the verdict on a claim attempt.

const (
	// IdempotencyClaimed means the key is ours: run the request, then call
	// Finish or Abandon.
	IdempotencyClaimed IdempotencyOutcome = iota

	// IdempotencyReplay means the request already ran to completion; return
	// the stored response instead of executing anything.
	IdempotencyReplay

	// IdempotencyInProgress means an earlier request with this key is still
	// running. Retrying after it settles yields the replay.
	IdempotencyInProgress

	// IdempotencyMismatch means the key was used before with a different
	// request. Almost always a client bug (a reused key, or a key derived
	// from too little of the payload).
	IdempotencyMismatch

	// IdempotencyNotReplayable means the original request completed but its
	// response was not stored, so it can be neither replayed nor safely
	// re-executed.
	IdempotencyNotReplayable
)

type IdempotencyService added in v1.3.2

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

IdempotencyService turns the raw replay records into the claim/finish lifecycle the HTTP layer needs, and owns their retention.

func NewIdempotencyService added in v1.3.2

func NewIdempotencyService(repo repository.IdempotencyRepository, ttl, lease time.Duration, maxResponseBytes int) *IdempotencyService

NewIdempotencyService creates the service. Non-positive values fall back to the defaults above.

func (*IdempotencyService) Abandon added in v1.3.2

func (s *IdempotencyService) Abandon(ctx context.Context, userID uuid.UUID, key string, claimedAt time.Time) error

Abandon drops the claim, leaving the key free for a genuine retry. Used when the request failed in a way that says nothing about whether it should be repeated — a server error, or a panic.

func (*IdempotencyService) Begin added in v1.3.2

func (s *IdempotencyService) Begin(ctx context.Context, userID uuid.UUID, key string, requestHash []byte) (IdempotencyClaim, error)

Begin claims the key for this request, or reports why the request must not run. requestHash fingerprints the request so a key reused for a different payload is refused instead of silently answered with the wrong response.

func (*IdempotencyService) Finish added in v1.3.2

func (s *IdempotencyService) Finish(ctx context.Context, userID uuid.UUID, key string, claimedAt time.Time, resp IdempotentResponse) error

Finish stores the response against a claim so retries replay it.

A response the caller could not capture — signalled with Status 0 — or one larger than the cap is recorded as completed with nothing to replay: the key stays consumed (re-executing would duplicate the write) but a retry is told the response is gone rather than handed a truncated one.

func (*IdempotencyService) MaxResponseBytes added in v1.3.2

func (s *IdempotencyService) MaxResponseBytes() int

MaxResponseBytes is the largest response body the service will store.

func (*IdempotencyService) StartReaper added in v1.3.2

func (s *IdempotencyService) StartReaper(ctx context.Context, interval time.Duration)

StartReaper deletes expired records on a timer until ctx is cancelled.

Hygiene only: Begin takes over any record past its expires_at, so a stopped reaper cannot make a stale key behave incorrectly — it only lets dead rows accumulate.

type IdempotentResponse added in v1.3.2

type IdempotentResponse struct {
	Status      int
	ContentType string
	Body        []byte
}

IdempotentResponse is a captured response, replayed verbatim on retry.

type LicenseService

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

func NewLicenseService

func NewLicenseService(licenseRepo repository.LicenseRepository) *LicenseService

func (*LicenseService) CreateLicense

func (s *LicenseService) CreateLicense(ctx context.Context, license *models.License) error

CreateLicense creates a new license for a user

func (*LicenseService) DeleteLicense

func (s *LicenseService) DeleteLicense(ctx context.Context, licenseID, userID uuid.UUID) error

DeleteLicense deletes a license and verifies user ownership

func (*LicenseService) GetLicense

func (s *LicenseService) GetLicense(ctx context.Context, licenseID, userID uuid.UUID) (*models.License, error)

GetLicense retrieves a license by ID and verifies user ownership

func (*LicenseService) ListLicenses

func (s *LicenseService) ListLicenses(ctx context.Context, userID uuid.UUID) ([]*models.License, error)

ListLicenses retrieves all licenses for a user

func (*LicenseService) ListLicensesUpdatedSince added in v1.3.2

func (s *LicenseService) ListLicensesUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.License, error)

ListLicensesUpdatedSince retrieves the user's licenses that changed strictly after the given instant, for delta-syncing clients.

func (*LicenseService) UpdateLicense

func (s *LicenseService) UpdateLicense(ctx context.Context, license *models.License, userID uuid.UUID) error

UpdateLicense updates a license and verifies user ownership

type LoginInput

type LoginInput struct {
	Email    string
	Password string
}

type NotificationService

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

func NewNotificationService

func NewNotificationService(
	notifRepo repository.NotificationRepository,
	credentialRepo repository.CredentialRepository,
	flightRepo repository.FlightRepository,
	licenseRepo repository.LicenseRepository,
	userRepo repository.UserRepository,
	emailSender *email.Sender,
	currencyService *currency.Service,
	customService customCurrencyLister,
) *NotificationService

func (*NotificationService) GetNotificationHistory

func (s *NotificationService) GetNotificationHistory(ctx context.Context, userID uuid.UUID, limit, offset int) ([]*models.NotificationLog, int, error)

GetNotificationHistory returns paginated notification history for a user

func (*NotificationService) GetPreferences

func (s *NotificationService) GetPreferences(ctx context.Context, userID uuid.UUID) (*models.NotificationPreferences, error)

GetPreferences returns notification preferences for a user

func (*NotificationService) StartBackgroundChecker

func (s *NotificationService) StartBackgroundChecker(ctx context.Context, interval time.Duration)

StartBackgroundChecker starts a goroutine that checks for notifications periodically

func (*NotificationService) TriggerCheck

func (s *NotificationService) TriggerCheck(ctx context.Context)

TriggerCheck runs the notification check immediately for all users, bypassing the check-hour gating. Used by admin maintenance endpoint and E2E tests.

func (*NotificationService) UpdatePreferences

func (s *NotificationService) UpdatePreferences(ctx context.Context, prefs *models.NotificationPreferences) error

UpdatePreferences updates notification preferences for a user

type OIDCAuthorization added in v1.3.2

type OIDCAuthorization struct {
	AuthorizationURL string
	BrowserToken     string
	// Expiry is when the pending login stops being usable, used as the
	// cookie's Max-Age so it disappears with the state it guards.
	Expiry time.Time
}

OIDCAuthorization is what the authorize step hands back: the provider URL to send the browser to, and the value that must be planted in the browser as a cookie so the callback can prove it is the same browser.

type OIDCConfig added in v1.3.2

type OIDCConfig struct {
	// Issuer is the provider's issuer URL. Its presence enables OIDC mode.
	Issuer string
	// ClientID and ClientSecret identify NinerLog to the provider.
	ClientID     string
	ClientSecret string
	// RedirectURL is this API's own callback, exactly as registered with the
	// provider (…/api/v1/auth/oidc/callback).
	RedirectURL string
	// PostLoginRedirect is the frontend URL the browser is sent to after a
	// successful login. Fixed by configuration and never read from the
	// request, so the callback cannot be turned into an open redirect.
	PostLoginRedirect string
	// Scopes requested at the provider. "openid" is always included.
	Scopes []string
	// ProviderName is the label the sign-in button shows ("Sign in with …").
	ProviderName string
	// NameClaim is the claim used as the pilot's display name.
	NameClaim string
	// LinkByVerifiedEmail lets a first OIDC login adopt a pre-existing local
	// account with the same address, but only when the ID token asserts
	// email_verified. Off by default: with an IdP where users choose their own
	// address this is an account-takeover path.
	LinkByVerifiedEmail bool
	// TrustEmailVerified marks provisioned users as email-verified even when
	// the ID token carries no email_verified claim. Needed for providers that
	// omit it — and required for ADMIN_EMAIL to take effect, since admin
	// status demands a verified address.
	TrustEmailVerified bool
	// LoginStateTTL and HandoffTTL bound the two single-use artefacts of a
	// login round trip.
	LoginStateTTL time.Duration
	HandoffTTL    time.Duration
}

OIDCConfig is the operator-supplied configuration for OIDC mode. It is built exclusively from environment variables; there is no per-user or database configuration for the provider.

func LoadOIDCConfig added in v1.3.2

func LoadOIDCConfig() (OIDCConfig, error)

LoadOIDCConfig reads OIDC configuration from the environment. It returns a disabled config (and no error) when OIDC_ISSUER is unset. When the issuer is set, every other mandatory value must be present and well-formed: a half-configured identity provider must stop startup rather than leave a deployment that can neither log in locally nor via OIDC.

func (OIDCConfig) CookiePath added in v1.3.2

func (c OIDCConfig) CookiePath() string

CookiePath scopes the login-state cookie to the OIDC endpoints.

It is derived from the configured callback rather than hard-coded, because the browser matches the cookie against the path *it* requested. A deployment that mounts the API under a prefix (a reverse proxy serving NinerLog at /backend, say) would otherwise store the cookie at a path the callback never matches, and every login would fail with an unexplained invalid state.

func (OIDCConfig) CookieSecure added in v1.3.2

func (c OIDCConfig) CookieSecure() bool

CookieSecure reports whether the login-state cookie must carry the Secure attribute. Derived from the configured callback URL, not from the inbound request: behind a TLS-terminating proxy the request reaching Go is plain http, but the browser is on https and would silently drop a non-Secure cookie set from a Secure context in modern browsers. Local http development must conversely not set Secure, or the cookie is dropped there instead.

func (OIDCConfig) Enabled added in v1.3.2

func (c OIDCConfig) Enabled() bool

Enabled reports whether OIDC mode is switched on.

type OIDCService added in v1.3.2

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

OIDCService implements the authorization-code + PKCE login flow against a single configured provider.

It is constructed only when OIDC_ISSUER is set; a nil *OIDCService means the deployment runs in local-credential mode, and handlers check for that before touching any method here.

func NewOIDCService added in v1.3.2

func NewOIDCService(
	cfg OIDCConfig,
	userRepo repository.UserRepository,
	identityRepo repository.OIDCIdentityRepository,
	authService *AuthService,
) (*OIDCService, error)

NewOIDCService returns a service for the supplied configuration, or nil when OIDC is not enabled. It does not contact the provider.

func (*OIDCService) BeginLogin added in v1.3.2

func (s *OIDCService) BeginLogin(ctx context.Context) (*OIDCAuthorization, error)

BeginLogin creates a pending authorization request and returns the provider URL to redirect the browser to.

Three independent single-use values are minted: `state` (returned by the provider, proves the callback belongs to a login we started), `nonce` (embedded in the ID token, proves the token was minted for this login and not replayed), and the PKCE verifier (proves the code is redeemed by the party that requested it). Only hashes of the state and the browser token are persisted.

func (*OIDCService) CompleteCallback added in v1.3.2

func (s *OIDCService) CompleteCallback(ctx context.Context, code, state, browserToken string) (string, error)

CompleteCallback validates the provider's response, provisions or updates the local user, and returns a single-use handoff code for the browser.

It deliberately returns only that code: the access and refresh tokens are minted later, by ExchangeHandoff, so they never travel in a URL.

func (*OIDCService) Config added in v1.3.2

func (s *OIDCService) Config() OIDCConfig

Config returns the effective configuration (no secrets are exposed by the callers that use it).

func (*OIDCService) ExchangeHandoff added in v1.3.2

func (s *OIDCService) ExchangeHandoff(ctx context.Context, handoff string) (*models.User, *TokenPair, error)

ExchangeHandoff redeems a handoff code for the user and a fresh token pair.

func (*OIDCService) PostLoginRedirect added in v1.3.2

func (s *OIDCService) PostLoginRedirect() string

PostLoginRedirect is the fixed frontend URL the callback redirects to.

func (*OIDCService) StartStateReaper added in v1.3.2

func (s *OIDCService) StartStateReaper(ctx context.Context, interval time.Duration)

StartStateReaper deletes expired login states and handoff codes on a timer. Hygiene only: consumption already refuses expired rows, so a stalled reaper cannot make a stale artefact usable.

type PasswordResetRequest added in v1.3.2

type PasswordResetRequest struct {
	// Token is the plaintext reset token to embed in the mail. Empty when no
	// account matched — callers must stay silent in that case so the response
	// does not reveal whether the address exists.
	Token string
	Email string
	Name  string
	// Locale is the account's preferred locale, for template selection.
	Locale string
	// TwoFactorEnabled tells the mail to warn the user up front that they will
	// also need their authenticator (or a recovery code) to finish the reset.
	TwoFactorEnabled bool
}

PasswordResetRequest describes the recipient of a password-reset mail. All fields are loaded from the database, never from the HTTP request.

type PasswordResetResult added in v1.3.2

type PasswordResetResult struct {
	UserID uuid.UUID
	Email  string
	Name   string
	Locale string
	// TwoFactorEnabled reports whether the account still has 2FA active. A
	// reset never turns it off, so this is simply the account's current state.
	TwoFactorEnabled bool
}

PasswordResetResult describes a completed password reset, so the handler can send the confirmation mail without loading the user again.

type RegisterInput

type RegisterInput struct {
	Email           string
	Password        string
	Name            string
	PreferredLocale string
}

type TokenPair

type TokenPair struct {
	AccessToken  string
	RefreshToken string
}

type TwoFactorService

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

func NewTwoFactorService

func NewTwoFactorService(userRepo repository.UserRepository, jwtManager *jwt.Manager, aead *cryptoutil.AEAD) *TwoFactorService

NewTwoFactorService constructs the service. aead may be nil, in which case TOTP secrets are stored unencrypted (backward-compatible). Provide an AEAD (see cryptoutil) to encrypt secrets at rest.

func (*TwoFactorService) Disable

func (s *TwoFactorService) Disable(ctx context.Context, userID uuid.UUID, password string) error

Disable disables 2FA for a user after password verification

func (*TwoFactorService) IsEnabled

func (s *TwoFactorService) IsEnabled(ctx context.Context, userID uuid.UUID) (bool, error)

IsEnabled checks if 2FA is enabled for a user

func (*TwoFactorService) SetupTOTP

func (s *TwoFactorService) SetupTOTP(ctx context.Context, userID uuid.UUID) (string, string, error)

SetupTOTP generates a new TOTP secret for a user (does not enable 2FA yet)

func (*TwoFactorService) ValidateTOTP

func (s *TwoFactorService) ValidateTOTP(ctx context.Context, userID uuid.UUID, code string) (bool, error)

ValidateTOTP validates a TOTP code or recovery code for a user.

The 2FA step is brute-force protected with the same per-account lockout used by password login: a locked account is rejected with ErrAccountLocked, each failed code counts toward the lockout, and a successful validation resets the counter. This prevents an attacker who has the password from grinding TOTP codes via repeated /auth/2fa/login calls.

func (*TwoFactorService) VerifyAndEnable

func (s *TwoFactorService) VerifyAndEnable(ctx context.Context, userID uuid.UUID, code string) ([]string, error)

VerifyAndEnable verifies a TOTP code and enables 2FA, returning recovery codes

type TwoFactorValidator added in v1.3.2

type TwoFactorValidator interface {
	// ValidateTOTP reports whether code is a valid TOTP code or an unused
	// recovery code for the user. Recovery codes are consumed on success, and
	// failures count toward the account lockout.
	ValidateTOTP(ctx context.Context, userID uuid.UUID, code string) (bool, error)
}

TwoFactorValidator is the part of TwoFactorService that the password-reset flow depends on. It is an interface so AuthService does not have to own the TOTP/recovery-code implementation, and so tests can substitute a stub.

type UnverifiedAccountConfig added in v1.3.2

type UnverifiedAccountConfig struct {
	// ReminderAfter is how long after signup the follow-up email goes out.
	ReminderAfter time.Duration
	// Retention is how long after that reminder the account survives unverified.
	Retention time.Duration
	// SweepInterval is how often the worker looks for work.
	SweepInterval time.Duration
	// BatchSize bounds reminders per sweep.
	BatchSize int
}

UnverifiedAccountConfig is the lifecycle timing. Zero values fall back to the defaults above.

func LoadUnverifiedAccountConfig added in v1.3.2

func LoadUnverifiedAccountConfig() UnverifiedAccountConfig

LoadUnverifiedAccountConfig reads the lifecycle timing from the environment. Every value is optional; an unparseable or non-positive setting falls back to the default rather than disabling a step, because a typo must not silently switch account deletion on a different schedule than intended.

type UnverifiedAccountService added in v1.3.2

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

UnverifiedAccountService reminds, then reaps, accounts that never verified their email address.

It only makes sense when email verification is actually enforced. With SMTP unconfigured, registration marks accounts verified on the spot, so there is nothing here to act on — and acting anyway would delete accounts that were never asked to verify. cmd/api/main.go therefore starts the worker only when the sender is configured, and the service re-checks on every sweep.

func NewUnverifiedAccountService added in v1.3.2

func NewUnverifiedAccountService(
	users repository.UserRepository,
	tokens verificationTokenIssuer,
	sender reminderSender,
	baseURLFn func() string,
	cfg UnverifiedAccountConfig,
) *UnverifiedAccountService

func (*UnverifiedAccountService) Config added in v1.3.2

Config reports the effective lifecycle timing, so it can be surfaced to administrators and stated in the reminder email.

func (*UnverifiedAccountService) Start added in v1.3.2

Start runs the sweep loop until ctx is cancelled.

func (*UnverifiedAccountService) Sweep added in v1.3.2

func (s *UnverifiedAccountService) Sweep(ctx context.Context) (reminded int, deleted int64)

Sweep sends any due reminders and deletes any accounts past the retention horizon. Exported so an administrator can trigger it and tests can run one pass deterministically.

type UploadInput added in v1.3.2

type UploadInput struct {
	Data     []byte
	Filename string
	Caption  string
}

UploadInput is one candidate image as it arrives from the handler. Data is the raw file; ContentType is *derived* from it, never taken from the client's declared part header.

type WebAuthnService added in v1.2.0

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

WebAuthnService implements passkey registration & login flows.

func NewWebAuthnService added in v1.2.0

func NewWebAuthnService(
	rpID, rpName string,
	rpOrigins []string,
	credRepo repository.WebAuthnCredentialRepository,
	sessionRepo repository.WebAuthnSessionRepository,
	userRepo repository.UserRepository,
	authService *AuthService,
	sessionTTL time.Duration,
	maxOpenCeremonies int,
) (*WebAuthnService, error)

NewWebAuthnService creates a new WebAuthnService. Returns nil and an error if the webauthn library cannot be initialized with the given config (e.g. invalid origins).

sessionTTL bounds how long ceremony state stays usable; non-positive values fall back to DefaultWebAuthnSessionTTL. maxOpenCeremonies caps how many ceremonies one user may hold open at once; non-positive values fall back to DefaultWebAuthnMaxOpenCeremonies.

func (*WebAuthnService) BeginLogin added in v1.2.0

func (s *WebAuthnService) BeginLogin(ctx context.Context, email string) (handle string, options *protocol.CredentialAssertion, err error)

BeginLogin starts a passkey login ceremony. If email is non-empty, the user's existing credentials are advertised; otherwise a discoverable-credential challenge is issued.

func (*WebAuthnService) BeginRegistration added in v1.2.0

func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID uuid.UUID) (handle string, options *protocol.CredentialCreation, err error)

BeginRegistration starts a passkey registration ceremony for an authenticated user. The returned handle must be presented to FinishRegistration.

func (*WebAuthnService) DeleteCredential added in v1.2.0

func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID, credentialID uuid.UUID) error

DeleteCredential revokes a passkey owned by the given user.

func (*WebAuthnService) FinishLogin added in v1.2.0

func (s *WebAuthnService) FinishLogin(ctx context.Context, handle string, responseJSON []byte) (*models.User, *TokenPair, error)

FinishLogin verifies an assertion and returns the authenticated user with a new TokenPair.

func (*WebAuthnService) FinishRegistration added in v1.2.0

func (s *WebAuthnService) FinishRegistration(ctx context.Context, userID uuid.UUID, handle string, label *string, responseJSON []byte) (*models.WebAuthnCredential, error)

FinishRegistration verifies an attestation and stores the new credential.

func (*WebAuthnService) ListCredentials added in v1.2.0

func (s *WebAuthnService) ListCredentials(ctx context.Context, userID uuid.UUID) ([]*models.WebAuthnCredential, error)

ListCredentials returns the registered passkeys for a user.

func (*WebAuthnService) StartSessionReaper added in v1.3.2

func (s *WebAuthnService) StartSessionReaper(ctx context.Context, interval time.Duration)

StartSessionReaper deletes expired ceremony rows on a timer until ctx is cancelled.

This is hygiene, not a control. Correctness is enforced by the `expires_at > NOW()` predicate in Consume, so a stopped or lagging reaper can never make a stale challenge usable — it only lets dead rows accumulate.

Directories

Path Synopsis
Package cloudbackup is the user-facing service for cloud backup destinations.
Package cloudbackup is the user-facing service for cloud backup destinations.
netguard
Package netguard restricts the network destinations the cloud-backup subsystem is allowed to connect to, mitigating server-side request forgery (SSRF).
Package netguard restricts the network destinations the cloud-backup subsystem is allowed to connect to, mitigating server-side request forgery (SSRF).
provider
Package provider defines the contract every cloud backup provider plugin must satisfy.
Package provider defines the contract every cloud backup provider plugin must satisfy.
provider/s3
Package s3 implements the cloud backup provider contract against Amazon S3 and S3-compatible object stores (MinIO, Backblaze B2, Cloudflare R2, Wasabi, etc.) using the minio-go client.
Package s3 implements the cloud backup provider contract against Amazon S3 and S3-compatible object stores (MinIO, Backblaze B2, Cloudflare R2, Wasabi, etc.) using the minio-go client.
provider/sftp
Package sftp implements the cloud backup provider contract against any SSH server with SFTP enabled (OpenSSH, Dropbear, atmoz/sftp, hosting providers offering SFTP-only accounts, etc.) using golang.org/x/crypto/ssh and github.com/pkg/sftp.
Package sftp implements the cloud backup provider contract against any SSH server with SFTP enabled (OpenSSH, Dropbear, atmoz/sftp, hosting providers offering SFTP-only accounts, etc.) using golang.org/x/crypto/ssh and github.com/pkg/sftp.
provider/webdav
Package webdav implements the cloud backup provider contract against any RFC 4918 compliant WebDAV server (Nextcloud, ownCloud, mailbox.org, Box, Apache mod_dav, …) using the studio-b12/gowebdav client.
Package webdav implements the cloud backup provider contract against any RFC 4918 compliant WebDAV server (Nextcloud, ownCloud, mailbox.org, Box, Apache mod_dav, …) using the studio-b12/gowebdav client.
Package flightrules is the single source of truth for "who/what counts as PIC, Dual, FI, Night, IFR, MP/SP …" across the codebase.
Package flightrules is the single source of truth for "who/what counts as PIC, Dual, FI, Night, IFR, MP/SP …" across the codebase.

Jump to

Keyboard shortcuts

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