mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-10 07:47:29 -04:00
A key can now be paused until the owner resumes it, or until a given
time. A paused key is rejected by validation before last_used is
updated, and a pause time that has passed lifts the pause by itself.
Existing keys stay active.
PATCH /api/auth/api-keys/:id takes {"disabled": bool, "paused_until":
RFC 3339 string or null}. Only the key owner can change it, and a
paused_until in the past is rejected. The key list returns the pause
fields. The Account page gets a Pause and Resume button for each key
and a Paused badge that shows the resume time.
Assisted-by: Claude Code:claude-sonnet-5-5
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
160 lines
4.9 KiB
Go
160 lines
4.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Auth provider constants.
|
|
const (
|
|
ProviderLocal = "local"
|
|
ProviderGitHub = "github"
|
|
ProviderOIDC = "oidc"
|
|
ProviderAgentWorker = "agent-worker"
|
|
)
|
|
|
|
// User represents an authenticated user.
|
|
type User struct {
|
|
ID string `gorm:"primaryKey;size:36"`
|
|
Email string `gorm:"size:255;index"`
|
|
Name string `gorm:"size:255"`
|
|
AvatarURL string `gorm:"size:512"`
|
|
Provider string `gorm:"size:50"` // ProviderLocal, ProviderGitHub, ProviderOIDC
|
|
Subject string `gorm:"size:255"` // provider-specific user ID
|
|
PasswordHash string `json:"-"` // bcrypt hash, empty for OAuth-only users
|
|
Role string `gorm:"size:20;default:user"`
|
|
Status string `gorm:"size:20;default:active"` // "active", "pending"
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Session represents a user login session.
|
|
type Session struct {
|
|
ID string `gorm:"primaryKey;size:64"` // HMAC-SHA256 hash of session token
|
|
UserID string `gorm:"size:36;index"`
|
|
ExpiresAt time.Time
|
|
RotatedAt time.Time
|
|
CreatedAt time.Time
|
|
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
|
|
}
|
|
|
|
// UserAPIKey represents a user-generated API key for programmatic access.
|
|
type UserAPIKey struct {
|
|
ID string `gorm:"primaryKey;size:36"`
|
|
UserID string `gorm:"size:36;index"`
|
|
Name string `gorm:"size:255"` // user-provided label
|
|
KeyHash string `gorm:"size:64;uniqueIndex"`
|
|
KeyPrefix string `gorm:"size:12"` // first 8 chars of key for display
|
|
Role string `gorm:"size:20"`
|
|
CreatedAt time.Time
|
|
ExpiresAt *time.Time `gorm:"index"`
|
|
LastUsed *time.Time
|
|
// Disabled pauses the key until the owner resumes it.
|
|
Disabled bool
|
|
// PausedUntil pauses the key until the given time; the key becomes
|
|
// active again by itself once that time has passed.
|
|
PausedUntil *time.Time
|
|
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
|
|
}
|
|
|
|
// IsPaused reports whether the key is paused at the given time.
|
|
func (k *UserAPIKey) IsPaused(now time.Time) bool {
|
|
return k.Disabled || (k.PausedUntil != nil && k.PausedUntil.After(now))
|
|
}
|
|
|
|
// PermissionMap is a flexible map of feature -> enabled, stored as JSON text.
|
|
// Known features: "agents", "skills", "collections", "mcp_jobs".
|
|
// New features can be added without schema changes.
|
|
type PermissionMap map[string]bool
|
|
|
|
// Value implements driver.Valuer for GORM JSON serialization.
|
|
func (p PermissionMap) Value() (driver.Value, error) {
|
|
if p == nil {
|
|
return "{}", nil
|
|
}
|
|
b, err := json.Marshal(p)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal PermissionMap: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// Scan implements sql.Scanner for GORM JSON deserialization.
|
|
func (p *PermissionMap) Scan(value any) error {
|
|
if value == nil {
|
|
*p = PermissionMap{}
|
|
return nil
|
|
}
|
|
var bytes []byte
|
|
switch v := value.(type) {
|
|
case string:
|
|
bytes = []byte(v)
|
|
case []byte:
|
|
bytes = v
|
|
default:
|
|
return fmt.Errorf("cannot scan %T into PermissionMap", value)
|
|
}
|
|
return json.Unmarshal(bytes, p)
|
|
}
|
|
|
|
// InviteCode represents an admin-generated invitation for user registration.
|
|
type InviteCode struct {
|
|
ID string `gorm:"primaryKey;size:36"`
|
|
Code string `gorm:"uniqueIndex;not null;size:64"` // HMAC-SHA256 hash of invite code
|
|
CodePrefix string `gorm:"size:12"` // first 8 chars for admin display
|
|
CreatedBy string `gorm:"size:36;not null"`
|
|
UsedBy *string `gorm:"size:36"`
|
|
UsedAt *time.Time
|
|
ExpiresAt time.Time `gorm:"not null;index"`
|
|
CreatedAt time.Time
|
|
Creator User `gorm:"foreignKey:CreatedBy"`
|
|
Consumer *User `gorm:"foreignKey:UsedBy"`
|
|
}
|
|
|
|
// ModelAllowlist controls which models a user can access.
|
|
// When Enabled is false (default), all models are allowed.
|
|
type ModelAllowlist struct {
|
|
Enabled bool `json:"enabled"`
|
|
Models []string `json:"models,omitempty"`
|
|
}
|
|
|
|
// Value implements driver.Valuer for GORM JSON serialization.
|
|
func (m ModelAllowlist) Value() (driver.Value, error) {
|
|
b, err := json.Marshal(m)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal ModelAllowlist: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// Scan implements sql.Scanner for GORM JSON deserialization.
|
|
func (m *ModelAllowlist) Scan(value any) error {
|
|
if value == nil {
|
|
*m = ModelAllowlist{}
|
|
return nil
|
|
}
|
|
var bytes []byte
|
|
switch v := value.(type) {
|
|
case string:
|
|
bytes = []byte(v)
|
|
case []byte:
|
|
bytes = v
|
|
default:
|
|
return fmt.Errorf("cannot scan %T into ModelAllowlist", value)
|
|
}
|
|
return json.Unmarshal(bytes, m)
|
|
}
|
|
|
|
// UserPermission stores per-user feature permissions.
|
|
type UserPermission struct {
|
|
ID string `gorm:"primaryKey;size:36"`
|
|
UserID string `gorm:"size:36;uniqueIndex"`
|
|
Permissions PermissionMap `gorm:"type:text"`
|
|
AllowedModels ModelAllowlist `gorm:"type:text"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
|
|
}
|