feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
233
app/models/user.go
Обычный файл
233
app/models/user.go
Обычный файл
@@ -0,0 +1,233 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/datatypes"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models/authidentity"
|
||||
"rsgit.ru/rsmon/rsmon/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
|
||||
}
|
||||
Ссылка в новой задаче
Block a user