db

package
v0.0.0-...-b0eb6e9 Latest Latest
Warning

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

Go to latest
Published: Nov 5, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package db manages the database connection, migrations, and encryption for the Arkeep server. It supports SQLite (via modernc pure-Go driver, no CGO required) and PostgreSQL. Migrations are embedded in the binary and applied automatically on startup via golang-migrate.

Index

Constants

View Source
const (
	LockShared    = "shared"
	LockExclusive = "exclusive"
)

Destination lock modes (issue #319). A backup holds its destinations shared: restic's own lock for a backup is non-exclusive, so backups run side by side. A retention sweep or an integrity check holds its destination exclusive.

View Source
const (
	NotifyInherit = "inherit" // follow the global notification setting
	NotifyAlways  = "always"
	NotifyNever   = "never"
)

Values of Policy.NotifyOnSuccess / Policy.NotifyOnFailure.

Variables

This section is empty.

Functions

func InitEncryption

func InitEncryption(key []byte) error

InitEncryption sets the AES-256 key used to encrypt and decrypt sensitive fields at rest. key must be exactly 32 bytes (AES-256).

Call this once during application startup, before calling db.New:

if err := db.InitEncryption([]byte(os.Getenv("ARKEEP_SECRET_KEY"))); err != nil {
    log.Fatal(err)
}

func LockModeFor

func LockModeFor(jobType string) string

LockModeFor returns the mode a job of jobType holds its destinations in.

func New

func New(cfg Config) (*gorm.DB, error)

New opens a database connection, applies pending migrations, and returns the ready-to-use *gorm.DB instance.

Types

type Agent

type Agent struct {
	SoftDelete
	Name       string `gorm:"not null"`
	Hostname   string `gorm:"not null"`
	IPAddress  string `gorm:"not null;default:''"`
	OS         string `gorm:"not null;default:''"`
	Arch       string `gorm:"not null;default:''"`
	Version    string `gorm:"not null;default:''"`
	Status     string `gorm:"not null;default:'offline'"` // "online", "offline", "error"
	LastSeenAt *time.Time
	// DockerAvailable is true when the agent can reach the Docker daemon on its host.
	// Advertised by the agent in the Register RPC via AgentCapabilities.docker.
	// Used by the GUI to show or hide the Docker volume source option in the policy form.
	DockerAvailable bool `gorm:"not null;default:false"`
	// CertFingerprint is the hex SHA-256 of the mTLS client certificate the
	// agent is bound to; nil until its first registration with one. Read-only
	// for GORM so that Save on a stale copy can never undo a binding: it is
	// written only by AgentRepository.BindCertFingerprint/ResetCertFingerprint.
	CertFingerprint *string `gorm:"->"`
}

Agent represents a registered backup agent running on a remote machine. Agents connect to the server via a persistent gRPC stream (pull pattern) and do not expose any ports. The RegistrationToken is used only during the initial handshake and is cleared after successful registration.

type AuditLog

type AuditLog struct {
	ID           uuid.UUID `gorm:"type:text;primaryKey"`
	CreatedAt    time.Time `gorm:"not null"`
	UserID       uuid.UUID `gorm:"type:text;not null;index"`
	UserEmail    string    `gorm:"not null"`
	Action       string    `gorm:"not null;index"` // e.g. "policy.update", "snapshot.restore"
	ResourceType string    `gorm:"not null;default:''"`
	ResourceID   string    `gorm:"type:text;not null;default:''"`
	Details      string    `gorm:"type:text;not null;default:'{}'"` // JSON
	IPAddress    string    `gorm:"not null;default:''"`
}

AuditLog records every significant mutation performed via the API: who did it, what was changed, when, and from which IP address. The table is append-only — records are never updated or deleted. user_email is stored denormalized so the log remains readable even if the user account is later deleted.

func (*AuditLog) BeforeCreate

func (a *AuditLog) BeforeCreate(tx *gorm.DB) error

BeforeCreate generates a UUID v7 for new audit records (no UpdatedAt — append-only).

func (AuditLog) TableName

func (AuditLog) TableName() string

TableName maps to the migration-created table name.

type Base

type Base struct {
	ID        uuid.UUID `gorm:"type:text;primaryKey"`
	CreatedAt time.Time `gorm:"not null"`
	UpdatedAt time.Time `gorm:"not null"`
}

base contains the common fields shared by all models. ID uses UUID v7 (time-ordered) for efficient B-tree indexing and natural chronological ordering without a separate created_at sort. CreatedAt and UpdatedAt are managed automatically by GORM.

func (*Base) BeforeCreate

func (b *Base) BeforeCreate(tx *gorm.DB) error

BeforeCreate generates a new UUID v7 if the ID is not already set. This ensures every record has a valid time-ordered ID before insertion.

type Config

type Config struct {
	Driver   string // "sqlite" or "postgres"
	DSN      string
	Logger   *zap.Logger
	LogLevel gormlogger.LogLevel
}

Config holds the configuration required to open a database connection. Driver defaults to "sqlite" if left empty.

type Destination

type Destination struct {
	SoftDelete
	Name        string          `gorm:"not null"`
	Type        string          `gorm:"not null"`               // "local", "s3", "sftp", "rest", "rclone"
	Credentials EncryptedString `gorm:"type:text"`              // JSON, encrypted
	Config      string          `gorm:"type:text;default:'{}'"` // JSON, not sensitive
	Enabled     bool            `gorm:"not null;default:true"`
	// RepoSizeBytes is the real deduplicated on-disk size of this destination's
	// restic repository (from `restic stats --mode raw-data`), refreshed after
	// each backup. Zero until the first backup or import completes.
	RepoSizeBytes     int64      `gorm:"not null;default:0"`
	RepoSizeUpdatedAt *time.Time `gorm:""`
	// RepoInitializedAt is when the restic repository was first known to
	// exist (the first job or snapshot sync that succeeded against it); nil
	// until then. Once set, a backup never runs `restic init` again, so a
	// repository that has gone missing — an unmounted disk, an emptied bucket —
	// fails the backup instead of being replaced by an empty one. Read-only
	// for GORM so that Save on a stale copy can never set or clear it: it is
	// written only by DestinationRepository.MarkRepoInitialized and
	// ResetRepoInitialized.
	RepoInitializedAt *time.Time `gorm:"->"`
	// RepoPassword is the Restic repository password. Originally populated
	// only for destinations whose snapshots were imported from a pre-existing
	// repository (browse/restore of those has no policy to take the password
	// from); now also the password the retention agent uses to unlock this
	// destination's repository for a standalone retention sweep, backfilled
	// from whichever policy was attached to this destination at the time
	// retention moved here (see the one-time migration backfill in
	// cmd/server/main.go). Any policy attached to a given destination shares
	// the same physical repository, so any one of their passwords is correct.
	RepoPassword EncryptedString `gorm:"type:text"`

	// Retention — one configuration per destination, applied uniformly to
	// every policy's own snapshot-tag pool here (see policy_destinations and
	// the "policy:<id>" restic tag convention). Moved off Policy so retention
	// runs on its own schedule, detached from any single backup job — see
	// server/internal/retentionscheduler.
	RetentionLast     int    `gorm:"not null;default:0"`
	RetentionHourly   int    `gorm:"not null;default:0"`
	RetentionDaily    int    `gorm:"not null;default:0"`
	RetentionWeekly   int    `gorm:"not null;default:0"`
	RetentionMonthly  int    `gorm:"not null;default:0"`
	RetentionYearly   int    `gorm:"not null;default:0"`
	RetentionSchedule string `gorm:"not null;default:''"` // cron expression; "" = unconfigured
	RetentionEnabled  bool   `gorm:"not null;default:false"`
	// RetentionAgentID is the connected agent responsible for running this
	// destination's retention sweeps. Explicit and admin-chosen — a
	// destination has no other owning agent (any number of policies, on any
	// agents, can write here). nil = not yet assigned.
	RetentionAgentID *uuid.UUID `gorm:"type:text"`
	// AppendOnly marks a repository that can never support `restic forget
	// --prune` (e.g. WORM/object-lock storage). The retention scheduler must
	// never even attempt a sweep here.
	AppendOnly bool `gorm:"not null;default:false"`
	// RetentionNeedsReview is set by the one-time migration backfill when this
	// destination was shared by 2+ policies with no single retention
	// configuration to inherit unambiguously. Cleared the first time an admin
	// explicitly saves retention config for this destination.
	RetentionNeedsReview bool `gorm:"not null;default:false"`

	// Integrity check (restic check) — run on its own schedule by
	// RetentionAgentID, which acts as the destination's maintenance agent for
	// both retention and check. See server/internal/checkscheduler.
	CheckEnabled       bool   `gorm:"not null;default:false"`
	CheckSchedule      string `gorm:"not null;default:''"`       // cron expression; "" = unconfigured
	CheckMode          string `gorm:"not null;default:'subset'"` // structure | subset | full
	CheckSubsetPercent int    `gorm:"not null;default:5"`        // used by mode "subset" only
	// Outcome of the most recent check job. LastCheckStatus is "" when no
	// check has completed yet, otherwise the job's terminal status.
	LastCheckAt     *time.Time
	LastCheckStatus string     `gorm:"not null;default:''"`
	LastCheckJobID  *uuid.UUID `gorm:"type:text"`
}

Destination represents a backup storage target. Credentials are encrypted at rest via EncryptedString. The Config field holds provider-specific settings serialized as JSON (e.g. bucket name, endpoint, region for S3).

type DestinationLock

type DestinationLock struct {
	DestinationID uuid.UUID `gorm:"type:text;primaryKey"`
	JobID         uuid.UUID `gorm:"type:text;primaryKey"`
	Mode          string    `gorm:"not null"` // LockShared | LockExclusive
	AcquiredAt    time.Time `gorm:"not null"`
}

DestinationLock is one job's hold on a destination's repository: the server-side gate that keeps a retention sweep or integrity check from running alongside anything else on the same repository, instead of racing restic's own lock file (issues #130, #319). Deleting the job drops its holds.

type EncryptedString

type EncryptedString string

EncryptedString is a string type that is transparently encrypted with AES-256-GCM before being written to the database, and decrypted after being read. Use it for any sensitive field (credentials, passwords, tokens).

The value stored in the database is a base64-encoded string in the format:

base64(nonce + ciphertext)

An empty EncryptedString is stored as an empty string without encryption.

func (*EncryptedString) Scan

func (e *EncryptedString) Scan(value interface{}) error

Scan implements sql.Scanner. Called by GORM after reading from the database. Decodes the base64 string and decrypts it with AES-256-GCM.

func (EncryptedString) Value

func (e EncryptedString) Value() (driver.Value, error)

Value implements driver.Valuer. Called by GORM before writing to the database. Encrypts the string value with AES-256-GCM and encodes it as base64.

type Job

type Job struct {
	Base
	// PolicyID is nil for a restore job started from an imported snapshot:
	// such a snapshot belongs to no policy.
	PolicyID  *uuid.UUID `gorm:"type:text;index"`
	AgentID   uuid.UUID  `gorm:"type:text;not null;index"`
	Type      string     `gorm:"not null;default:'backup'"`  // "backup", "restore"
	Status    string     `gorm:"not null;default:'pending'"` // "pending", "waiting", "running", "succeeded", "failed", "cancelled", "interrupted"
	StartedAt *time.Time
	EndedAt   *time.Time
	Error     string `gorm:"type:text;default:''"` // populated on failure

	// ResumeOfJobID is the interrupted job this run resumes, nil for a normal
	// run. No foreign key: the original job may be removed by job retention.
	ResumeOfJobID *uuid.UUID `gorm:"type:text"`
	// ResumeAttempt counts consecutive resumes, so a host that dies at the same
	// point every time stops being retried. Zero for a normal run.
	ResumeAttempt int `gorm:"not null;default:0"`
}

Job represents a single backup execution triggered by the scheduler or manually. Status transitions: pending -> running -> succeeded | failed | cancelled | interrupted.

"interrupted" means the agent vanished while the job was running (host shut down, sleep, network loss), as opposed to "failed", which is a real error reported by the agent. Only interrupted jobs are eligible for automatic resume.

type JobDestination

type JobDestination struct {
	Base
	JobID         uuid.UUID `gorm:"type:text;not null;index"`
	DestinationID uuid.UUID `gorm:"type:text;not null;index"`
	Status        string    `gorm:"not null;default:'pending'"` // mirrors Job.Status
	SnapshotID    string    `gorm:"default:''"`                 // opaque ID returned by the backup engine
	SizeBytes     int64     `gorm:"default:0"`
	StartedAt     *time.Time
	EndedAt       *time.Time
	Error         string `gorm:"type:text;default:''"`
}

JobDestination tracks the result of a backup job for each individual destination. A job can partially succeed if some destinations fail.

type JobDestinationCommand

type JobDestinationCommand struct {
	Base
	JobID         uuid.UUID `gorm:"type:text;not null;index"`
	DestinationID uuid.UUID `gorm:"type:text;not null;index"`
	// SourceName is the policy source's name — also the restic
	// --stdin-filename and the suffix of its retention tag.
	SourceName string `gorm:"not null"`
	Status     string `gorm:"not null;default:'pending'"`
	SnapshotID string `gorm:"default:''"`
	SizeBytes  int64  `gorm:"default:0"`
	StartedAt  *time.Time
	EndedAt    *time.Time
	Error      string `gorm:"type:text;default:''"`
}

JobDestinationCommand tracks the result of one command-type source backed up to one destination. A command source is its own restic invocation and its own snapshot, so a job can have several of these per destination — which is why they cannot live in JobDestination (UNIQUE(job_id, destination_id), one scalar snapshot_id).

type JobLog

type JobLog struct {
	Base
	JobID     uuid.UUID `gorm:"type:text;not null;index"`
	Level     string    `gorm:"not null"` // "info", "warn", "error"
	Message   string    `gorm:"type:text;not null"`
	Timestamp time.Time `gorm:"not null;index"`
}

JobLog stores structured log lines emitted during a job execution. Logs are flushed to the database in batches during execution so that the GUI can show partial logs even for in-progress jobs.

type JobRetentionTag

type JobRetentionTag struct {
	Base
	JobID         uuid.UUID `gorm:"type:text;not null;index"`
	DestinationID uuid.UUID `gorm:"type:text;not null;index"`
	// Tag is the restic --tag value swept, e.g. "policy:<id>" or
	// "policy:<id>:command:<name>".
	Tag       string `gorm:"not null"`
	Status    string `gorm:"not null;default:'pending'"` // "pending", "running", "succeeded", "failed", "skipped"
	StartedAt *time.Time
	EndedAt   *time.Time
	Error     string `gorm:"type:text;default:''"`
}

JobRetentionTag tracks the result of one restic `forget --prune --tag` sweep within a standalone retention job (JOB_TYPE_FORGET). A retention job covers one Destination but must sweep every policy's own tag pool there independently (see agent/internal/restic/wrapper.go's Forget, which refuses to run unscoped), so a job can have several of these per destination — no snapshot/size fields, unlike JobDestinationCommand: a forget never creates a snapshot.

type Notification

type Notification struct {
	Base
	UserID  uuid.UUID `gorm:"type:text;not null;index"`
	Type    string    `gorm:"not null"` // "job_success", "job_failure", "agent_offline", etc.
	Title   string    `gorm:"not null"`
	Body    string    `gorm:"type:text;not null"`
	ReadAt  *time.Time
	Payload string `gorm:"type:text;default:'{}'"` // JSON, extra context for the frontend
}

Notification stores in-app notifications delivered to users via WebSocket. Read notifications are kept for 30 days and then purged by a background job.

type NotificationDelivery

type NotificationDelivery struct {
	Base
	NotificationID uuid.UUID  `gorm:"type:text;not null;index"`
	Type           string     `gorm:"not null"`                   // "email" | "webhook"
	Status         string     `gorm:"not null;default:'pending'"` // "pending" | "sent" | "exhausted"
	Attempts       int        `gorm:"not null;default:0"`
	LastError      string     `gorm:"type:text;not null;default:''"`
	NextRetryAt    *time.Time // nil = ready to process immediately
}

NotificationDelivery tracks the delivery state of a single notification over a single external channel (email or webhook). Each Notification can have at most one NotificationDelivery row per channel type.

Status transitions:

pending → sent      (delivery succeeded)
pending → pending   (retry scheduled after backoff)
pending → exhausted (max 3 retries exceeded, no further attempts)

Rows are automatically removed when the parent Notification is deleted (ON DELETE CASCADE on the foreign key).

func (NotificationDelivery) TableName

func (NotificationDelivery) TableName() string

TableName maps to the migration-created table name.

type OIDCProvider

type OIDCProvider struct {
	Base
	Name         string          `gorm:"not null"`
	Issuer       string          `gorm:"not null"`
	ClientID     string          `gorm:"not null"`
	ClientSecret EncryptedString `gorm:"type:text;not null"`
	Scopes       string          `gorm:"not null;default:'openid email profile'"` // space-separated
	Enabled      bool            `gorm:"not null;default:false"`
	// GroupsClaim names the ID token / UserInfo claim holding the user's
	// groups. Required when AllowedGroups or AdminGroups is set.
	GroupsClaim string `gorm:"not null;default:''"`
	// AllowedGroups restricts sign-in to members of at least one of these
	// groups; empty allows every user of the IdP.
	AllowedGroups string `gorm:"not null;default:''"` // comma-separated
	// AdminGroups grants the admin role to their members and the user role to
	// everyone else, re-evaluated on every login; empty leaves roles alone.
	AdminGroups string `gorm:"not null;default:''"` // comma-separated
	// SessionMaxHours caps how long a session started through this provider
	// lasts before the user must sign in at the IdP again.
	SessionMaxHours int `gorm:"not null;default:12"`
}

OIDCProvider stores the configuration for an external OIDC identity provider. ClientSecret is encrypted at rest. Multiple providers are supported. The callback URL is computed server-side as {base_url}/auth/oidc/callback and is not stored in the database.

func (OIDCProvider) TableName

func (OIDCProvider) TableName() string

TableName overrides GORM's default naming convention, which would produce "o_id_c_providers" by splitting each uppercase letter as a word boundary.

type PasswordChangeChallenge

type PasswordChangeChallenge struct {
	Base
	UserID    uuid.UUID  `gorm:"type:text;not null;index"`
	TokenHash string     `gorm:"not null;index"` // SHA-256 hex of the raw token
	ExpiresAt time.Time  `gorm:"not null"`
	UsedAt    *time.Time // nil = still usable
}

PasswordChangeChallenge is the short-lived interim state of a login whose account must change its password: the password (and the second factor, when enabled) has been verified, but no session is issued until a new password is set. Only the SHA-256 hash of the token is stored. It is single-use (UsedAt).

type PasswordResetToken

type PasswordResetToken struct {
	Base
	UserID    uuid.UUID  `gorm:"type:text;not null;index"`
	TokenHash string     `gorm:"not null;index"` // SHA-256 hex of the raw token
	ExpiresAt time.Time  `gorm:"not null"`
	UsedAt    *time.Time // nil = not yet used
}

PasswordResetToken stores a hashed, single-use token for the self-service password reset flow. Only local accounts can reset their password — OIDC users are managed by their identity provider. The raw token is never stored: only its SHA-256 hash. Tokens expire after a short window and are consumed (UsedAt set) on the first successful reset.

type Policy

type Policy struct {
	SoftDelete
	Name            string          `gorm:"not null"`
	AgentID         uuid.UUID       `gorm:"type:text;not null;index"`
	Schedule        string          `gorm:"not null"` // cron expression
	Enabled         bool            `gorm:"not null;default:true"`
	Sources         string          `gorm:"type:text;not null"`     // JSON array of source paths
	RepoPassword    EncryptedString `gorm:"type:text;not null"`     // Restic repository password
	HookPreBackup   string          `gorm:"type:text;default:''"`   // shell command, optional
	HookPostBackup  string          `gorm:"type:text;default:''"`   // shell command, optional
	ExcludePatterns string          `gorm:"type:text;default:'[]'"` // JSON array of --exclude patterns
	// ResumeInterrupted enables automatic resume of a backup whose agent
	// disconnected mid-run. New policies default to true — restic reuses the
	// packs already uploaded, so resuming only transfers what is missing.
	//
	// No `default` tag on purpose: GORM omits a zero-valued field from the INSERT
	// when it knows the column has a default, which would silently turn an
	// explicit false into true. The DEFAULT TRUE in the migration is what
	// backfills pre-existing rows.
	ResumeInterrupted bool `gorm:"not null"`
	// NotifyOnSuccess / NotifyOnFailure override the global job_success /
	// job_failure notification toggles for this policy's jobs. One of the
	// Notify* values below.
	NotifyOnSuccess string `gorm:"type:text;not null"`
	NotifyOnFailure string `gorm:"type:text;not null"`
	// HealthcheckURL is an optional Healthchecks.io ping URL; backup jobs ping
	// it on start, success and failure (see internal/healthcheck). Empty
	// disables the pings.
	HealthcheckURL string `gorm:"type:text;not null;default:''"`
	LastRunAt      *time.Time
	NextRunAt      *time.Time
}

Policy defines what to back up, when, and how. It is associated with one agent and one or more destinations via PolicyDestination. The schedule uses standard cron expression syntax (e.g. "0 2 * * *" for 2 AM daily).

Association fields are intentionally absent from this struct. GORM cannot resolve foreign keys when the primary key is uuid.UUID (a custom type). Related records are loaded via explicit queries in the repository layer (see repository/policy.go: GetByIDWithDestinations).

type PolicyDestination

type PolicyDestination struct {
	Base
	PolicyID      uuid.UUID `gorm:"type:text;not null;index"`
	DestinationID uuid.UUID `gorm:"type:text;not null;index"`
	Priority      int       `gorm:"not null;default:0"`
}

PolicyDestination is the join table between Policy and Destination. Priority determines the order in which destinations are tried (lower = first). This enables 3-2-1 backup rules with multiple destinations per policy.

type RecoveryCode

type RecoveryCode struct {
	Base
	UserID   uuid.UUID  `gorm:"type:text;not null;index"`
	CodeHash string     `gorm:"not null;index"` // SHA-256 hex of the normalised code
	UsedAt   *time.Time // nil = not yet used
}

RecoveryCode is a single-use fallback credential issued when a user enables two-factor authentication. Only the SHA-256 hash of the normalised code is stored; the raw codes are shown to the user exactly once.

type RefreshToken

type RefreshToken struct {
	Base
	UserID    uuid.UUID `gorm:"type:text;not null;index"`
	TokenHash string    `gorm:"not null;uniqueIndex"` // SHA-256 hex of the raw token
	ExpiresAt time.Time `gorm:"not null;index"`
	RevokedAt *time.Time
	UserAgent string
	IPAddress string
	// SessionExpiresAt is the absolute deadline of an OIDC session, copied on
	// every rotation so refreshing cannot extend it. nil = no deadline (local
	// logins).
	SessionExpiresAt *time.Time
}

RefreshToken stores a hashed refresh token associated with a user session. The raw token is never stored — only its SHA-256 hash. Tokens are rotated on every use and expire after 7 days.

type Setting

type Setting struct {
	Key       string          `gorm:"primaryKey"`
	Value     EncryptedString `gorm:"type:text;not null"`
	UpdatedAt time.Time       `gorm:"not null;autoUpdateTime"`
}

Setting is a generic key-value configuration entry stored in the database. Keys are namespaced by convention (e.g. "smtp.host", "webhook.url"). Sensitive values (e.g. "smtp.password") are encrypted at the application layer via EncryptedString before being persisted.

Setting does not embed base because it uses a string primary key (the key itself) rather than a UUID, and does not need CreatedAt.

type Snapshot

type Snapshot struct {
	Base
	// PolicyID and JobID are nil for snapshots imported from a pre-existing
	// repository: they belong to a destination but were not produced by any
	// backup run of any policy.
	PolicyID      *uuid.UUID `gorm:"type:text;index"`
	DestinationID uuid.UUID  `gorm:"type:text;not null;index"`
	JobID         *uuid.UUID `gorm:"type:text;index"`
	SnapshotID    string     `gorm:"not null;index"` // opaque ID from the backup engine
	// SizeBytes is the real footprint this backup added to the repository
	// (restic data_added_packed), not the logical source size — so it reconciles
	// with the destination's real repo size and never double-counts. Zero when
	// the backup added nothing new (e.g. re-backup of unchanged data).
	SizeBytes  int64     `gorm:"default:0"`
	FileCount  int64     `gorm:"default:0"`
	Tags       string    `gorm:"type:text;default:'[]'"` // JSON array
	Sources    string    `gorm:"type:text;default:'[]'"` // JSON array of paths backed up
	Hostname   string    `gorm:"not null;default:''"`
	IsImported bool      `gorm:"not null;default:false"`
	SnapshotAt time.Time `gorm:"not null;index"`
}

Snapshot represents a point-in-time backup recorded by the backup engine. Snapshots are synced from the engine after each successful job and cached in the database for fast listing and filtering without hitting the engine.

type SoftDelete

type SoftDelete struct {
	Base
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

softDelete extends base with a nullable DeletedAt field for soft deletion. GORM automatically filters out soft-deleted records from all queries unless Unscoped() is used explicitly.

type TwoFactorChallenge

type TwoFactorChallenge struct {
	Base
	UserID    uuid.UUID  `gorm:"type:text;not null;index"`
	TokenHash string     `gorm:"not null;index"` // SHA-256 hex of the raw token
	Attempts  int        `gorm:"not null;default:0"`
	ExpiresAt time.Time  `gorm:"not null"`
	UsedAt    *time.Time // nil = still usable
	// SessionExpiresAt carries the OIDC session deadline to the tokens issued
	// once the code is verified. nil for local logins.
	SessionExpiresAt *time.Time
}

TwoFactorChallenge is the short-lived interim state of a two-step login: the password has been verified but the TOTP code has not. Only the SHA-256 hash of the challenge token is stored — the raw token is returned to the client once and echoed back on the second step. Attempts counts failed codes; the challenge is consumed (UsedAt set) on success or after too many failures.

type User

type User struct {
	Base
	Email        string          `gorm:"uniqueIndex;not null"`
	Password     EncryptedString `gorm:"type:text"` // empty for OIDC users
	DisplayName  string          `gorm:"not null"`
	Role         string          `gorm:"not null;default:'user'"`         // "admin" or "user"
	IsActive     bool            `gorm:"not null;default:true"`           // false = account disabled
	OIDCProvider string          `gorm:"column:oidc_provider;default:''"` // provider ID if OIDC user
	OIDCSub      string          `gorm:"column:oidc_sub;default:''"`      // subject claim from OIDC token
	LastLoginAt  *time.Time
	// TOTPSecret is the base32 TOTP shared secret, encrypted at rest. Empty
	// means no secret. A non-empty secret with TwoFactorEnabled false is a
	// pending enrollment that was never confirmed.
	TOTPSecret       EncryptedString `gorm:"type:text"`
	TwoFactorEnabled bool            `gorm:"not null;default:false"`
	// MustChangePassword stops a local login before any session is issued
	// and asks for a new password. Set by an admin with Set password.
	MustChangePassword bool `gorm:"not null;default:false"`
}

User represents a local or OIDC-authenticated user. Password is only set for local accounts — OIDC users authenticate via the provider and have an empty Password field.

Directories

Path Synopsis
Package dbtest gives tests a SQLite database with every migration applied.
Package dbtest gives tests a SQLite database with every migration applied.

Jump to

Keyboard shortcuts

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