2024-05-31 09:54:39 +00:00
|
|
|
package storage
|
|
|
|
|
|
|
|
import (
|
2024-10-15 14:16:18 +00:00
|
|
|
"crypto/ed25519"
|
2024-08-28 15:20:38 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"errors"
|
2024-11-04 15:25:39 +00:00
|
|
|
"fmt"
|
2024-09-12 06:56:57 +00:00
|
|
|
"strings"
|
2024-05-31 09:54:39 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
2024-09-27 14:53:22 +00:00
|
|
|
"github.com/google/uuid"
|
2024-08-28 15:20:38 +00:00
|
|
|
"github.com/mstarongithub/passkey"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"gitlab.com/mstarongitlab/linstrom/ap"
|
2024-11-06 15:57:44 +00:00
|
|
|
"gitlab.com/mstarongitlab/linstrom/util"
|
2024-05-31 09:54:39 +00:00
|
|
|
"gorm.io/gorm"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Database representation of a user account
|
|
|
|
// This can be a bot, remote or not
|
|
|
|
// If remote, this is used for caching the account
|
2024-08-28 15:20:38 +00:00
|
|
|
type Account struct {
|
|
|
|
ID string `gorm:"primarykey"` // ID is a uuid for this account
|
2024-10-15 14:16:18 +00:00
|
|
|
// Username of the user (eg "max" if the full username is @max@example.com)
|
2024-08-28 15:20:38 +00:00
|
|
|
// Assume unchangable (once set by a user) to be kind to other implementations
|
|
|
|
// Would be an easy avenue to fuck with them though
|
2024-11-11 08:04:42 +00:00
|
|
|
Username string `gorm:"unique"`
|
2024-08-28 15:20:38 +00:00
|
|
|
CreatedAt time.Time // When this entry was created. Automatically set by gorm
|
|
|
|
// When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm
|
|
|
|
UpdatedAt time.Time
|
2024-06-06 11:54:50 +00:00
|
|
|
// When this entry was deleted (for soft deletions)
|
|
|
|
// Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to
|
|
|
|
// If not null, this entry is marked as deleted
|
2024-10-26 09:42:51 +00:00
|
|
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
|
|
|
// Server RemoteServer // `gorm:"foreignKey:ServerId;references:ID"` // The server this user is from
|
|
|
|
ServerId uint // Id of the server this user is from, needed for including RemoteServer
|
|
|
|
DisplayName string // The display name of the user. Can be different from the handle
|
|
|
|
CustomFields []uint `gorm:"serializer:json"` // IDs to the custom fields a user has
|
|
|
|
Description string // The description of a user account
|
|
|
|
Tags []string `gorm:"serializer:json"` // Hashtags
|
|
|
|
IsBot bool // Whether to mark this account as a script controlled one
|
2024-11-11 08:04:42 +00:00
|
|
|
Relations []uint `gorm:"serializer:json"` // List of ids of all relations this account has. Both follows and followers
|
2024-10-26 09:42:51 +00:00
|
|
|
Icon string // ID of a media file used as icon
|
2024-11-06 15:57:44 +00:00
|
|
|
Background *string // ID of a media file used as background image
|
|
|
|
Banner *string // ID of a media file used as banner
|
2024-10-26 09:42:51 +00:00
|
|
|
Indexable bool // Whether this account can be found by crawlers
|
|
|
|
PublicKey []byte // The public key of the account
|
2024-05-31 09:54:39 +00:00
|
|
|
// Whether this account restricts following
|
|
|
|
// If true, the owner must approve of a follow request first
|
|
|
|
RestrictedFollow bool
|
|
|
|
// List of things the owner identifies as
|
|
|
|
// Example [cat human robot] means that the owner probably identifies as
|
2024-08-28 15:20:38 +00:00
|
|
|
// a cyborg-catgirl/boy/human or a cathuman shaped robot, refer to Gender for pronouns
|
|
|
|
IdentifiesAs []Being `gorm:"serializer:json"`
|
2024-05-31 09:54:39 +00:00
|
|
|
// List of pronouns the owner identifies with
|
|
|
|
// An unordered list since the owner can freely set it
|
2024-09-19 11:50:37 +00:00
|
|
|
// Examples: [she her], [it they its them] or, if you want to go fancy, [this is super serious]
|
2024-08-28 15:20:38 +00:00
|
|
|
Gender []string `gorm:"serializer:json"`
|
2024-11-04 15:25:39 +00:00
|
|
|
// The roles assocciated with an account. Values are the names of the roles
|
2024-08-28 15:20:38 +00:00
|
|
|
Roles []string `gorm:"serializer:json"`
|
2024-05-31 09:54:39 +00:00
|
|
|
|
2024-11-05 15:29:01 +00:00
|
|
|
Location *string
|
|
|
|
Birthday *time.Time
|
|
|
|
|
2024-05-31 09:54:39 +00:00
|
|
|
// --- And internal account stuff ---
|
|
|
|
// Still public fields since they wouldn't be able to be stored in the db otherwise
|
2024-10-15 14:16:18 +00:00
|
|
|
PrivateKey []byte // The private key of the account. Nil if remote user
|
|
|
|
WebAuthnId []byte // The unique and random ID of this account used for passkey authentication
|
2024-08-28 15:20:38 +00:00
|
|
|
// Whether the account got verified and is allowed to be active
|
|
|
|
// For local accounts being active means being allowed to login and perform interactions
|
|
|
|
// For remote users, if an account is not verified, any interactions it sends are discarded
|
2024-11-04 06:54:29 +00:00
|
|
|
Verified bool
|
|
|
|
// TODO: Turn this into a map to give passkeys names.
|
|
|
|
// Needed for supporting a decent passkey management interface.
|
|
|
|
// Or check if webauthn.Credential has sufficiently easy to identify data
|
|
|
|
// to use instead of a string mapping
|
2024-08-28 15:20:38 +00:00
|
|
|
PasskeyCredentials []webauthn.Credential `gorm:"serializer:json"` // Webauthn credentials data
|
|
|
|
// Has a RemoteAccountLinks included if remote user
|
|
|
|
RemoteLinks *RemoteAccountLinks
|
|
|
|
}
|
|
|
|
|
|
|
|
// Contains static and cached info about a remote account, mostly links
|
|
|
|
type RemoteAccountLinks struct {
|
|
|
|
// ---- Section: gorm
|
|
|
|
// Sets this struct up as a value that an Account may have
|
|
|
|
gorm.Model
|
|
|
|
AccountID string
|
|
|
|
|
|
|
|
// Just about every link here is optional to accomodate for servers with only minimal accounts
|
|
|
|
// Minimal being handle, ap link and inbox
|
|
|
|
ApLink string
|
|
|
|
ViewLink *string
|
|
|
|
FollowersLink *string
|
|
|
|
FollowingLink *string
|
|
|
|
InboxLink string
|
|
|
|
OutboxLink *string
|
|
|
|
FeaturedLink *string
|
|
|
|
FeaturedTagsLink *string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find an account in the db using a given full handle (@max@example.com)
|
|
|
|
// Returns an account and nil if an account is found, otherwise nil and the error
|
|
|
|
func (s *Storage) FindAccountByFullHandle(handle string) (*Account, error) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
log.Debug().Str("account-handle", handle).Msg("Looking for account by handle")
|
2024-09-12 06:56:57 +00:00
|
|
|
log.Debug().Str("account-handle", handle).Msg("Checking if there's a cache hit")
|
|
|
|
|
|
|
|
// Try and find the account in cache first
|
|
|
|
cacheAccId, err := s.cacheHandleToAccUid(handle)
|
|
|
|
if err == nil {
|
|
|
|
log.Info().Str("account-handle", handle).Msg("Hit account handle in cache")
|
|
|
|
// Then always load via id since unique key access should be faster than string matching
|
|
|
|
return s.FindAccountById(*cacheAccId)
|
|
|
|
} else {
|
|
|
|
if !errors.Is(err, errCacheNotFound) {
|
|
|
|
log.Error().Err(err).Str("account-handle", handle).Msg("Problem while checking cache for account")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Failed to find in cache, go the slow route of hitting the db
|
|
|
|
log.Debug().Str("account-handle", handle).Msg("Didn't hit account in cache, going to db")
|
2024-08-28 15:20:38 +00:00
|
|
|
name, server, err := ap.SplitFullHandle(handle)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn().Err(err).Str("account-handle", handle).Msg("Failed to split up account handle")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
acc := Account{}
|
|
|
|
res := s.db.Where("name = ?", name).Where("server = ?", server).First(&acc)
|
|
|
|
if res.Error != nil {
|
|
|
|
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
|
|
|
log.Info().Str("account-handle", handle).Msg("Account with handle not found")
|
|
|
|
} else {
|
|
|
|
log.Error().Err(err).Str("account-handle", handle).Msg("Failed to get account with handle")
|
|
|
|
}
|
|
|
|
return nil, res.Error
|
|
|
|
}
|
2024-09-12 06:56:57 +00:00
|
|
|
log.Info().Str("account-handle", handle).Msg("Found account, also inserting into cache")
|
|
|
|
if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
|
|
|
|
log.Warn().
|
|
|
|
Err(err).
|
|
|
|
Str("account-handle", handle).
|
|
|
|
Msg("Found account but failed to insert into cache")
|
|
|
|
}
|
|
|
|
if err = s.cache.Set(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), acc.ID); err != nil {
|
|
|
|
log.Warn().
|
|
|
|
Err(err).
|
|
|
|
Str("account-handle", handle).
|
|
|
|
Msg("Failed to store handle to id in cache")
|
|
|
|
}
|
2024-08-28 15:20:38 +00:00
|
|
|
return &acc, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find an account given a specific ID
|
|
|
|
func (s *Storage) FindAccountById(id string) (*Account, error) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
log.Debug().Str("account-id", id).Msg("Looking for account by id")
|
2024-09-12 06:56:57 +00:00
|
|
|
log.Debug().Str("account-id", id).Msg("First trying to hit cache")
|
|
|
|
acc, err := s.cacheAccIdToData(id)
|
|
|
|
if err == nil {
|
|
|
|
log.Info().Str("account-id", id).Msg("Found account in cache")
|
|
|
|
return acc, nil
|
|
|
|
} else if !errors.Is(err, errCacheNotFound) {
|
|
|
|
log.Error().Err(err).Str("account-id", id).Msg("Error while looking for account in cache")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug().Str("account-id", id).Msg("Didn't hit account in cache, checking db")
|
2024-11-05 15:29:01 +00:00
|
|
|
acc = &Account{ID: id}
|
|
|
|
res := s.db.First(acc)
|
2024-08-28 15:20:38 +00:00
|
|
|
if res.Error != nil {
|
|
|
|
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
|
|
|
log.Warn().Str("account-id", id).Msg("Account not found")
|
2024-11-06 15:57:44 +00:00
|
|
|
return nil, ErrEntryNotFound
|
2024-08-28 15:20:38 +00:00
|
|
|
} else {
|
|
|
|
log.Error().Err(res.Error).Str("account-id", id).Msg("Failed to look for account")
|
2024-11-06 15:57:44 +00:00
|
|
|
return nil, res.Error
|
2024-08-28 15:20:38 +00:00
|
|
|
}
|
|
|
|
}
|
2024-09-12 06:56:57 +00:00
|
|
|
log.Info().Str("account-id", id).Msg("Found account in db, also adding to cache")
|
|
|
|
if err = s.cache.Set(cacheUserIdToAccPrefix+id, acc); err != nil {
|
|
|
|
log.Warn().Err(err).Str("account-id", id).Msg("Failed to add account to cache")
|
|
|
|
}
|
|
|
|
return acc, nil
|
|
|
|
}
|
|
|
|
|
2024-10-15 14:16:18 +00:00
|
|
|
func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-10-15 14:16:18 +00:00
|
|
|
log.Debug().Str("account-username", username).Msg("Looking for local account")
|
|
|
|
log.Debug().Str("account-username", username).Msg("Checking cache first")
|
|
|
|
|
|
|
|
// Try and find the account in cache first
|
|
|
|
cacheAccId, err := s.cacheLocalUsernameToAccUid(username)
|
|
|
|
if err == nil {
|
|
|
|
log.Info().Str("account-username", username).Msg("Hit account handle in cache")
|
|
|
|
// Then always load via id since unique key access should be faster than string matching
|
|
|
|
return s.FindAccountById(*cacheAccId)
|
|
|
|
} else {
|
2024-11-05 15:29:01 +00:00
|
|
|
if err != errCacheNotFound {
|
2024-10-15 14:16:18 +00:00
|
|
|
log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Failed to find in cache, go the slow route of hitting the db
|
|
|
|
log.Debug().Str("account-username", username).Msg("Didn't hit account in cache, going to db")
|
|
|
|
|
|
|
|
acc := Account{}
|
|
|
|
res := s.db.Where("username = ?", username).
|
2024-11-05 15:29:01 +00:00
|
|
|
Where("server_id = ?", serverSelf.ID).
|
2024-10-15 14:16:18 +00:00
|
|
|
First(&acc)
|
|
|
|
if res.Error != nil {
|
|
|
|
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
|
|
|
log.Info().
|
|
|
|
Str("account-username", username).
|
|
|
|
Msg("Local account with username not found")
|
|
|
|
} else {
|
|
|
|
log.Error().Err(err).Str("account-username", username).Msg("Failed to get local account with username")
|
|
|
|
}
|
2024-11-05 15:29:01 +00:00
|
|
|
return nil, ErrEntryNotFound
|
2024-10-15 14:16:18 +00:00
|
|
|
}
|
|
|
|
log.Info().Str("account-username", username).Msg("Found account, also inserting into cache")
|
|
|
|
if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
|
|
|
|
log.Warn().
|
|
|
|
Err(err).
|
|
|
|
Str("account-username", username).
|
|
|
|
Msg("Found account but failed to insert into cache")
|
|
|
|
}
|
|
|
|
if err = s.cache.Set(cacheLocalUsernameToIdPrefix+username, acc.ID); err != nil {
|
|
|
|
log.Warn().
|
|
|
|
Err(err).
|
|
|
|
Str("account-username", username).
|
|
|
|
Msg("Failed to store local username to id in cache")
|
|
|
|
}
|
|
|
|
return &acc, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Storage) FindAccountByPasskeyId(pkeyId []byte) (*Account, error) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-10-15 14:16:18 +00:00
|
|
|
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Looking for account")
|
|
|
|
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Checking cache first")
|
|
|
|
|
|
|
|
// Try and find the account in cache first
|
|
|
|
cacheAccId, err := s.cachePkeyIdToAccId(pkeyId)
|
|
|
|
if err == nil {
|
|
|
|
log.Info().Bytes("account-passkey-id", pkeyId).Msg("Hit passkey id in cache")
|
|
|
|
// Then always load via id since unique key access should be faster than string matching
|
|
|
|
return s.FindAccountById(*cacheAccId)
|
|
|
|
} else {
|
|
|
|
if err != errCacheNotFound {
|
|
|
|
log.Error().Err(err).Bytes("account-passkey-id", pkeyId).Msg("Problem while checking cache for account")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Failed to find in cache, go the slow route of hitting the db
|
|
|
|
log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Didn't hit account in cache, going to db")
|
|
|
|
|
|
|
|
acc := Account{}
|
|
|
|
res := s.db.Where("web_authn_id = ?", pkeyId).
|
|
|
|
First(&acc)
|
|
|
|
if res.Error != nil {
|
|
|
|
if res.Error == gorm.ErrRecordNotFound {
|
|
|
|
log.Info().
|
|
|
|
Bytes("account-passkey-id", pkeyId).
|
|
|
|
Msg("Local account with passkey id not found")
|
2024-11-07 09:45:57 +00:00
|
|
|
return nil, ErrEntryNotFound
|
2024-10-15 14:16:18 +00:00
|
|
|
} else {
|
|
|
|
log.Error().Err(res.Error).Bytes("account-passkey-id", pkeyId).Msg("Failed to get local account with passkey id")
|
2024-11-07 09:45:57 +00:00
|
|
|
return nil, res.Error
|
2024-10-15 14:16:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
log.Info().Bytes("account-passkey-id", pkeyId).Msg("Found account, also inserting into cache")
|
|
|
|
// if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
|
|
|
|
// log.Warn().
|
|
|
|
// Err(err).
|
|
|
|
// Bytes("account-passkey-id", pkeyId).
|
|
|
|
// Msg("Found account but failed to insert into cache")
|
|
|
|
// }
|
|
|
|
// if err = s.cache.Set(cachePasskeyIdToAccIdPrefix+string(pkeyId), acc.ID); err != nil {
|
|
|
|
// log.Warn().
|
|
|
|
// Err(err).
|
|
|
|
// Bytes("account-passkey-id", pkeyId).
|
|
|
|
// Msg("Failed to store local username to id in cache")
|
|
|
|
// }
|
|
|
|
return &acc, nil
|
|
|
|
}
|
|
|
|
|
2024-09-12 06:56:57 +00:00
|
|
|
// Update a given account in storage and cache
|
|
|
|
func (s *Storage) UpdateAccount(acc *Account) error {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-09-12 06:56:57 +00:00
|
|
|
// If the account is nil or doesn't have an id, error out
|
|
|
|
if acc == nil || acc.ID == "" {
|
|
|
|
return ErrInvalidData
|
|
|
|
}
|
|
|
|
res := s.db.Save(acc)
|
|
|
|
if res.Error != nil {
|
|
|
|
return res.Error
|
|
|
|
}
|
|
|
|
if err := s.cache.Set(cacheUserIdToAccPrefix+acc.ID, acc); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2024-08-28 15:20:38 +00:00
|
|
|
}
|
|
|
|
|
2024-09-12 14:57:53 +00:00
|
|
|
// Create a new empty account for future use
|
2024-08-28 15:20:38 +00:00
|
|
|
func (s *Storage) NewEmptyAccount() (*Account, error) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
log.Debug().Msg("Creating new empty account")
|
|
|
|
acc := Account{}
|
|
|
|
// Generate the 64 bit id for passkey and webauthn stuff
|
2024-09-27 14:53:22 +00:00
|
|
|
log.Debug().Msg("Creating webauthn id for new account")
|
2024-08-28 15:20:38 +00:00
|
|
|
data := make([]byte, 64)
|
|
|
|
c, err := rand.Read(data)
|
|
|
|
for err != nil || c != len(data) || c < 64 {
|
|
|
|
data = make([]byte, 64)
|
|
|
|
c, err = rand.Read(data)
|
|
|
|
}
|
2024-09-27 14:53:22 +00:00
|
|
|
log.Debug().Msg("Random webauthn id for new account created")
|
|
|
|
acc.ID = uuid.NewString()
|
2024-11-04 15:25:39 +00:00
|
|
|
|
|
|
|
accountRole, err := s.NewEmptyRole(acc.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to generate account role for new account: %w", err)
|
|
|
|
}
|
2024-11-06 15:57:44 +00:00
|
|
|
accountRole.IsUserRole = true
|
|
|
|
if err = s.UpdateRole(accountRole); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to generate account role for new account: %w", err)
|
|
|
|
}
|
2024-11-04 15:25:39 +00:00
|
|
|
|
2024-08-28 15:20:38 +00:00
|
|
|
acc.WebAuthnId = data
|
2024-11-11 08:04:42 +00:00
|
|
|
acc.Relations = []uint{}
|
2024-08-28 15:20:38 +00:00
|
|
|
acc.Tags = []string{}
|
|
|
|
acc.Gender = []string{}
|
|
|
|
acc.CustomFields = []uint{}
|
|
|
|
acc.IdentifiesAs = []Being{}
|
|
|
|
acc.PasskeyCredentials = []webauthn.Credential{}
|
2024-11-04 15:25:39 +00:00
|
|
|
acc.Roles = []string{DefaultUserRole.Name, accountRole.Name}
|
2024-11-06 15:57:44 +00:00
|
|
|
acc.Icon = "placeholder"
|
2024-09-27 14:53:22 +00:00
|
|
|
log.Debug().Any("account", &acc).Msg("Saving new account in db")
|
|
|
|
res := s.db.Save(&acc)
|
2024-08-28 15:20:38 +00:00
|
|
|
if res.Error != nil {
|
|
|
|
log.Error().Err(res.Error).Msg("Failed to safe new account")
|
|
|
|
return nil, res.Error
|
|
|
|
}
|
|
|
|
log.Info().Str("account-id", acc.ID).Msg("Created new account")
|
|
|
|
return &acc, nil
|
|
|
|
}
|
|
|
|
|
2024-09-12 14:57:53 +00:00
|
|
|
// Create a new local account using the given handle
|
|
|
|
// The handle in this case is only the part before the domain (example: @bob@example.com would have a handle of bob)
|
|
|
|
// It also sets up a bunch of values that tend to be obvious for local accounts
|
2024-08-28 15:20:38 +00:00
|
|
|
func (s *Storage) NewLocalAccount(handle string) (*Account, error) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
log.Trace().Caller().Send()
|
|
|
|
log.Debug().Str("account-handle", handle).Msg("Creating new local account")
|
|
|
|
acc, err := s.NewEmptyAccount()
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("Failed to create empty account for use")
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-10-15 14:16:18 +00:00
|
|
|
acc.Username = handle
|
2024-10-26 09:42:51 +00:00
|
|
|
// acc.Server = serverSelf
|
|
|
|
acc.ServerId = serverSelf.ID
|
2024-08-28 15:20:38 +00:00
|
|
|
acc.DisplayName = handle
|
|
|
|
|
2024-10-15 14:16:18 +00:00
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(nil)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("Failed to generate key pair for new local account")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
acc.PrivateKey = privateKey
|
|
|
|
acc.PublicKey = publicKey
|
|
|
|
|
2024-08-28 15:20:38 +00:00
|
|
|
log.Debug().
|
|
|
|
Str("account-handle", handle).
|
|
|
|
Str("account-id", acc.ID).
|
|
|
|
Msg("Saving new local account")
|
|
|
|
res := s.db.Save(acc)
|
|
|
|
if res.Error != nil {
|
|
|
|
log.Error().Err(res.Error).Any("account-full", acc).Msg("Failed to save local account")
|
|
|
|
return nil, res.Error
|
|
|
|
}
|
|
|
|
log.Info().
|
|
|
|
Str("account-handle", handle).
|
|
|
|
Str("account-id", acc.ID).
|
|
|
|
Msg("Created new local account")
|
|
|
|
return acc, nil
|
|
|
|
}
|
|
|
|
|
2024-11-06 15:57:44 +00:00
|
|
|
func (s *Storage) DeleteAccount(accId string) error {
|
2024-11-07 09:45:57 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
|
|
|
return s.db.Delete(&Account{ID: accId}).Error
|
2024-11-06 15:57:44 +00:00
|
|
|
}
|
|
|
|
|
2024-08-28 15:20:38 +00:00
|
|
|
// ---- Section WebAuthn.User
|
|
|
|
// Implements the webauthn.User interface for interaction with passkeys
|
|
|
|
|
|
|
|
func (a *Account) WebAuthnID() []byte {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
return a.WebAuthnId
|
|
|
|
}
|
2024-05-31 09:54:39 +00:00
|
|
|
|
2024-08-28 15:20:38 +00:00
|
|
|
func (u *Account) WebAuthnName() string {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-10-15 14:16:18 +00:00
|
|
|
return u.Username
|
2024-05-31 09:54:39 +00:00
|
|
|
}
|
|
|
|
|
2024-08-28 15:20:38 +00:00
|
|
|
func (u *Account) WebAuthnDisplayName() string {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
return u.DisplayName
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *Account) WebAuthnCredentials() []webauthn.Credential {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
return u.PasskeyCredentials
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *Account) WebAuthnIcon() string {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---- Section passkey.User
|
|
|
|
// Implements the passkey.User interface
|
|
|
|
|
|
|
|
func (u *Account) PutCredential(new webauthn.Credential) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
u.PasskeyCredentials = append(u.PasskeyCredentials, new)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Section passkey.UserStore
|
|
|
|
// Implements the passkey.UserStore interface
|
|
|
|
|
|
|
|
func (s *Storage) GetOrCreateUser(userID string) passkey.User {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
log.Debug().
|
|
|
|
Str("account-handle", userID).
|
|
|
|
Msg("Looking for or creating account for passkey stuff")
|
|
|
|
acc := &Account{}
|
2024-10-26 09:42:51 +00:00
|
|
|
res := s.db.Where(Account{Username: userID, ServerId: serverSelf.ID}).
|
2024-08-28 15:20:38 +00:00
|
|
|
First(acc)
|
|
|
|
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
|
|
|
log.Debug().Str("account-handle", userID)
|
|
|
|
var err error
|
|
|
|
acc, err = s.NewLocalAccount(userID)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().
|
|
|
|
Err(err).
|
|
|
|
Str("account-handle", userID).
|
|
|
|
Msg("Failed to create new account for webauthn request")
|
|
|
|
return nil
|
|
|
|
}
|
2024-05-31 09:54:39 +00:00
|
|
|
}
|
2024-08-28 15:20:38 +00:00
|
|
|
return acc
|
2024-05-31 09:54:39 +00:00
|
|
|
}
|
2024-05-31 15:21:29 +00:00
|
|
|
|
2024-08-28 15:20:38 +00:00
|
|
|
func (s *Storage) GetUserByWebAuthnId(id []byte) passkey.User {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
log.Debug().Bytes("webauthn-id", id).Msg("Looking for account with webauthn id")
|
|
|
|
acc := Account{}
|
|
|
|
res := s.db.Where(Account{WebAuthnId: id}).First(&acc)
|
|
|
|
if res.Error != nil {
|
|
|
|
log.Error().
|
|
|
|
Err(res.Error).
|
|
|
|
Bytes("webauthn-id", id).
|
|
|
|
Msg("Failed to find user with webauthn ID")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
log.Info().Msg("Found account with given webauthn id")
|
|
|
|
return &acc
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Storage) SaveUser(rawUser passkey.User) {
|
2024-11-06 15:57:44 +00:00
|
|
|
defer util.Untrace(util.Trace(&log.Logger))
|
2024-08-28 15:20:38 +00:00
|
|
|
user, ok := rawUser.(*Account)
|
|
|
|
if !ok {
|
|
|
|
log.Error().Any("raw-user", rawUser).Msg("Failed to cast raw user to db account")
|
|
|
|
}
|
|
|
|
s.db.Save(user)
|
2024-05-31 15:21:29 +00:00
|
|
|
}
|