This commit is contained in:
Melody 2024-08-28 17:20:38 +02:00
parent 94197780e1
commit 2977f09245
32 changed files with 763 additions and 936 deletions

7
storage/errors.go Normal file
View file

@ -0,0 +1,7 @@
package storage
type NotImplementedError struct{}
func (n NotImplementedError) Error() string {
return "Not implemented yet"
}

View file

@ -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,
}

View file

@ -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{},
}

View 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})
}

View file

@ -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
View file

@ -0,0 +1,5 @@
package storage
func (s *Storage) NewRemoteUser(fullHandle string) (*Account, error) {
return nil, nil
}

8
storage/roles.go Normal file
View 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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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")
}
}