github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/modules/account/account.go (about)

     1  package account
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  
     7  	"github.com/ethereum/go-ethereum/crypto"
     8  
     9  	confid "github.com/machinefi/w3bstream/pkg/depends/conf/id"
    10  	"github.com/machinefi/w3bstream/pkg/depends/kit/logr"
    11  	"github.com/machinefi/w3bstream/pkg/depends/kit/sqlx"
    12  	"github.com/machinefi/w3bstream/pkg/depends/kit/statusx"
    13  	"github.com/machinefi/w3bstream/pkg/depends/util"
    14  	"github.com/machinefi/w3bstream/pkg/enums"
    15  	"github.com/machinefi/w3bstream/pkg/errors/status"
    16  	"github.com/machinefi/w3bstream/pkg/models"
    17  	"github.com/machinefi/w3bstream/pkg/modules/operator"
    18  	"github.com/machinefi/w3bstream/pkg/types"
    19  )
    20  
    21  type CreateAccountByUsernameReq struct {
    22  	Username  string              `json:"username"`
    23  	Role      enums.AccountRole   `json:"role"`
    24  	AvatarURL string              `json:"avatarURL,omitempty" validate:"@url"`
    25  	Password  string              `json:"-"`
    26  	Source    enums.AccountSource `json:"-"`
    27  }
    28  
    29  type CreateAccountByUsernameRsp struct {
    30  	*models.Account
    31  	Password string `json:"password"`
    32  }
    33  
    34  func CreateAccountByUsername(ctx context.Context, r *CreateAccountByUsernameReq) (*CreateAccountByUsernameRsp, error) {
    35  	d := types.MustMgrDBExecutorFromContext(ctx)
    36  	g := confid.MustSFIDGeneratorFromContext(ctx)
    37  
    38  	rel := &models.RelAccount{AccountID: g.MustGenSFID()}
    39  	if r.Source == 0 {
    40  		r.Source = enums.ACCOUNT_SOURCE__SUBMIT
    41  	}
    42  	acc := (*models.Account)(nil)
    43  	passwd := r.Password
    44  
    45  	err := sqlx.NewTasks(d).With(
    46  		func(db sqlx.DBExecutor) error {
    47  			acc = &models.Account{
    48  				RelAccount: *rel,
    49  				AccountInfo: models.AccountInfo{
    50  					State:  enums.ACCOUNT_STATE__ENABLED,
    51  					Role:   r.Role,
    52  					Avatar: r.AvatarURL,
    53  				},
    54  			}
    55  			if err := acc.Create(db); err != nil {
    56  				if sqlx.DBErr(err).IsConflict() {
    57  					return status.AccountConflict
    58  				}
    59  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
    60  			}
    61  			return nil
    62  		},
    63  		func(db sqlx.DBExecutor) error {
    64  			if err := (&models.AccountIdentity{
    65  				RelAccount: *rel,
    66  				AccountIdentityInfo: models.AccountIdentityInfo{
    67  					Type:       enums.ACCOUNT_IDENTITY_TYPE__USERNAME,
    68  					IdentityID: r.Username,
    69  					Source:     r.Source,
    70  				},
    71  			}).Create(db); err != nil {
    72  				if sqlx.DBErr(err).IsConflict() {
    73  					return status.AccountIdentityConflict
    74  				}
    75  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
    76  			}
    77  			return nil
    78  		},
    79  		func(db sqlx.DBExecutor) error {
    80  			if passwd == "" {
    81  				passwd = string(util.GenRandomPassword(8, 3))
    82  			}
    83  			if err := (&models.AccountPassword{
    84  				RelAccount:         *rel,
    85  				RelAccountPassword: models.RelAccountPassword{PasswordID: g.MustGenSFID()},
    86  				AccountPasswordData: models.AccountPasswordData{
    87  					Type: enums.PASSWORD_TYPE__LOGIN,
    88  					Password: util.HashOfAccountPassword(
    89  						rel.AccountID.String(), passwd,
    90  					),
    91  				},
    92  			}).Create(db); err != nil {
    93  				if sqlx.DBErr(err).IsConflict() {
    94  					return status.AccountPasswordConflict
    95  				}
    96  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
    97  			}
    98  			return nil
    99  		},
   100  		func(d sqlx.DBExecutor) error {
   101  			req := operator.CreateReq{
   102  				Name:       operator.DefaultOperatorName,
   103  				PrivateKey: generateRandomPrivateKey(),
   104  			}
   105  			ctx := types.WithAccount(types.WithMgrDBExecutor(ctx, d), acc)
   106  			_, err := operator.Create(ctx, &req)
   107  			return err
   108  		},
   109  	).Do()
   110  
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	return &CreateAccountByUsernameRsp{
   115  		Account:  acc,
   116  		Password: passwd,
   117  	}, nil
   118  }
   119  
   120  type UpdatePasswordReq struct {
   121  	OldPassword string `json:"oldPassword"`
   122  	Password    string `json:"password"`
   123  }
   124  
   125  func UpdateAccountPassword(ctx context.Context, accountID types.SFID, r *UpdatePasswordReq) error {
   126  	d := types.MustMgrDBExecutorFromContext(ctx)
   127  
   128  	var (
   129  		rel = models.RelAccount{AccountID: accountID}
   130  		acc *models.Account
   131  		aci *models.AccountIdentity
   132  		ap  *models.AccountPassword
   133  	)
   134  
   135  	err := sqlx.NewTasks(d).With(
   136  		func(db sqlx.DBExecutor) error {
   137  			acc = &models.Account{RelAccount: rel}
   138  			if err := acc.FetchByAccountID(db); err != nil {
   139  				if sqlx.DBErr(err).IsNotFound() {
   140  					return status.AccountNotFound
   141  				}
   142  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   143  			}
   144  			if acc.State != enums.ACCOUNT_STATE__ENABLED {
   145  				return status.DisabledAccount
   146  			}
   147  			return nil
   148  		},
   149  		func(db sqlx.DBExecutor) error {
   150  			aci = &models.AccountIdentity{
   151  				RelAccount: rel,
   152  				AccountIdentityInfo: models.AccountIdentityInfo{
   153  					Type: enums.ACCOUNT_IDENTITY_TYPE__USERNAME,
   154  				},
   155  			}
   156  			if err := aci.FetchByAccountIDAndType(db); err != nil {
   157  				if sqlx.DBErr(err).IsNotFound() {
   158  					return status.AccountIdentityNotFound
   159  				}
   160  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   161  			}
   162  			return nil
   163  		},
   164  		func(db sqlx.DBExecutor) error {
   165  			ap = &models.AccountPassword{
   166  				RelAccount: rel,
   167  				AccountPasswordData: models.AccountPasswordData{
   168  					Type: enums.PASSWORD_TYPE__LOGIN,
   169  				},
   170  			}
   171  			if err := ap.FetchByAccountIDAndType(db); err != nil {
   172  				if sqlx.DBErr(err).IsNotFound() {
   173  					return status.AccountPasswordNotFound
   174  				}
   175  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   176  			}
   177  			if ap.Password != util.HashOfAccountPassword(accountID.String(), r.OldPassword) {
   178  				return status.InvalidOldPassword
   179  			}
   180  			if r.OldPassword == r.Password {
   181  				return status.InvalidNewPassword
   182  			}
   183  			return nil
   184  		},
   185  		func(db sqlx.DBExecutor) error {
   186  			ap.Password = util.HashOfAccountPassword(accountID.String(), r.Password)
   187  			if err := ap.UpdateByAccountIDAndType(db); err != nil {
   188  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   189  			}
   190  			return nil
   191  		},
   192  	).Do()
   193  
   194  	if err != nil {
   195  		return err
   196  	}
   197  	return nil
   198  }
   199  
   200  type LoginByUsernameReq struct {
   201  	Username string `json:"username"`
   202  	Password string `json:"password"`
   203  }
   204  
   205  type LoginRsp struct {
   206  	AccountID   types.SFID        `json:"accountID"`
   207  	AccountRole enums.AccountRole `json:"accountRole"`
   208  	Token       string            `json:"token"`
   209  	ExpireAt    types.Timestamp   `json:"expireAt"`
   210  	Issuer      string            `json:"issuer"`
   211  }
   212  
   213  func ValidateLoginByUsername(ctx context.Context, r *LoginByUsernameReq) (*models.Account, error) {
   214  	d := types.MustMgrDBExecutorFromContext(ctx)
   215  
   216  	var (
   217  		rel models.RelAccount
   218  		aci *models.AccountIdentity
   219  		acc *models.Account
   220  	)
   221  
   222  	err := sqlx.NewTasks(d).With(
   223  		func(db sqlx.DBExecutor) error {
   224  			aci = &models.AccountIdentity{
   225  				RelAccount: models.RelAccount{},
   226  				AccountIdentityInfo: models.AccountIdentityInfo{
   227  					Type:       enums.ACCOUNT_IDENTITY_TYPE__USERNAME,
   228  					IdentityID: r.Username,
   229  				},
   230  			}
   231  			if err := aci.FetchByTypeAndIdentityID(db); err != nil {
   232  				if sqlx.DBErr(err).IsNotFound() {
   233  					return status.AccountIdentityNotFound
   234  				}
   235  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   236  			}
   237  			rel.AccountID = aci.AccountID
   238  			return nil
   239  		},
   240  		func(db sqlx.DBExecutor) error {
   241  			acc = &models.Account{RelAccount: rel}
   242  			if err := acc.FetchByAccountID(db); err != nil {
   243  				if sqlx.DBErr(err).IsNotFound() {
   244  					return status.AccountNotFound
   245  				}
   246  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   247  			}
   248  			if acc.State != enums.ACCOUNT_STATE__ENABLED {
   249  				return status.DisabledAccount
   250  			}
   251  			return nil
   252  		},
   253  		func(db sqlx.DBExecutor) error {
   254  			ap := &models.AccountPassword{
   255  				RelAccount: rel,
   256  				AccountPasswordData: models.AccountPasswordData{
   257  					Type: enums.PASSWORD_TYPE__LOGIN,
   258  				},
   259  			}
   260  			if err := ap.FetchByAccountIDAndType(db); err != nil {
   261  				if sqlx.DBErr(err).IsNotFound() {
   262  					return status.AccountPasswordNotFound
   263  				}
   264  				return status.DatabaseError.StatusErr().WithDesc(err.Error())
   265  			}
   266  			if util.HashOfAccountPassword(acc.AccountID.String(), r.Password) != ap.Password {
   267  				return status.InvalidPassword
   268  			}
   269  			return nil
   270  		},
   271  	).Do()
   272  
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  	return acc, nil
   277  }
   278  
   279  func GetAccountByAccountID(ctx context.Context, accountID types.SFID) (*models.Account, error) {
   280  	ctx, l := logr.Start(ctx, "account.GetAccountByAccountID")
   281  	defer l.End()
   282  
   283  	d := types.MustMgrDBExecutorFromContext(ctx)
   284  	m := &models.Account{RelAccount: models.RelAccount{AccountID: accountID}}
   285  
   286  	err := m.FetchByAccountID(d)
   287  	if err != nil {
   288  		if sqlx.DBErr(err).IsNotFound() {
   289  			return nil, status.AccountNotFound
   290  		}
   291  		return nil, status.DatabaseError.StatusErr().WithDesc(err.Error())
   292  	}
   293  	return m, err
   294  }
   295  
   296  func CreateAdminIfNotExist(ctx context.Context) (string, error) {
   297  	ret, err := CreateAccountByUsername(ctx, &CreateAccountByUsernameReq{
   298  		Username: "admin",
   299  		Role:     enums.ACCOUNT_ROLE__ADMIN,
   300  		Password: "iotex.W3B.admin",
   301  		Source:   enums.ACCOUNT_SOURCE__INIT,
   302  	})
   303  	if err != nil {
   304  		key := statusx.FromErr(err).Key
   305  		if key == status.AccountConflict.Key() ||
   306  			key == status.AccountIdentityConflict.Key() {
   307  			return "", nil
   308  		}
   309  		return "", err
   310  	}
   311  	return ret.Password, nil
   312  }
   313  
   314  func generateRandomPrivateKey() string {
   315  	priKey, err := crypto.GenerateKey()
   316  	if err != nil {
   317  		return ""
   318  	}
   319  	return hex.EncodeToString(crypto.FromECDSA(priKey))
   320  }