Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
234 строки
6.6 KiB
Go
234 строки
6.6 KiB
Go
package models
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/lib/pq"
|
|
"github.com/pkg/errors"
|
|
"gorm.io/datatypes"
|
|
|
|
"rocketgit.ru/rsmon/worker/app/models/authidentity"
|
|
"rocketgit.ru/rsmon/worker/app/models/concerns"
|
|
)
|
|
|
|
// User represents a platform user.
|
|
//
|
|
// A User is a global identity that may belong to many tenants via the
|
|
// Access join table (see Access). Contacts created by or assigned to a
|
|
// User carry that UserID; admins reach them via the cross-tenant
|
|
// /admin/users page, while per-account management is via
|
|
// /settings/users (which shows Accesses preloaded with User + Invite).
|
|
//
|
|
// Authentication state (password, confirmation, lock, recover) lives
|
|
// here, not on Access, because those attributes are account-independent.
|
|
type User struct {
|
|
// concerns.Model
|
|
ID int64 `gorm:"primarykey" json:"id"`
|
|
|
|
Email *string `gorm:"uniqueIndex;size:255" json:"email" validate:"required"`
|
|
Name string `json:"name"`
|
|
Enabled bool `gorm:"not null;default:true" json:"-"`
|
|
// Operator grants platform-wide operational access. Account ownership alone
|
|
// must never grant cross-tenant task inspection or replay.
|
|
Operator bool `gorm:"not null;default:false" json:"operator"`
|
|
Timezone string `json:"timezone"`
|
|
Language string `gorm:"default:ru" json:"language"`
|
|
// Settings holds small per-user UI preferences. It intentionally stays
|
|
// separate from account settings because the sidebar is a personal view.
|
|
Settings datatypes.JSON `gorm:"type:jsonb;not null;default:'{}'::jsonb" json:"settings"`
|
|
Phone string `gorm:"index,size:255" json:"phone"`
|
|
TelegramID *int64 `gorm:"uniqueIndex" json:"telegram_id"`
|
|
TelegramUsername string `gorm:"size:255" json:"telegram_username"`
|
|
|
|
Accesses []Access `json:"-"`
|
|
Contacts []Contact `json:"-"`
|
|
|
|
LastActiveAt *time.Time `json:"last_active_at"`
|
|
LastActiveIP *string `json:"last_active_ip"`
|
|
|
|
EncryptedPassword *string `json:"-"`
|
|
PasswordSetAt *time.Time `json:"-"`
|
|
|
|
// Confirm
|
|
ConfirmationToken *string `json:"-"`
|
|
Confirmed bool `json:"confirmed"`
|
|
|
|
// Lock
|
|
AttemptCount int `json:"-"`
|
|
LastAttempt *time.Time `json:"-"`
|
|
LockedAt *time.Time `json:"-"`
|
|
|
|
// Recover
|
|
RecoverToken *string `json:"-"`
|
|
RecoverTokenAt *time.Time `json:"-"`
|
|
|
|
RememberTokens pq.StringArray `gorm:"index;type:varchar(100)[]" json:"-"`
|
|
|
|
// DeletionRequestedAt is set when the user requests account deletion.
|
|
// During the 7-day grace period the user can cancel the deletion
|
|
// (clearing this field). After 7 days the user and all related data
|
|
// are hard-deleted by a scheduled job.
|
|
DeletionRequestedAt *time.Time `json:"deletion_requested_at"`
|
|
|
|
concerns.Timestamped `json:"-"`
|
|
}
|
|
|
|
// DeletionPending returns true if the user has requested deletion and is
|
|
// still within the 7-day grace period.
|
|
func (u *User) DeletionPending() bool {
|
|
return u.DeletionRequestedAt != nil
|
|
}
|
|
|
|
// GetLabel returns info label for user.
|
|
func (u *User) GetLabel() string {
|
|
return u.DisplayName()
|
|
}
|
|
|
|
// DisplayName implements qor.CurrentUser for admin.
|
|
func (u *User) DisplayName() string {
|
|
if u.Email != nil {
|
|
return u.Name + " " + *u.Email
|
|
}
|
|
return u.Name
|
|
}
|
|
|
|
// AfterSocialLogin is a callback after social login.
|
|
func (u *User) AfterSocialLogin(inviteID int64) (*User, error) {
|
|
oldUser := User{}
|
|
DB().Where("email = ?", u.Email).Where("id != ?", u.ID).First(&oldUser)
|
|
if oldUser.ID > 0 {
|
|
log.Println("new user", u.ID, "has same email", u.Email, "as old user", oldUser.ID, "so replacing")
|
|
err := DB().Model(&authidentity.AuthIdentity{}).Where("user_id = ?", u.ID).Updates(
|
|
authidentity.Basic{
|
|
UserID: &oldUser.ID,
|
|
},
|
|
).Error
|
|
|
|
return &oldUser, err
|
|
}
|
|
err := u.AfterRegister(inviteID)
|
|
return u, err
|
|
}
|
|
|
|
// AfterInvite is a callback after invite acceptance.
|
|
func (u *User) AfterInvite(invite *Invite) error {
|
|
invite.InviteeID = &u.ID
|
|
|
|
err := DB().Table("accesses").Where("invite_id = ?", invite.ID).Updates(map[string]interface{}{"user_id": u.ID}).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "invite: failed to add accesses to invited user")
|
|
}
|
|
|
|
err = DB().Model(&authidentity.AuthIdentity{}).Where("provider = ? AND user_id = ?", "password", u.ID).Updates(map[string]interface{}{
|
|
"confirmed_at": time.Now(),
|
|
}).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "invite: failed set user as confirmed")
|
|
}
|
|
|
|
eml := invite.Email
|
|
u.Email = &eml
|
|
if invite.Name != "" {
|
|
u.Name = invite.Name
|
|
}
|
|
err = DB().Save(&u).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "invite: failed to save user")
|
|
}
|
|
|
|
if invite.Name == "" {
|
|
log.Println("set name", u.Name)
|
|
invite.Name = u.Name
|
|
}
|
|
|
|
invite.State = stateOK
|
|
|
|
err = DB().Save(&invite).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "invite: failed to save invite")
|
|
}
|
|
|
|
invite.Invitee = u
|
|
|
|
return nil
|
|
}
|
|
|
|
// AfterRegister is a callback after registration.
|
|
func (u *User) AfterRegister(inviteID int64) error {
|
|
var err error
|
|
|
|
if inviteID > 0 {
|
|
invite := Invite{}
|
|
err := DB().First(&invite, inviteID).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "invite: failed to find invite")
|
|
}
|
|
|
|
err = u.AfterInvite(&invite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = CreateAccountForUser("", u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AfterLogin is a callback after login.
|
|
func (u *User) AfterLogin(inviteID int64) error {
|
|
var err error
|
|
if inviteID > 0 {
|
|
invite := Invite{}
|
|
err = DB().First(&invite, inviteID).Error
|
|
if err == nil {
|
|
invite.InviteeID = &u.ID
|
|
invite.State = stateOK
|
|
err = DB().Save(&invite).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to save invite")
|
|
}
|
|
err = DB().Table("accesses").Where("invite_id = ?", invite.ID).Updates(map[string]interface{}{"user_id": u.ID}).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to add accesses to invited user")
|
|
}
|
|
|
|
err = DB().Model(&authidentity.AuthIdentity{}).Where("provider = ? AND user_id = ?", "password", u.ID).Updates(map[string]interface{}{ //nolint:lll
|
|
"confirmed_at": time.Now(),
|
|
}).Error
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed set user as confirmed")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Gravatar returns the Gravatar URL for the user.
|
|
func (u *User) Gravatar(size int) string {
|
|
if u.Email == nil {
|
|
return ""
|
|
}
|
|
|
|
hash := md5.Sum([]byte(*u.Email))
|
|
return fmt.Sprintf("https://www.gravatar.com/avatar/%x?s=%d&d=blank", hash, size)
|
|
}
|
|
|
|
// AsJSON returns a JSON representation of user.
|
|
func (u User) AsJSON() map[string]interface{} { //nolint:gocritic // hugeParam: accepted for interface compatibility
|
|
r := map[string]interface{}{
|
|
"id": u.ID,
|
|
"email": u.Email,
|
|
"avatar": u.Gravatar(32),
|
|
"deletion_requested_at": u.DeletionRequestedAt,
|
|
}
|
|
|
|
return r
|
|
}
|