github.com/decred/politeia@v1.4.0/politeiawww/legacy/user/cockroachdb/cms.go (about)

     1  package cockroachdb
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/decred/politeia/politeiawww/legacy/user"
     8  	"github.com/jinzhu/gorm"
     9  )
    10  
    11  const (
    12  	// CMS plugin table names
    13  	tableCMSUsers     = "cms_users"
    14  	tableCMSCodeStats = "cms_code_stats"
    15  )
    16  
    17  // newCMSUser creates a new User record and a corresponding CMSUser record
    18  // with the provided user info.
    19  //
    20  // This function must be called using a transaction.
    21  func (c *cockroachdb) newCMSUser(tx *gorm.DB, nu user.NewCMSUser) error {
    22  	// Create a new User record
    23  	u := user.User{
    24  		Email:                     nu.Email,
    25  		Username:                  nu.Username,
    26  		NewUserVerificationToken:  nu.NewUserVerificationToken,
    27  		NewUserVerificationExpiry: nu.NewUserVerificationExpiry,
    28  	}
    29  	id, err := c.userNew(tx, u)
    30  	if err != nil {
    31  		return err
    32  	}
    33  
    34  	// Create a CMSUser record
    35  	cms := CMSUser{
    36  		ID:             *id,
    37  		ContractorType: nu.ContractorType,
    38  	}
    39  	err = tx.Create(&cms).Error
    40  	if err != nil {
    41  		return err
    42  	}
    43  	return nil
    44  }
    45  
    46  // cmdNewCMSUser inserts a new CMSUser record into the database.
    47  func (c *cockroachdb) cmdNewCMSUser(payload string) (string, error) {
    48  	// Decode payload
    49  	nu, err := user.DecodeNewCMSUser([]byte(payload))
    50  	if err != nil {
    51  		return "", err
    52  	}
    53  
    54  	// Create a new User record and a new CMSUser
    55  	// record using a transaction.
    56  	tx := c.userDB.Begin()
    57  	err = c.newCMSUser(tx, *nu)
    58  	if err != nil {
    59  		tx.Rollback()
    60  		return "", err
    61  	}
    62  	err = tx.Commit().Error
    63  	if err != nil {
    64  		return "", err
    65  	}
    66  
    67  	// Prepare reply
    68  	var nur user.NewCMSUserReply
    69  	reply, err := user.EncodeNewCMSUserReply(nur)
    70  	if err != nil {
    71  		return "", nil
    72  	}
    73  
    74  	return string(reply), nil
    75  }
    76  
    77  // updateCMSUser updates an existing  CMSUser record with the provided user
    78  // info.
    79  //
    80  // This function must be called using a transaction.
    81  func (c *cockroachdb) updateCMSUser(tx *gorm.DB, nu user.UpdateCMSUser) error {
    82  	cms := CMSUser{
    83  		ID: nu.ID,
    84  	}
    85  	var superVisorUserIds string
    86  	for i, userIds := range nu.SupervisorUserIDs {
    87  		if i == 0 {
    88  			superVisorUserIds = userIds.String()
    89  		} else {
    90  			superVisorUserIds += ", " + userIds.String()
    91  		}
    92  	}
    93  	var proposalsOwned string
    94  	for i, proposal := range nu.ProposalsOwned {
    95  		if i == 0 {
    96  			proposalsOwned = proposal
    97  		} else {
    98  			proposalsOwned += ", " + proposal
    99  		}
   100  	}
   101  	err := tx.First(&cms).Error
   102  	if err != nil {
   103  		if errors.Is(err, gorm.ErrRecordNotFound) {
   104  			cms.Domain = nu.Domain
   105  			cms.GitHubName = nu.GitHubName
   106  			cms.MatrixName = nu.MatrixName
   107  			cms.ContractorName = nu.ContractorName
   108  			cms.ContractorType = nu.ContractorType
   109  			cms.ContractorLocation = nu.ContractorLocation
   110  			cms.ContractorContact = nu.ContractorContact
   111  			cms.SupervisorUserID = superVisorUserIds
   112  			cms.ProposalsOwned = proposalsOwned
   113  			err = tx.Create(&cms).Error
   114  			if err != nil {
   115  				return err
   116  			}
   117  			return nil
   118  		}
   119  		return err
   120  	}
   121  	if nu.Domain != 0 {
   122  		cms.Domain = nu.Domain
   123  	}
   124  	if nu.GitHubName != "" {
   125  		cms.GitHubName = nu.GitHubName
   126  	}
   127  	if nu.MatrixName != "" {
   128  		cms.MatrixName = nu.MatrixName
   129  	}
   130  	if nu.ContractorName != "" {
   131  		cms.ContractorName = nu.ContractorName
   132  	}
   133  	if nu.ContractorType != 0 {
   134  		cms.ContractorType = nu.ContractorType
   135  	}
   136  	if nu.ContractorLocation != "" {
   137  		cms.ContractorLocation = nu.ContractorLocation
   138  	}
   139  	if nu.ContractorContact != "" {
   140  		cms.ContractorContact = nu.ContractorContact
   141  	}
   142  	if superVisorUserIds != "" {
   143  		cms.SupervisorUserID = superVisorUserIds
   144  	}
   145  	if proposalsOwned != "" {
   146  		cms.ProposalsOwned = proposalsOwned
   147  	}
   148  
   149  	err = tx.Save(&cms).Error
   150  	if err != nil {
   151  		return err
   152  	}
   153  	return nil
   154  }
   155  
   156  // cmdUpdateCMSUser updates an existing CMSUser record in the database.
   157  func (c *cockroachdb) cmdUpdateCMSUser(payload string) (string, error) {
   158  	// Decode payload
   159  	uu, err := user.DecodeUpdateCMSUser([]byte(payload))
   160  	if err != nil {
   161  		return "", err
   162  	}
   163  
   164  	// Create a new User record and a new CMSUser
   165  	// record using a transaction.
   166  	tx := c.userDB.Begin()
   167  	err = c.updateCMSUser(tx, *uu)
   168  	if err != nil {
   169  		tx.Rollback()
   170  		return "", err
   171  	}
   172  	err = tx.Commit().Error
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  
   177  	// Prepare reply
   178  	var uur user.UpdateCMSUserReply
   179  	reply, err := user.EncodeUpdateCMSUserReply(uur)
   180  	if err != nil {
   181  		return "", nil
   182  	}
   183  
   184  	return string(reply), nil
   185  }
   186  
   187  // cmdCMSUsersByDomain returns all CMS users within the provided domain.
   188  func (c *cockroachdb) cmdCMSUsersByDomain(payload string) (string, error) {
   189  	// Decode payload
   190  	p, err := user.DecodeCMSUsersByDomain([]byte(payload))
   191  	if err != nil {
   192  		return "", err
   193  	}
   194  
   195  	// Lookup cms users
   196  	var users []CMSUser
   197  	err = c.userDB.
   198  		Where("domain = ?", p.Domain).
   199  		Preload("User").
   200  		Find(&users).
   201  		Error
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  
   206  	// Prepare reply
   207  	u, err := c.convertCMSUsersFromDatabase(users)
   208  	if err != nil {
   209  		return "", err
   210  	}
   211  	r := user.CMSUsersByDomainReply{
   212  		Users: u,
   213  	}
   214  	reply, err := user.EncodeCMSUsersByDomainReply(r)
   215  	if err != nil {
   216  		return "", err
   217  	}
   218  
   219  	return string(reply), nil
   220  }
   221  
   222  // cmdCMSUsersByContractorType returns all CMS users within the provided
   223  // contractor type.
   224  func (c *cockroachdb) cmdCMSUsersByContractorType(payload string) (string, error) {
   225  	// Decode payload
   226  	p, err := user.DecodeCMSUsersByContractorType([]byte(payload))
   227  	if err != nil {
   228  		return "", err
   229  	}
   230  
   231  	// Lookup cms users
   232  	var users []CMSUser
   233  	err = c.userDB.
   234  		Where("contractor_type = ?", p.ContractorType).
   235  		Preload("User").
   236  		Find(&users).
   237  		Error
   238  	if err != nil {
   239  		return "", err
   240  	}
   241  
   242  	// Prepare reply
   243  	u, err := c.convertCMSUsersFromDatabase(users)
   244  	if err != nil {
   245  		return "", err
   246  	}
   247  	r := user.CMSUsersByContractorTypeReply{
   248  		Users: u,
   249  	}
   250  	reply, err := user.EncodeCMSUsersByContractorTypeReply(r)
   251  	if err != nil {
   252  		return "", err
   253  	}
   254  
   255  	return string(reply), nil
   256  }
   257  
   258  // cmdCMSUsersByProposalToken returns all CMS users within the provided
   259  // contractor type.
   260  func (c *cockroachdb) cmdCMSUsersByProposalToken(payload string) (string, error) {
   261  	// Decode payload
   262  	p, err := user.DecodeCMSUsersByProposalToken([]byte(payload))
   263  	if err != nil {
   264  		return "", err
   265  	}
   266  
   267  	// Lookup cms users
   268  	var users []CMSUser
   269  	err = c.userDB.
   270  		Where("'" + p.Token + "' = ANY(string_to_array(proposals_owned, ','))").
   271  		Preload("User").
   272  		Find(&users).
   273  		Error
   274  	if err != nil {
   275  		return "", err
   276  	}
   277  
   278  	// Prepare reply
   279  	u, err := c.convertCMSUsersFromDatabase(users)
   280  	if err != nil {
   281  		return "", err
   282  	}
   283  	r := user.CMSUsersByProposalTokenReply{
   284  		Users: u,
   285  	}
   286  	reply, err := user.EncodeCMSUsersByProposalTokenReply(r)
   287  	if err != nil {
   288  		return "", err
   289  	}
   290  
   291  	return string(reply), nil
   292  }
   293  
   294  // cmdCMSUserByID returns the user information for a given user ID.
   295  func (c *cockroachdb) cmdCMSUserByID(payload string) (string, error) {
   296  	// Decode payload
   297  	p, err := user.DecodeCMSUserByID([]byte(payload))
   298  	if err != nil {
   299  		return "", err
   300  	}
   301  	var cmsUser CMSUser
   302  	err = c.userDB.
   303  		Where("id = ?", p.ID).
   304  		Preload("User").
   305  		Find(&cmsUser).
   306  		Error
   307  	if err != nil {
   308  		if errors.Is(err, gorm.ErrRecordNotFound) {
   309  			// It's ok if there are no cms records found for this user.
   310  			// But we do need to request the rest of the user details from the
   311  			// www User table.
   312  			var u User
   313  			err = c.userDB.
   314  				Where("id = ?", p.ID).
   315  				Find(&u).
   316  				Error
   317  			if err != nil {
   318  				if errors.Is(err, gorm.ErrRecordNotFound) {
   319  					err = user.ErrUserNotFound
   320  				}
   321  				return "", err
   322  			}
   323  			cmsUser.User = u
   324  		} else {
   325  			return "", err
   326  		}
   327  	}
   328  
   329  	// Prepare reply
   330  	u, err := c.convertCMSUserFromDatabase(cmsUser)
   331  	if err != nil {
   332  		return "", err
   333  	}
   334  	r := user.CMSUserByIDReply{
   335  		User: u,
   336  	}
   337  	reply, err := user.EncodeCMSUserByIDReply(r)
   338  	if err != nil {
   339  		return "", err
   340  	}
   341  
   342  	return string(reply), nil
   343  }
   344  
   345  func (c *cockroachdb) cmdCMSUserSubContractors(payload string) (string, error) {
   346  	// Decode payload
   347  	p, err := user.DecodeCMSUserByID([]byte(payload))
   348  	if err != nil {
   349  		return "", err
   350  	}
   351  	var cmsUsers []CMSUser
   352  
   353  	// This is done this way currently because GORM doesn't appear to properly
   354  	// parse the following:
   355  	// Where("? = ANY(string_to_array(supervisor_user_id, ','))", p.ID)
   356  	err = c.userDB.
   357  		Where("'" + p.ID + "' = ANY(string_to_array(supervisor_user_id, ','))").
   358  		Preload("User").
   359  		Find(&cmsUsers).
   360  		Error
   361  	if err != nil {
   362  		return "", err
   363  	}
   364  	// Prepare reply
   365  	subUsers := make([]user.CMSUser, 0, len(cmsUsers))
   366  	for _, u := range cmsUsers {
   367  		convertUser, err := c.convertCMSUserFromDatabase(u)
   368  		if err != nil {
   369  			return "", err
   370  		}
   371  		subUsers = append(subUsers, *convertUser)
   372  	}
   373  	r := user.CMSUserSubContractorsReply{
   374  		Users: subUsers,
   375  	}
   376  	reply, err := user.EncodeCMSUserSubContractorsReply(r)
   377  	if err != nil {
   378  		return "", err
   379  	}
   380  
   381  	return string(reply), nil
   382  }
   383  
   384  // NewCMSCodeStats is an exported function (for testing) to insert a new
   385  // code stats row into the cms_code_stats table.
   386  func (c *cockroachdb) NewCMSCodeStats(nu *user.NewCMSCodeStats) error {
   387  
   388  	tx := c.userDB.Begin()
   389  	for _, ncs := range nu.UserCodeStats {
   390  		cms := convertCodestatsToDatabase(ncs)
   391  		err := c.newCMSCodeStats(tx, cms)
   392  		if err != nil {
   393  			tx.Rollback()
   394  			return err
   395  		}
   396  	}
   397  	return tx.Commit().Error
   398  }
   399  
   400  // cmdNewCMSCodeStats inserts a new CMSUser record into the database.
   401  func (c *cockroachdb) cmdNewCMSCodeStats(payload string) (string, error) {
   402  	// Decode payload
   403  	nu, err := user.DecodeNewCMSCodeStats([]byte(payload))
   404  	if err != nil {
   405  		return "", err
   406  	}
   407  	err = c.NewCMSCodeStats(nu)
   408  	if err != nil {
   409  		return "", err
   410  	}
   411  
   412  	// Prepare reply
   413  	var nur user.NewCMSCodeStatsReply
   414  	reply, err := user.EncodeNewCMSCodeStatsReply(nur)
   415  	if err != nil {
   416  		return "", nil
   417  	}
   418  
   419  	return string(reply), nil
   420  }
   421  
   422  // cmdUpdateCMSCodeStats updates an existing CMSUser record into the database.
   423  func (c *cockroachdb) cmdUpdateCMSCodeStats(payload string) (string, error) {
   424  	// Decode payload
   425  	nu, err := user.DecodeUpdateCMSCodeStats([]byte(payload))
   426  	if err != nil {
   427  		return "", err
   428  	}
   429  
   430  	err = c.UpdateCMSCodeStats(nu)
   431  	if err != nil {
   432  		return "", err
   433  	}
   434  
   435  	// Prepare reply
   436  	var nur user.UpdateCMSCodeStatsReply
   437  	reply, err := user.EncodeUpdateCMSCodeStatsReply(nur)
   438  	if err != nil {
   439  		return "", nil
   440  	}
   441  
   442  	return string(reply), nil
   443  }
   444  
   445  func (c *cockroachdb) UpdateCMSCodeStats(ucs *user.UpdateCMSCodeStats) error {
   446  	tx := c.userDB.Begin()
   447  	for _, ncs := range ucs.UserCodeStats {
   448  		cms := convertCodestatsToDatabase(ncs)
   449  		err := c.updateCMSCodeStats(tx, cms)
   450  		if err != nil {
   451  			tx.Rollback()
   452  			return err
   453  		}
   454  	}
   455  	return tx.Commit().Error
   456  }
   457  
   458  // updateCMSCodeStats updates a CMS Code stats record
   459  //
   460  // This function must be called using a transaction.
   461  func (c *cockroachdb) updateCMSCodeStats(tx *gorm.DB, cs CMSCodeStats) error {
   462  	err := tx.Save(&cs).Error
   463  	if err != nil {
   464  		return err
   465  	}
   466  	return nil
   467  }
   468  
   469  // newCMSCodeStats creates a new User record and a corresponding CMSUser record
   470  // with the provided user info.
   471  //
   472  // This function must be called using a transaction.
   473  func (c *cockroachdb) newCMSCodeStats(tx *gorm.DB, cs CMSCodeStats) error {
   474  	err := tx.Create(&cs).Error
   475  	if err != nil {
   476  		return err
   477  	}
   478  	return nil
   479  }
   480  
   481  func (c *cockroachdb) cmdCMSCodeStatsByUserMonthYear(payload string) (string, error) {
   482  	// Decode payload
   483  	p, err := user.DecodeCMSCodeStatsByUserMonthYear([]byte(payload))
   484  	if err != nil {
   485  		return "", err
   486  	}
   487  
   488  	userCodeStats, err := c.CMSCodeStatsByUserMonthYear(p)
   489  	if err != nil {
   490  		return "", err
   491  	}
   492  	r := user.CMSCodeStatsByUserMonthYearReply{
   493  		UserCodeStats: userCodeStats,
   494  	}
   495  	reply, err := user.EncodeCMSCodeStatsByUserMonthYearReply(r)
   496  	if err != nil {
   497  		return "", err
   498  	}
   499  
   500  	return string(reply), nil
   501  }
   502  
   503  func (c *cockroachdb) CMSCodeStatsByUserMonthYear(p *user.CMSCodeStatsByUserMonthYear) ([]user.CodeStats, error) {
   504  	var cmsCodeStats []CMSCodeStats
   505  	err := c.userDB.
   506  		Where("git_hub_name = ? AND month = ? AND year = ?", p.GithubName,
   507  			p.Month, p.Year).
   508  		Find(&cmsCodeStats).
   509  		Error
   510  	if err != nil {
   511  		if err == gorm.ErrRecordNotFound {
   512  			err = user.ErrCodeStatsNotFound
   513  			return nil, err
   514  		}
   515  		return nil, err
   516  	}
   517  	// Prepare reply
   518  	userCodeStats := make([]user.CodeStats, 0, len(cmsCodeStats))
   519  	for _, u := range cmsCodeStats {
   520  		userCodeStats = append(userCodeStats, convertCodestatsFromDatabase(u))
   521  	}
   522  	return userCodeStats, nil
   523  }
   524  
   525  // Exec executes a cms plugin command.
   526  func (c *cockroachdb) cmsPluginExec(cmd, payload string) (string, error) {
   527  	switch cmd {
   528  	case user.CmdNewCMSUser:
   529  		return c.cmdNewCMSUser(payload)
   530  	case user.CmdCMSUsersByDomain:
   531  		return c.cmdCMSUsersByDomain(payload)
   532  	case user.CmdCMSUsersByContractorType:
   533  		return c.cmdCMSUsersByContractorType(payload)
   534  	case user.CmdUpdateCMSUser:
   535  		return c.cmdUpdateCMSUser(payload)
   536  	case user.CmdCMSUserByID:
   537  		return c.cmdCMSUserByID(payload)
   538  	case user.CmdCMSUserSubContractors:
   539  		return c.cmdCMSUserSubContractors(payload)
   540  	case user.CmdCMSUsersByProposalToken:
   541  		return c.cmdCMSUsersByProposalToken(payload)
   542  	case user.CmdNewCMSUserCodeStats:
   543  		return c.cmdNewCMSCodeStats(payload)
   544  	case user.CmdUpdateCMSUserCodeStats:
   545  		return c.cmdUpdateCMSCodeStats(payload)
   546  	case user.CmdCMSCodeStatsByUserMonthYear:
   547  		return c.cmdCMSCodeStatsByUserMonthYear(payload)
   548  	default:
   549  		return "", user.ErrInvalidPluginCmd
   550  	}
   551  }
   552  
   553  // cmsPluginCreateTables creates all cms plugin tables and inserts a cms
   554  // plugin version record into the database.
   555  //
   556  // This function must be called using a transaction.
   557  func (c *cockroachdb) cmsPluginCreateTables(tx *gorm.DB) error {
   558  	if !tx.HasTable(tableCMSUsers) {
   559  		// Build tables
   560  		err := tx.CreateTable(&CMSUser{}).Error
   561  		if err != nil {
   562  			return err
   563  		}
   564  		// Insert version record
   565  		kv := KeyValue{
   566  			Key:   user.CMSPluginID,
   567  			Value: []byte(user.CMSPluginVersion),
   568  		}
   569  		err = tx.Create(&kv).Error
   570  		if err != nil {
   571  			return err
   572  		}
   573  	}
   574  	if !tx.HasTable(tableCMSCodeStats) {
   575  		err := tx.CreateTable(&CMSCodeStats{}).Error
   576  		if err != nil {
   577  			return err
   578  		}
   579  	}
   580  	return nil
   581  }
   582  
   583  // cmsPluginSetup creates all cms plugin tables and ensures the database
   584  // is using the correct cms plugin version.
   585  func (c *cockroachdb) cmsPluginSetup() error {
   586  	// Setup database tables
   587  	tx := c.userDB.Begin()
   588  	err := c.cmsPluginCreateTables(tx)
   589  	if err != nil {
   590  		tx.Rollback()
   591  		return err
   592  	}
   593  
   594  	err = tx.Commit().Error
   595  	if err != nil {
   596  		return err
   597  	}
   598  
   599  	// Check version record
   600  	kv := KeyValue{
   601  		Key: user.CMSPluginID,
   602  	}
   603  	err = c.userDB.Find(&kv).Error
   604  	if err != nil {
   605  		return err
   606  	}
   607  
   608  	// XXX a version mismatch will need to trigger a
   609  	// migration but just return an error for now.
   610  	if string(kv.Value) != user.CMSPluginVersion {
   611  		return fmt.Errorf("wrong plugin version")
   612  	}
   613  
   614  	return nil
   615  }