Sync
This commit is contained in:
parent
94197780e1
commit
2977f09245
32 changed files with 763 additions and 936 deletions
7
storage/errors.go
Normal file
7
storage/errors.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package storage
|
||||
|
||||
type NotImplementedError struct{}
|
||||
|
||||
func (n NotImplementedError) Error() string {
|
||||
return "Not implemented yet"
|
||||
}
|
|
@ -7,9 +7,9 @@ import (
|
|||
)
|
||||
|
||||
type MediaFile struct {
|
||||
ID string `gorm:"primarykey"` // The unique ID of this media file
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
ID string `gorm:"primarykey"` // The unique ID of this media file
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
// 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
|
||||
|
@ -25,12 +25,3 @@ type MediaFile struct {
|
|||
// Caching user and server icons locally however should reduce burden on remote servers by quite a bit though
|
||||
LocallyCached bool
|
||||
}
|
||||
|
||||
// Placeholder media file. Acts as placeholder for media file fields that have not been initialised yet but need a value
|
||||
var placeholderMediaFile = &MediaFile{
|
||||
ID: "placeholder",
|
||||
Remote: false,
|
||||
Link: "placeholder", // TODO: Replace this with a file path to a staticly included image
|
||||
Type: "image/png",
|
||||
LocallyCached: true,
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import (
|
|||
)
|
||||
|
||||
type Note struct {
|
||||
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
// 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
|
||||
|
@ -29,19 +29,3 @@ type Note struct {
|
|||
OriginServer string // Url of the origin server. Also the primary key for those
|
||||
Tags []string `gorm:"serializer:json"` // Hashtags
|
||||
}
|
||||
|
||||
var placeholderNote = &Note{
|
||||
ID: "placeholder",
|
||||
Creator: "placeholder",
|
||||
Remote: false,
|
||||
RawContent: "placeholder",
|
||||
ContentWarning: nil,
|
||||
Attachments: []string{},
|
||||
Emotes: []string{},
|
||||
RepliesTo: nil,
|
||||
Quotes: nil,
|
||||
Target: NOTE_TARGET_HOME,
|
||||
Pings: []string{},
|
||||
OriginServer: "placeholder",
|
||||
Tags: []string{},
|
||||
}
|
||||
|
|
45
storage/passkeySessions.go
Normal file
45
storage/passkeySessions.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PasskeySession struct {
|
||||
ID string `gorm:"primarykey"`
|
||||
Data webauthn.SessionData `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// ---- Section SessionStore
|
||||
|
||||
func (s *Storage) GenSessionID() (string, error) {
|
||||
x := uuid.NewString()
|
||||
log.Debug().Str("session-id", x).Msg("Generated new passkey session id")
|
||||
return x, nil
|
||||
}
|
||||
|
||||
func (s *Storage) GetSession(sessionId string) (*webauthn.SessionData, bool) {
|
||||
log.Debug().Str("id", sessionId).Msg("Looking for passkey session")
|
||||
session := PasskeySession{}
|
||||
res := s.db.Where("id = ?", sessionId).First(&session)
|
||||
if res.Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
log.Debug().Str("id", sessionId).Any("webauthn-data", &session).Msg("Found passkey session")
|
||||
return &session.Data, true
|
||||
}
|
||||
|
||||
func (s *Storage) SaveSession(token string, data *webauthn.SessionData) {
|
||||
log.Debug().Str("id", token).Any("webauthn-data", data).Msg("Saving passkey session")
|
||||
session := PasskeySession{
|
||||
ID: token,
|
||||
Data: *data,
|
||||
}
|
||||
s.db.Save(&session)
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteSession(token string) {
|
||||
log.Debug().Str("id", token).Msg("Deleting passkey session (if one exists)")
|
||||
s.db.Delete(&PasskeySession{ID: token})
|
||||
}
|
|
@ -7,9 +7,9 @@ import (
|
|||
)
|
||||
|
||||
type RemoteServer struct {
|
||||
ID string `gorm:"primarykey"` // ID is also server url
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
ID string `gorm:"primarykey"` // ID is also server url
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this entry was last updated
|
||||
// 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
|
||||
|
@ -19,11 +19,3 @@ type RemoteServer struct {
|
|||
Icon string // ID of a media file
|
||||
IsSelf bool // Whether this server is yours truly
|
||||
}
|
||||
|
||||
var placeholderServer = &RemoteServer{
|
||||
ID: "placeholder",
|
||||
ServerType: REMOTE_SERVER_LINSTROM,
|
||||
Name: "placeholder",
|
||||
Icon: "placeholder",
|
||||
IsSelf: false,
|
||||
}
|
||||
|
|
5
storage/remoteUser.go
Normal file
5
storage/remoteUser.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package storage
|
||||
|
||||
func (s *Storage) NewRemoteUser(fullHandle string) (*Account, error) {
|
||||
return nil, nil
|
||||
}
|
8
storage/roles.go
Normal file
8
storage/roles.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package storage
|
||||
|
||||
type Role struct {
|
||||
// Name of the role
|
||||
Name string
|
||||
// If set, counts as all permissions being set and all restrictions being disabled
|
||||
FullAdmin bool
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
@ -35,34 +33,16 @@ func storageFromEmptyDb(db *gorm.DB) (*Storage, error) {
|
|||
// AutoMigrate ensures the db is in a state where all the structs given here
|
||||
// have their own tables and relations setup. It also updates tables if necessary
|
||||
db.AutoMigrate(
|
||||
placeholderMediaFile,
|
||||
placeholderUser(),
|
||||
placeholderNote,
|
||||
placeholderServer,
|
||||
MediaFile{},
|
||||
Account{},
|
||||
RemoteServer{},
|
||||
Note{},
|
||||
Role{},
|
||||
PasskeySession{},
|
||||
)
|
||||
// Afterwards add the placeholder entries for each table.
|
||||
// FirstOrCreate either creates a new entry or retrieves the first matching one
|
||||
// We only care about the creation if there is none yet, so no need to carry the result over
|
||||
if res := db.FirstOrCreate(placeholderMediaFile); res.Error != nil {
|
||||
return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error)
|
||||
}
|
||||
if res := db.FirstOrCreate(placeholderUser()); res.Error != nil {
|
||||
return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error)
|
||||
}
|
||||
if res := db.FirstOrCreate(placeholderNote); res.Error != nil {
|
||||
return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error)
|
||||
}
|
||||
if res := db.FirstOrCreate(placeholderServer); res.Error != nil {
|
||||
return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error)
|
||||
}
|
||||
|
||||
// And finally, build the actual storage struct
|
||||
return &Storage{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: Placeholder. Update to proper implementation later. Including signature
|
||||
func (s *Storage) FindLocalAccount(handle string) (string, error) {
|
||||
return handle, nil
|
||||
}
|
||||
|
|
278
storage/user.go
278
storage/user.go
|
@ -1,21 +1,30 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mstarongithub/passkey"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gitlab.com/mstarongitlab/linstrom/ap"
|
||||
"gitlab.com/mstarongitlab/linstrom/config"
|
||||
"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
|
||||
type User struct {
|
||||
ID string `gorm:"primarykey"` // ID is a uuid for this account
|
||||
Handle string // Handle is the full handle, eg @max@example.com
|
||||
CreatedAt time.Time // When this entry was created
|
||||
UpdatedAt time.Time // When this account was last updated. Will also be used for refreshing remote accounts
|
||||
type Account struct {
|
||||
ID string `gorm:"primarykey"` // ID is a uuid for this account
|
||||
// Handle of the user (eg "max" if the full username is @max@example.com)
|
||||
// Assume unchangable (once set by a user) to be kind to other implementations
|
||||
// Would be an easy avenue to fuck with them though
|
||||
Handle string
|
||||
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
|
||||
// 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
|
||||
|
@ -39,57 +48,230 @@ type User struct {
|
|||
RestrictedFollow bool
|
||||
// List of things the owner identifies as
|
||||
// Example [cat human robot] means that the owner probably identifies as
|
||||
// a cyborg-catgirl/boy/human
|
||||
IdentifiesAs []Being
|
||||
// a cyborg-catgirl/boy/human or a cathuman shaped robot, refer to Gender for pronouns
|
||||
IdentifiesAs []Being `gorm:"serializer:json"`
|
||||
// List of pronouns the owner identifies with
|
||||
// An unordered list since the owner can freely set it
|
||||
// Examples: [she her], [it they its them]
|
||||
Gender []string
|
||||
Gender []string `gorm:"serializer:json"`
|
||||
// The roles assocciated with an account
|
||||
Roles []string `gorm:"serializer:json"`
|
||||
|
||||
// --- And internal account stuff ---
|
||||
// Still public fields since they wouldn't be able to be stored in the db otherwise
|
||||
|
||||
PasswordHash []byte // Hash of the user's password
|
||||
TotpToken []byte // Token for totp verification
|
||||
// All the registered passkeys, name of passkey to credentials
|
||||
// Could this be exported to another table? Yes
|
||||
// Would it make sense? Probably not
|
||||
// Will just take the performance hit of json conversion
|
||||
// Access should be rare enough anyway
|
||||
Passkeys map[string]webauthn.Credential `gorm:"serializer:json"`
|
||||
PrivateKeyPem *string // The private key of the account. Nil if remote user
|
||||
PrivateKeyPem *string // The private key of the account. Nil if remote user
|
||||
WebAuthnId []byte // The unique and random ID of this account used for passkey authentication
|
||||
// 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
|
||||
Verified bool
|
||||
PasskeyCredentials []webauthn.Credential `gorm:"serializer:json"` // Webauthn credentials data
|
||||
// Has a RemoteAccountLinks included if remote user
|
||||
RemoteLinks *RemoteAccountLinks
|
||||
}
|
||||
|
||||
func NewEmptyUser() *User {
|
||||
return &User{
|
||||
ID: uuid.NewString(),
|
||||
Handle: "placeholder",
|
||||
Remote: false,
|
||||
Server: "placeholder",
|
||||
DisplayName: "placeholder",
|
||||
CustomFields: []uint{},
|
||||
Description: "placeholder",
|
||||
Tags: []string{},
|
||||
IsBot: true,
|
||||
Follows: []string{},
|
||||
Followers: []string{},
|
||||
Icon: "placeholder",
|
||||
Background: "placeholder",
|
||||
Banner: "placeholder",
|
||||
Indexable: false,
|
||||
PublicKeyPem: nil,
|
||||
RestrictedFollow: false,
|
||||
IdentifiesAs: []Being{BEING_ROBOT},
|
||||
Gender: []string{"it", "its"},
|
||||
PasswordHash: []byte("placeholder"),
|
||||
TotpToken: []byte("placeholder"),
|
||||
Passkeys: map[string]webauthn.Credential{},
|
||||
PrivateKeyPem: nil,
|
||||
// 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) {
|
||||
log.Trace().Caller().Send()
|
||||
log.Debug().Str("account-handle", handle).Msg("Looking for account by handle")
|
||||
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
|
||||
}
|
||||
log.Info().Str("account-handle", handle).Msg("Found account")
|
||||
return &acc, nil
|
||||
}
|
||||
|
||||
func placeholderUser() *User {
|
||||
tmp := NewEmptyUser()
|
||||
tmp.ID = "placeholder"
|
||||
return tmp
|
||||
// Find an account given a specific ID
|
||||
func (s *Storage) FindAccountById(id string) (*Account, error) {
|
||||
log.Trace().Caller().Send()
|
||||
log.Debug().Str("account-id", id).Msg("Looking for account by id")
|
||||
acc := Account{}
|
||||
res := s.db.First(&acc, id)
|
||||
if res.Error != nil {
|
||||
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
|
||||
log.Warn().Str("account-id", id).Msg("Account not found")
|
||||
} else {
|
||||
log.Error().Err(res.Error).Str("account-id", id).Msg("Failed to look for account")
|
||||
}
|
||||
return nil, res.Error
|
||||
}
|
||||
log.Info().Str("account-id", id).Msg("Found account")
|
||||
return &acc, nil
|
||||
}
|
||||
|
||||
func (s *Storage) NewEmptyAccount() (*Account, error) {
|
||||
log.Trace().Caller().Send()
|
||||
log.Debug().Msg("Creating new empty account")
|
||||
acc := Account{}
|
||||
// Generate the 64 bit id for passkey and webauthn stuff
|
||||
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)
|
||||
}
|
||||
acc.WebAuthnId = data
|
||||
acc.Followers = []string{}
|
||||
acc.Tags = []string{}
|
||||
acc.Follows = []string{}
|
||||
acc.Gender = []string{}
|
||||
acc.CustomFields = []uint{}
|
||||
acc.IdentifiesAs = []Being{}
|
||||
acc.PasskeyCredentials = []webauthn.Credential{}
|
||||
res := s.db.Save(acc)
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Storage) NewLocalAccount(handle string) (*Account, error) {
|
||||
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
|
||||
}
|
||||
acc.Handle = handle
|
||||
acc.Server = config.GlobalConfig.General.GetFullDomain()
|
||||
acc.Remote = false
|
||||
acc.DisplayName = handle
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ---- Section WebAuthn.User
|
||||
// Implements the webauthn.User interface for interaction with passkeys
|
||||
|
||||
func (a *Account) WebAuthnID() []byte {
|
||||
log.Trace().Caller().Send()
|
||||
return a.WebAuthnId
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnName() string {
|
||||
log.Trace().Caller().Send()
|
||||
return u.Handle
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnDisplayName() string {
|
||||
log.Trace().Caller().Send()
|
||||
return u.DisplayName
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnCredentials() []webauthn.Credential {
|
||||
log.Trace().Caller().Send()
|
||||
return u.PasskeyCredentials
|
||||
}
|
||||
|
||||
func (u *Account) WebAuthnIcon() string {
|
||||
log.Trace().Caller().Send()
|
||||
return ""
|
||||
}
|
||||
|
||||
// ---- Section passkey.User
|
||||
// Implements the passkey.User interface
|
||||
|
||||
func (u *Account) PutCredential(new webauthn.Credential) {
|
||||
log.Trace().Caller().Send()
|
||||
u.PasskeyCredentials = append(u.PasskeyCredentials, new)
|
||||
}
|
||||
|
||||
// Section passkey.UserStore
|
||||
// Implements the passkey.UserStore interface
|
||||
|
||||
func (s *Storage) GetOrCreateUser(userID string) passkey.User {
|
||||
log.Trace().Caller().Send()
|
||||
log.Debug().
|
||||
Str("account-handle", userID).
|
||||
Msg("Looking for or creating account for passkey stuff")
|
||||
acc := &Account{}
|
||||
res := s.db.Where(Account{Handle: userID, Server: config.GlobalConfig.General.GetFullDomain()}).
|
||||
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
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
func (s *Storage) GetUserByWebAuthnId(id []byte) passkey.User {
|
||||
log.Trace().Caller().Send()
|
||||
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) {
|
||||
log.Trace().Caller().Send()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// What kind of being a user identifies as
|
||||
type Being string
|
||||
|
||||
|
@ -16,16 +11,3 @@ const (
|
|||
BEING_ROBOT = Being("robot")
|
||||
BEING_DOLL = Being("doll")
|
||||
)
|
||||
|
||||
func (r *Being) Value() (driver.Value, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *Being) Scan(raw any) error {
|
||||
if v, ok := raw.(string); ok {
|
||||
*r = Being(v)
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("value not a string")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue