mirror of
https://github.com/pocketbase/pocketbase.git
synced 2024-12-14 22:16:23 +02:00
282 lines
7.2 KiB
Go
282 lines
7.2 KiB
Go
package daos
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/models"
|
|
"github.com/pocketbase/pocketbase/models/schema"
|
|
"github.com/pocketbase/pocketbase/tools/list"
|
|
"github.com/pocketbase/pocketbase/tools/security"
|
|
)
|
|
|
|
// UserQuery returns a new User model select query.
|
|
func (dao *Dao) UserQuery() *dbx.SelectQuery {
|
|
return dao.ModelQuery(&models.User{})
|
|
}
|
|
|
|
// LoadProfile loads the profile record associated to the provided user.
|
|
func (dao *Dao) LoadProfile(user *models.User) error {
|
|
collection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
profile, err := dao.FindFirstRecordByData(collection, models.ProfileCollectionUserFieldName, user.Id)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
user.Profile = profile
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadProfiles loads the profile records associated to the provied users list.
|
|
func (dao *Dao) LoadProfiles(users []*models.User) error {
|
|
collection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// extract user ids
|
|
ids := make([]string, len(users))
|
|
usersMap := map[string]*models.User{}
|
|
for i, user := range users {
|
|
ids[i] = user.Id
|
|
usersMap[user.Id] = user
|
|
}
|
|
|
|
profiles, err := dao.FindRecordsByExpr(collection, dbx.HashExp{
|
|
models.ProfileCollectionUserFieldName: list.ToInterfaceSlice(ids),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// populate each user.Profile member
|
|
for _, profile := range profiles {
|
|
userId := profile.GetStringDataValue(models.ProfileCollectionUserFieldName)
|
|
user, ok := usersMap[userId]
|
|
if !ok {
|
|
continue
|
|
}
|
|
user.Profile = profile
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FindUserById finds a single User model by its id.
|
|
//
|
|
// This method also auto loads the related user profile record
|
|
// into the found model.
|
|
func (dao *Dao) FindUserById(id string) (*models.User, error) {
|
|
model := &models.User{}
|
|
|
|
err := dao.UserQuery().
|
|
AndWhere(dbx.HashExp{"id": id}).
|
|
Limit(1).
|
|
One(model)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// try to load the user profile (if exist)
|
|
if err := dao.LoadProfile(model); err != nil {
|
|
log.Println(err)
|
|
}
|
|
|
|
return model, nil
|
|
}
|
|
|
|
// FindUserByEmail finds a single User model by its email address.
|
|
//
|
|
// This method also auto loads the related user profile record
|
|
// into the found model.
|
|
func (dao *Dao) FindUserByEmail(email string) (*models.User, error) {
|
|
model := &models.User{}
|
|
|
|
err := dao.UserQuery().
|
|
AndWhere(dbx.HashExp{"email": email}).
|
|
Limit(1).
|
|
One(model)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// try to load the user profile (if exist)
|
|
if err := dao.LoadProfile(model); err != nil {
|
|
log.Println(err)
|
|
}
|
|
|
|
return model, nil
|
|
}
|
|
|
|
// FindUserByToken finds the user associated with the provided JWT token.
|
|
// Returns an error if the JWT token is invalid or expired.
|
|
//
|
|
// This method also auto loads the related user profile record
|
|
// into the found model.
|
|
func (dao *Dao) FindUserByToken(token string, baseTokenKey string) (*models.User, error) {
|
|
unverifiedClaims, err := security.ParseUnverifiedJWT(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// check required claims
|
|
id, _ := unverifiedClaims["id"].(string)
|
|
if id == "" {
|
|
return nil, errors.New("Missing or invalid token claims.")
|
|
}
|
|
|
|
user, err := dao.FindUserById(id)
|
|
if err != nil || user == nil {
|
|
return nil, err
|
|
}
|
|
|
|
verificationKey := user.TokenKey + baseTokenKey
|
|
|
|
// verify token signature
|
|
if _, err := security.ParseJWT(token, verificationKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// IsUserEmailUnique checks if the provided email address is not
|
|
// already in use by other users.
|
|
func (dao *Dao) IsUserEmailUnique(email string, excludeId string) bool {
|
|
if email == "" {
|
|
return false
|
|
}
|
|
|
|
var exists bool
|
|
err := dao.UserQuery().
|
|
Select("count(*)").
|
|
AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})).
|
|
AndWhere(dbx.HashExp{"email": email}).
|
|
Limit(1).
|
|
Row(&exists)
|
|
|
|
return err == nil && !exists
|
|
}
|
|
|
|
// DeleteUser deletes the provided User model.
|
|
//
|
|
// This method will also cascade the delete operation to all
|
|
// Record models that references the provided User model
|
|
// (delete or set to NULL, depending on the related user shema field settings).
|
|
//
|
|
// The delete operation may fail if the user is part of a required
|
|
// reference in another Record model (aka. cannot be deleted or set to NULL).
|
|
func (dao *Dao) DeleteUser(user *models.User) error {
|
|
// fetch related records
|
|
// note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction
|
|
relatedRecords, err := dao.FindUserRelatedRecords(user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return dao.RunInTransaction(func(txDao *Dao) error {
|
|
// check if related records has to be deleted (if `CascadeDelete` is set)
|
|
// OR
|
|
// just unset the user related fields (if they are not required)
|
|
// -----------------------------------------------------------
|
|
recordsLoop:
|
|
for _, record := range relatedRecords {
|
|
var needSave bool
|
|
|
|
for _, field := range record.Collection().Schema.Fields() {
|
|
if field.Type != schema.FieldTypeUser {
|
|
continue // not a user field
|
|
}
|
|
|
|
ids := record.GetStringSliceDataValue(field.Name)
|
|
|
|
// unset the user id
|
|
for i := len(ids) - 1; i >= 0; i-- {
|
|
if ids[i] == user.Id {
|
|
ids = append(ids[:i], ids[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
options, _ := field.Options.(*schema.UserOptions)
|
|
|
|
// cascade delete
|
|
// (only if there are no other user references in case of multiple select)
|
|
if options.CascadeDelete && len(ids) == 0 {
|
|
if err := txDao.DeleteRecord(record); err != nil {
|
|
return err
|
|
}
|
|
// no need to further iterate the user fields (the record is deleted)
|
|
continue recordsLoop
|
|
}
|
|
|
|
if field.Required && len(ids) == 0 {
|
|
return fmt.Errorf("Failed delete the user because a record exist with required user reference to the current model (%q, %q).", record.Id, record.Collection().Name)
|
|
}
|
|
|
|
// apply the reference changes
|
|
record.SetDataValue(field.Name, field.PrepareValue(ids))
|
|
needSave = true
|
|
}
|
|
|
|
if needSave {
|
|
if err := txDao.SaveRecord(record); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
// -----------------------------------------------------------
|
|
|
|
return txDao.Delete(user)
|
|
})
|
|
}
|
|
|
|
// SaveUser upserts the provided User model.
|
|
//
|
|
// An empty profile record will be created if the user
|
|
// doesn't have a profile record set yet.
|
|
func (dao *Dao) SaveUser(user *models.User) error {
|
|
profileCollection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// fetch the related user profile record (if exist)
|
|
var userProfile *models.Record
|
|
if user.HasId() {
|
|
userProfile, _ = dao.FindFirstRecordByData(
|
|
profileCollection,
|
|
models.ProfileCollectionUserFieldName,
|
|
user.Id,
|
|
)
|
|
}
|
|
|
|
return dao.RunInTransaction(func(txDao *Dao) error {
|
|
if err := txDao.Save(user); err != nil {
|
|
return err
|
|
}
|
|
|
|
// create default/empty profile record if doesn't exist
|
|
if userProfile == nil {
|
|
userProfile = models.NewRecord(profileCollection)
|
|
userProfile.SetDataValue(models.ProfileCollectionUserFieldName, user.Id)
|
|
if err := txDao.Save(userProfile); err != nil {
|
|
return err
|
|
}
|
|
user.Profile = userProfile
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|