Documentation
¶
Index ¶
- Constants
- Variables
- func GetCheckInterval() time.Duration
- func IdempotencyStatus(o IdempotencyOutcome) int
- func UnverifiedCleanupDisabledReason(smtpConfigured, oidcEnabled bool) string
- type AircraftService
- func (s *AircraftService) CountAircraft(ctx context.Context, userID uuid.UUID) (int, error)
- func (s *AircraftService) CreateAircraft(ctx context.Context, aircraft *models.Aircraft) error
- func (s *AircraftService) DeleteAircraft(ctx context.Context, id, userID uuid.UUID) error
- func (s *AircraftService) GetAircraft(ctx context.Context, id, userID uuid.UUID) (*models.Aircraft, error)
- func (s *AircraftService) GetAircraftStats(ctx context.Context, userID uuid.UUID) (*AircraftStatsResult, error)
- func (s *AircraftService) ListAircraft(ctx context.Context, userID uuid.UUID) ([]*models.Aircraft, error)
- func (s *AircraftService) ListAircraftUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.Aircraft, error)
- func (s *AircraftService) UpdateAircraft(ctx context.Context, aircraft *models.Aircraft, userID uuid.UUID, ...) (int, error)
- type AircraftStatsResult
- type AuthService
- func (s *AuthService) ChangePassword(ctx context.Context, userID uuid.UUID, currentPassword, newPassword string) error
- func (s *AuthService) CreateEmailVerificationToken(ctx context.Context, userID uuid.UUID) (string, error)
- func (s *AuthService) DeleteUser(ctx context.Context, userID uuid.UUID, password string) error
- func (s *AuthService) DeleteUserConfirmed(ctx context.Context, userID uuid.UUID) error
- func (s *AuthService) GenerateTokensForUser(ctx context.Context, userID uuid.UUID) (*TokenPair, error)
- func (s *AuthService) GetUserByID(ctx context.Context, userID uuid.UUID) (*models.User, error)
- func (s *AuthService) Login(ctx context.Context, input LoginInput) (*models.User, *TokenPair, error)
- func (s *AuthService) Logout(ctx context.Context, refreshToken string) error
- func (s *AuthService) MarkEmailVerified(ctx context.Context, userID uuid.UUID) error
- func (s *AuthService) RecordLogin(ctx context.Context, user *models.User)
- func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*TokenPair, error)
- func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*models.User, string, error)
- func (s *AuthService) RequestPasswordReset(ctx context.Context, email string) (*PasswordResetRequest, error)
- func (s *AuthService) ResendVerification(ctx context.Context, email string) (token, userEmail, userName, locale string, err error)
- func (s *AuthService) ResetPassword(ctx context.Context, token, newPassword, twoFactorCode string) (*PasswordResetResult, error)
- func (s *AuthService) UpdateUser(ctx context.Context, user *models.User) error
- func (s *AuthService) VerifyEmail(ctx context.Context, token string) (*models.User, *TokenPair, error)
- func (s *AuthService) VerifyPassword(ctx context.Context, userID uuid.UUID, password string) error
- type ClassRatingRepository
- type ClassRatingService
- func (s *ClassRatingService) CreateClassRating(ctx context.Context, cr *models.ClassRating, userID uuid.UUID) error
- func (s *ClassRatingService) DeleteClassRating(ctx context.Context, ratingID, userID uuid.UUID) error
- func (s *ClassRatingService) GetClassRating(ctx context.Context, ratingID, userID uuid.UUID) (*models.ClassRating, error)
- func (s *ClassRatingService) ListClassRatings(ctx context.Context, licenseID, userID uuid.UUID) ([]*models.ClassRating, error)
- func (s *ClassRatingService) UpdateClassRating(ctx context.Context, cr *models.ClassRating, userID uuid.UUID) error
- type ContactService
- func (s *ContactService) CreateContact(ctx context.Context, contact *models.Contact) error
- func (s *ContactService) DeleteContact(ctx context.Context, id, userID uuid.UUID) error
- func (s *ContactService) ExportVCard(ctx context.Context, userID uuid.UUID) ([]byte, error)
- func (s *ContactService) FindOrCreateContact(ctx context.Context, userID uuid.UUID, name string) (*models.Contact, bool, error)
- func (s *ContactService) GetContact(ctx context.Context, id, userID uuid.UUID) (*models.Contact, error)
- func (s *ContactService) LinkCrewMembers(ctx context.Context, userID uuid.UUID, members []models.FlightCrewMember) (int, error)
- func (s *ContactService) ListContacts(ctx context.Context, userID uuid.UUID) ([]*models.Contact, error)
- func (s *ContactService) ListContactsUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.Contact, error)
- func (s *ContactService) NewCrewLinker(userID uuid.UUID) *CrewLinker
- func (s *ContactService) SearchContacts(ctx context.Context, userID uuid.UUID, query string, limit int) ([]*models.Contact, error)
- func (s *ContactService) UpdateContact(ctx context.Context, contact *models.Contact, userID uuid.UUID) (int, error)
- type CredentialService
- func (s *CredentialService) CreateCredential(ctx context.Context, credential *models.Credential) error
- func (s *CredentialService) DeleteCredential(ctx context.Context, id, userID uuid.UUID) error
- func (s *CredentialService) GetCredential(ctx context.Context, id, userID uuid.UUID) (*models.Credential, error)
- func (s *CredentialService) ListCredentials(ctx context.Context, userID uuid.UUID) ([]*models.Credential, error)
- func (s *CredentialService) ListCredentialsUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.Credential, error)
- func (s *CredentialService) UpdateCredential(ctx context.Context, credential *models.Credential, userID uuid.UUID) error
- type CrewLinker
- type CurrencyResult
- type DeletionPage
- type DeletionService
- type DocumentFileService
- func (s *DocumentFileService) Delete(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, ...) error
- func (s *DocumentFileService) Enabled() bool
- func (s *DocumentFileService) Get(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, ...) (*models.DocumentFile, error)
- func (s *DocumentFileService) List(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, ...) ([]*models.DocumentFile, error)
- func (s *DocumentFileService) Upload(ctx context.Context, userID uuid.UUID, subject models.DocumentSubjectType, ...) (*models.DocumentFile, error)
- type EmailDeliveryService
- func (s *EmailDeliveryService) CountSuppressions(ctx context.Context) (int, error)
- func (s *EmailDeliveryService) IsSuppressed(ctx context.Context, recipient string) bool
- func (s *EmailDeliveryService) ListEvents(ctx context.Context, recipient string, limit int) ([]*models.EmailDeliveryEvent, error)
- func (s *EmailDeliveryService) ListSuppressions(ctx context.Context, limit int) ([]*models.EmailSuppression, error)
- func (s *EmailDeliveryService) RecordAttempt(ctx context.Context, a emailpkg.Attempt)
- func (s *EmailDeliveryService) RefreshSuppressionGauge(ctx context.Context)
- func (s *EmailDeliveryService) Unsuppress(ctx context.Context, email string) error
- func (s *EmailDeliveryService) UserIDForEmail(ctx context.Context, email string) (uuid.UUID, error)
- type FlightService
- func (s *FlightService) CountFlights(ctx context.Context, userID uuid.UUID, opts *repository.FlightQueryOptions) (int, error)
- func (s *FlightService) CreateFlight(ctx context.Context, flight *models.Flight) error
- func (s *FlightService) DeleteAllFlights(ctx context.Context, userID uuid.UUID) (int64, error)
- func (s *FlightService) DeleteBaseline(ctx context.Context, userID uuid.UUID) error
- func (s *FlightService) DeleteFlight(ctx context.Context, flightID, userID uuid.UUID) error
- func (s *FlightService) GetBaseline(ctx context.Context, userID uuid.UUID) (*models.FlightBaseline, error)
- func (s *FlightService) GetCurrency(ctx context.Context, userID uuid.UUID) (*CurrencyResult, error)
- func (s *FlightService) GetFlight(ctx context.Context, flightID, userID uuid.UUID) (*models.Flight, error)
- func (s *FlightService) GetStatsByUserID(ctx context.Context, userID uuid.UUID, startDate, endDate *time.Time, ...) (*models.FlightStatistics, *models.FlightBaseline, error)
- func (s *FlightService) ListFlights(ctx context.Context, userID uuid.UUID, opts *repository.FlightQueryOptions) ([]*models.Flight, error)
- func (s *FlightService) UpdateFlight(ctx context.Context, flight *models.Flight, userID uuid.UUID) error
- func (s *FlightService) UpsertBaseline(ctx context.Context, baseline *models.FlightBaseline) error
- type FlightSessionEventInput
- type FlightSessionService
- func (s *FlightSessionService) Discard(ctx context.Context, userID uuid.UUID) error
- func (s *FlightSessionService) GetCurrent(ctx context.Context, userID uuid.UUID) (*models.FlightSession, error)
- func (s *FlightSessionService) RecordEvent(ctx context.Context, userID uuid.UUID, input FlightSessionEventInput) (*models.FlightSession, bool, error)
- type FlightSignatureService
- func (s *FlightSignatureService) CompleteFromToken(ctx context.Context, token, signerName string, credentialNo *string, ...) (sig *models.FlightSignature, ownerEmail, ownerName string, err error)
- func (s *FlightSignatureService) CreateRequest(ctx context.Context, flightID, userID uuid.UUID, instructorEmail *string, ...) (sig *models.FlightSignature, rawToken string, err error)
- func (s *FlightSignatureService) ExpirePendingRequests(ctx context.Context) (int64, error)
- func (s *FlightSignatureService) Get(ctx context.Context, flightID, userID, signatureID uuid.UUID) (*models.FlightSignature, error)
- func (s *FlightSignatureService) List(ctx context.Context, flightID, userID uuid.UUID) ([]*models.FlightSignature, error)
- func (s *FlightSignatureService) MarkEmailSent(ctx context.Context, sig *models.FlightSignature) error
- func (s *FlightSignatureService) Resend(ctx context.Context, flightID, userID, signatureID uuid.UUID, ...) (sig *models.FlightSignature, rawToken string, err error)
- func (s *FlightSignatureService) ResolveToken(ctx context.Context, token string) (*models.FlightSignature, *models.Flight, error)
- func (s *FlightSignatureService) Revoke(ctx context.Context, flightID, userID, signatureID uuid.UUID) error
- func (s *FlightSignatureService) SignLive(ctx context.Context, flightID, userID uuid.UUID, signerName string, ...) (*models.FlightSignature, error)
- func (s *FlightSignatureService) Void(ctx context.Context, flightID, userID, signatureID uuid.UUID, reason string) error
- type IdempotencyClaim
- type IdempotencyOutcome
- type IdempotencyService
- func (s *IdempotencyService) Abandon(ctx context.Context, userID uuid.UUID, key string, claimedAt time.Time) error
- func (s *IdempotencyService) Begin(ctx context.Context, userID uuid.UUID, key string, requestHash []byte) (IdempotencyClaim, error)
- func (s *IdempotencyService) Finish(ctx context.Context, userID uuid.UUID, key string, claimedAt time.Time, ...) error
- func (s *IdempotencyService) MaxResponseBytes() int
- func (s *IdempotencyService) StartReaper(ctx context.Context, interval time.Duration)
- type IdempotentResponse
- type LicenseService
- func (s *LicenseService) CreateLicense(ctx context.Context, license *models.License) error
- func (s *LicenseService) DeleteLicense(ctx context.Context, licenseID, userID uuid.UUID) error
- func (s *LicenseService) GetLicense(ctx context.Context, licenseID, userID uuid.UUID) (*models.License, error)
- func (s *LicenseService) ListLicenses(ctx context.Context, userID uuid.UUID) ([]*models.License, error)
- func (s *LicenseService) ListLicensesUpdatedSince(ctx context.Context, userID uuid.UUID, updatedSince time.Time) ([]*models.License, error)
- func (s *LicenseService) UpdateLicense(ctx context.Context, license *models.License, userID uuid.UUID) error
- type LoginInput
- type NotificationService
- func (s *NotificationService) GetNotificationHistory(ctx context.Context, userID uuid.UUID, limit, offset int) ([]*models.NotificationLog, int, error)
- func (s *NotificationService) GetPreferences(ctx context.Context, userID uuid.UUID) (*models.NotificationPreferences, error)
- func (s *NotificationService) StartBackgroundChecker(ctx context.Context, interval time.Duration)
- func (s *NotificationService) TriggerCheck(ctx context.Context)
- func (s *NotificationService) UpdatePreferences(ctx context.Context, prefs *models.NotificationPreferences) error
- type OIDCAuthorization
- type OIDCConfig
- type OIDCService
- func (s *OIDCService) BeginLogin(ctx context.Context) (*OIDCAuthorization, error)
- func (s *OIDCService) CompleteCallback(ctx context.Context, code, state, browserToken string) (string, error)
- func (s *OIDCService) Config() OIDCConfig
- func (s *OIDCService) ExchangeHandoff(ctx context.Context, handoff string) (*models.User, *TokenPair, error)
- func (s *OIDCService) PostLoginRedirect() string
- func (s *OIDCService) StartStateReaper(ctx context.Context, interval time.Duration)
- type PasswordResetRequest
- type PasswordResetResult
- type RegisterInput
- type TokenPair
- type TwoFactorService
- func (s *TwoFactorService) Disable(ctx context.Context, userID uuid.UUID, password string) error
- func (s *TwoFactorService) IsEnabled(ctx context.Context, userID uuid.UUID) (bool, error)
- func (s *TwoFactorService) SetupTOTP(ctx context.Context, userID uuid.UUID) (string, string, error)
- func (s *TwoFactorService) ValidateTOTP(ctx context.Context, userID uuid.UUID, code string) (bool, error)
- func (s *TwoFactorService) VerifyAndEnable(ctx context.Context, userID uuid.UUID, code string) ([]string, error)
- type TwoFactorValidator
- type UnverifiedAccountConfig
- type UnverifiedAccountService
- type UploadInput
- type WebAuthnService
- func (s *WebAuthnService) BeginLogin(ctx context.Context, email string) (handle string, options *protocol.CredentialAssertion, err error)
- func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID uuid.UUID) (handle string, options *protocol.CredentialCreation, err error)
- func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID, credentialID uuid.UUID) error
- func (s *WebAuthnService) FinishLogin(ctx context.Context, handle string, responseJSON []byte) (*models.User, *TokenPair, error)
- func (s *WebAuthnService) FinishRegistration(ctx context.Context, userID uuid.UUID, handle string, label *string, ...) (*models.WebAuthnCredential, error)
- func (s *WebAuthnService) ListCredentials(ctx context.Context, userID uuid.UUID) ([]*models.WebAuthnCredential, error)
- func (s *WebAuthnService) StartSessionReaper(ctx context.Context, interval time.Duration)
Constants ¶
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 )
const ( DefaultDeliveryEventLimit = 100 MaxDeliveryEventLimit = 500 )
DefaultDeliveryEventLimit / MaxDeliveryEventLimit bound the admin listing.
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).
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.
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.
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.
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.
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 ¶
var ( ErrAircraftNotFound = errors.New("aircraft not found") ErrDuplicateRegistration = errors.New("aircraft registration already exists") )
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") // 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") )
var ( ErrClassRatingNotFound = errors.New("class rating not found") ErrInvalidClassType = errors.New("invalid class type") )
var ( ErrContactNotFound = errors.New("contact not found") // 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") )
var ( ErrCredentialNotFound = errors.New("credential not found") ErrExpiryBeforeIssue = errors.New("expiry date must be after issue date") )
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") )
var ( ErrFlightNotFound = errors.New("flight not found") ErrInvalidFlight = errors.New("invalid flight data") // 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") )
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") )
var ( ErrLicenseNotFound = errors.New("license not found") ErrInvalidLicense = errors.New("invalid license data") ErrLicenseAlreadyExists = errors.New("license already exists") )
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.", }, ) )
var ( // 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.
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") )
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.", }, ) )
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") )
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.", }, ) )
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.
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.
var ErrInvalidSessionEvent = errors.New("invalid flight session event")
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 ¶
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
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 (*AircraftService) CreateAircraft ¶
func (*AircraftService) DeleteAircraft ¶
func (*AircraftService) GetAircraft ¶
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 (*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 ¶
DeleteUser permanently deletes a user account after verifying the password
func (*AuthService) DeleteUserConfirmed ¶ added in v1.3.2
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 ¶
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
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 ¶
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 ¶
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
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 NewClassRatingService ¶
func NewClassRatingService(crRepo ClassRatingRepository, licRepo repository.LicenseRepository) *ClassRatingService
func (*ClassRatingService) CreateClassRating ¶
func (s *ClassRatingService) CreateClassRating(ctx context.Context, cr *models.ClassRating, userID uuid.UUID) error
func (*ClassRatingService) DeleteClassRating ¶
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 (*ContactService) DeleteContact ¶
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
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 (*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 (*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 (*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 (*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 (*CrewLinker) Link ¶ added in v1.3.3
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
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 ¶
CreateFlight creates a new flight log entry
func (*FlightService) DeleteAllFlights ¶
DeleteAllFlights deletes all flights for a user
func (*FlightService) DeleteBaseline ¶ added in v1.2.3
DeleteBaseline removes the user's initial-hours snapshot.
func (*FlightService) DeleteFlight ¶
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
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
func (s *FlightSessionService) RecordEvent(ctx context.Context, userID uuid.UUID, input FlightSessionEventInput) (*models.FlightSession, bool, error)
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 NewFlightSignatureService(sigRepo repository.FlightSignatureRepository, flightRepo repository.FlightRepository, userRepo repository.UserRepository) *FlightSignatureService
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
func (s *FlightSignatureService) ResolveToken(ctx context.Context, token string) (*models.FlightSignature, *models.Flight, error)
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
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 ¶
CreateLicense creates a new license for a user
func (*LicenseService) DeleteLicense ¶
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 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 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) SetupTOTP ¶
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
func (s *UnverifiedAccountService) Config() UnverifiedAccountConfig
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
func (s *UnverifiedAccountService) Start(ctx context.Context)
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
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.
Source Files
¶
- aircraft.go
- auth.go
- class_rating.go
- contact.go
- contact_vcard.go
- credential.go
- deletion.go
- document_file.go
- email_delivery.go
- flight.go
- flight_session.go
- flight_signature.go
- idempotency.go
- license.go
- notification.go
- notification_metrics.go
- oidc.go
- oidc_config.go
- password_policy.go
- twofactor.go
- unverified_account.go
- unverified_account_metrics.go
- webauthn.go
- webauthn_metrics.go
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. |