Files
worker/app/models/user.go
Gleb Tv 2c884c5612
Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
refactor: adopt worker module path
2026-07-13 17:56:12 +03:00

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
}