service

package
v1.3.4 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const (
	// DefaultTombstoneRetention bounds how long a deletion stays reportable.
	DefaultTombstoneRetention = 90 * 24 * time.Hour

	// DefaultDeletionPageSize / MaxDeletionPageSize bound the feed.
	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.
	DefaultIdempotencyTTL = 24 * time.Hour

	// DefaultIdempotencyLease is how long an in-progress claim is honoured
	// before another request may take it over. Must exceed the request
	// timeout (15s).
	DefaultIdempotencyLease = 60 * time.Second

	// DefaultIdempotencyMaxResponseBytes caps what is stored per record.
	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.
	DefaultOIDCLoginStateTTL = 10 * time.Minute

	// DefaultOIDCHandoffTTL bounds the window between the provider
	// redirecting the browser back and the SPA exchanging its code for
	// tokens.
	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.

View Source
const (
	// PasswordMinLength is the shortest accepted password.
	PasswordMinLength = 12
	// PasswordMaxLength is the longest accepted password (bcrypt's 72-byte
	// input limit).
	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: one reminder a day after signup, and that reminder starts a 30-day deletion clock.

View Source
const (
	// CleanupDisabledByOIDC: OIDC mode refuses the feature outright; it is
	// not configurable.
	CleanupDisabledByOIDC = "oidc_mode"
	// CleanupDisabledNoSMTP: without SMTP, registration marks accounts
	// verified on the spot.
	CleanupDisabledNoSMTP = "smtp_not_configured"
	// CleanupDisabledByConfig: UNVERIFIED_CLEANUP_ENABLED=false.
	CleanupDisabledByConfig = "disabled_by_configuration"
)

Reasons the reaper is not running, reported to administrators.

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.
	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).
	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.
	ErrTwoFactorRequired = errors.New("two-factor authentication code required")
	// ErrTwoFactorUnavailable is returned when a 2FA-protected reset is attempted
	// but no validator is wired.
	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.
	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 — reads
	// included — when the feature is switched off (DOCUMENT_FILES_ENABLED=
	// false). 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.
	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.
	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, indistinguishably.
	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.
	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.
	ErrOIDCProviderUnavailable = errors.New("oidc provider is unavailable")

	// ErrOIDCInvalidState covers every unusable authorization response,
	// indistinguishably: an unknown, expired or already-consumed state, and a
	// state presented by a different browser than the one that started the
	// login.
	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;
	// 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.
	//   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.
	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, indistinguishably, for every
	// unusable ceremony handle: expired, already consumed, wrong ceremony,
	// scoped to another user, or never issued.
	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.
	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.
	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.

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

ErrInvalidDeletionEntity is returned for an entity filter outside the enum.

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.

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.

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.

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.

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; the confirmation itself happens in the handler.

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. Called by every path that hands a session to a user: password login, the 2FA second factor, passkeys, OIDC, and the sign-up verification link. A refresh is not a login. Failure is logged and swallowed.

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 with EmailVerified=false, stores an email-verification token, and returns the plaintext token for the handler to deliver via email. No JWT tokens are issued; the user must consume the verification token (see VerifyEmail) before logging 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, never from the HTTP request.

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; "unknown email" and "already verified" are indistinguishable to callers.

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: 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. The reset token is consumed only on success, so a wrong or missing code can be retried with the same link.

func (*AuthService) RevokeAllSessions added in v1.3.4

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

RevokeAllSessions deletes every refresh token a user holds, forcing a fresh login on all devices. Used when an admin disables an account, so a disabled user cannot keep an existing session alive.

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.

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.

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 touch any flight: flight_crew_members.contact_id is ON DELETE SET NULL and the crew row keeps the name it was logged with. Deleting is always allowed, even for a contact referenced by signed flights.

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.

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.

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. Entries on flights carrying a completed instructor signature, and entries not linked to the contact, are excluded; the repository enforces that boundary in SQL.

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.

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. A member with a blank or over-length name is left unlinked; the crew row is still written with the name as logged. 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;
	// the feed may be incomplete and the client must fall back to a full
	// ID-set reconciliation.
	WatermarkExpired bool
}

DeletionPage is one page of the feed plus how many deletions match and whether the client's watermark predates the retention horizon.

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; the filter is applied in SQL.

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.

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 — the same horizon WatermarkExpired is computed from.

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; an image is only addressable 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.

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.

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: a lookup error returns false and the send proceeds.

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.

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.

func (*EmailDeliveryService) Unsuppress added in v1.3.2

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

Unsuppress lifts a suppression; mail to the address is attempted again.

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. 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 for the confirmation mail.

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.
	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 on 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; a key reused for a different payload is refused.

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 and a retry is told the response is gone.

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.

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.
	Expiry time.Time
}

OIDCAuthorization is what the authorize step hands back: the provider URL to send the browser to, and the value to plant in the browser as a cookie for the callback's same-browser check.

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, never read from the request.
	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.
	LinkByVerifiedEmail bool
	// TrustEmailVerified marks provisioned users as email-verified even when
	// the ID token carries no email_verified claim. Required for ADMIN_EMAIL
	// to take effect with providers that omit the claim.
	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 or startup stops.

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, including any reverse-proxy prefix.

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's scheme.

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`, `nonce`, and the PKCE verifier. 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 only a single-use handoff code for the browser; the access and refresh tokens are minted later, by ExchangeHandoff.

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.

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.
	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.
	TwoFactorEnabled bool
}

PasswordResetResult describes a completed password reset.

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. It shares the per-account lockout with password login: a locked account is rejected with ErrAccountLocked, each failed code counts toward the lockout, and a successful validation resets the counter.

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.

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.

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. cmd/api/main.go starts the worker only when the email 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.

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 for the admin trigger and tests.

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; the content type 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. Hygiene only: expiry is enforced by the `expires_at > NOW()` predicate in Consume.

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 (SSRF mitigation).
Package netguard restricts the network destinations the cloud-backup subsystem is allowed to connect to (SSRF mitigation).
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