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 }