github.com/decred/politeia@v1.4.0/politeiawww/legacy/cmsuser.go (about) 1 // Copyright (c) 2017-2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package legacy 6 7 import ( 8 "bytes" 9 "encoding/hex" 10 "errors" 11 "fmt" 12 "sort" 13 "strings" 14 "time" 15 16 cms "github.com/decred/politeia/politeiawww/api/cms/v1" 17 www "github.com/decred/politeia/politeiawww/api/www/v1" 18 "github.com/decred/politeia/politeiawww/legacy/user" 19 "github.com/google/uuid" 20 ) 21 22 // processInviteNewUser creates a new user in the db if it doesn't already 23 // exist and sets a verification token and expiry; the token must be 24 // verified before it expires. If the user already exists in the db 25 // and its token is expired, it generates a new one. 26 // 27 // Note that this function always returns a InviteNewUserReply. The caller 28 // shall verify error and determine how to return this information upstream. 29 func (p *Politeiawww) processInviteNewUser(u cms.InviteNewUser) (*cms.InviteNewUserReply, error) { 30 log.Tracef("processInviteNewUser: %v", u.Email) 31 32 // Validate email 33 if !validEmail.MatchString(u.Email) { 34 log.Debugf("processInviteNewUser: invalid email '%v'", u.Email) 35 return nil, www.UserError{ 36 ErrorCode: www.ErrorStatusMalformedEmail, 37 } 38 } 39 40 // Check if the user is already verified. 41 existingUser, err := p.userByEmail(u.Email) 42 if err == nil { 43 if existingUser.NewUserVerificationToken == nil { 44 return &cms.InviteNewUserReply{}, nil 45 } 46 } 47 48 // Generate the verification token and expiry. 49 token, expiry, err := newVerificationTokenAndExpiry() 50 if err != nil { 51 return nil, err 52 } 53 54 // Try to email the verification link first; if it fails, then 55 // the new user won't be created. 56 // 57 // This is conditional on the email server being setup. 58 err = p.emailUserCMSInvite(u.Email, hex.EncodeToString(token)) 59 if err != nil { 60 log.Errorf("processInviteNewUser: verification email "+ 61 "failed for '%v': %v", u.Email, err) 62 return &cms.InviteNewUserReply{}, nil 63 } 64 65 // If the user already exists, the user record is updated 66 // in the db in order to reset the verification token and 67 // expiry. 68 if existingUser != nil { 69 existingUser.NewUserVerificationToken = token 70 existingUser.NewUserVerificationExpiry = expiry 71 err = p.db.UserUpdate(*existingUser) 72 if err != nil { 73 return nil, err 74 } 75 76 return &cms.InviteNewUserReply{ 77 VerificationToken: hex.EncodeToString(token), 78 }, nil 79 } 80 81 // Create a new cms user with the provided information. 82 // This temporarily sets the username to the given email, which will be 83 // overwritten during registration. This is needed since the constraints 84 // on cockroachdb for usernames requires there to be no duplicates. If 85 // unset, the username of "" will cause a duplicate error to be thrown. 86 nu := user.NewCMSUser{ 87 Email: strings.ToLower(u.Email), 88 Username: strings.ToLower(u.Email), 89 NewUserVerificationToken: token, 90 NewUserVerificationExpiry: expiry, 91 } 92 93 // Set the user to temporary if request includes it. 94 // While this will allow a user to bypass the DCC process, the temporary 95 // users will have extensive restrictions on their account and ability 96 // to submit invoices. 97 if u.Temporary { 98 nu.ContractorType = int(cms.ContractorTypeTemp) 99 } else { 100 nu.ContractorType = int(cms.ContractorTypeNominee) 101 } 102 103 payload, err := user.EncodeNewCMSUser(nu) 104 if err != nil { 105 return nil, err 106 } 107 pc := user.PluginCommand{ 108 ID: user.CMSPluginID, 109 Command: user.CmdNewCMSUser, 110 Payload: string(payload), 111 } 112 _, err = p.db.PluginExec(pc) 113 if err != nil { 114 return nil, err 115 } 116 117 // Update user emails cache 118 usr, err := p.db.UserGetByUsername(nu.Username) 119 if err != nil { 120 return nil, err 121 } 122 p.setUserEmailsCache(usr.Email, usr.ID) 123 124 return &cms.InviteNewUserReply{ 125 VerificationToken: hex.EncodeToString(token), 126 }, nil 127 } 128 129 // processRegisterUser allows a CMS user that has received an invite to 130 // register their account. The username and password for the account are 131 // updated and the user's email address and identity are marked as verified. 132 func (p *Politeiawww) processRegisterUser(u cms.RegisterUser) (*cms.RegisterUserReply, error) { 133 log.Tracef("processRegisterUser: %v", u.Email) 134 var reply cms.RegisterUserReply 135 136 // Check that the user already exists. 137 existingUser, err := p.userByEmail(u.Email) 138 if err != nil { 139 if errors.Is(err, user.ErrUserNotFound) { 140 log.Debugf("RegisterUser failure for %v: user not found", 141 u.Email) 142 return nil, www.UserError{ 143 ErrorCode: www.ErrorStatusVerificationTokenInvalid, 144 } 145 } 146 return nil, err 147 } 148 149 // Validate public key and ensure its unique. 150 err = validatePubKey(u.PublicKey) 151 if err != nil { 152 return nil, err 153 } 154 _, err = p.db.UserGetByPubKey(u.PublicKey) 155 switch err { 156 case user.ErrUserNotFound: 157 // Pubkey is unique; continue 158 case nil: 159 // Pubkey is not unique 160 return nil, www.UserError{ 161 ErrorCode: www.ErrorStatusDuplicatePublicKey, 162 } 163 default: 164 // All other errors 165 return nil, err 166 } 167 168 // Format and validate the username. 169 username := formatUsername(u.Username) 170 err = validateUsername(username) 171 if err != nil { 172 return nil, err 173 } 174 175 // Ensure username is unique. 176 _, err = p.db.UserGetByUsername(u.Username) 177 switch err { 178 case nil: 179 // Duplicate username 180 return nil, www.UserError{ 181 ErrorCode: www.ErrorStatusDuplicateUsername, 182 } 183 case user.ErrUserNotFound: 184 // Username doesn't exist; continue 185 default: 186 return nil, err 187 } 188 189 // Validate the password. 190 err = validatePassword(u.Password) 191 if err != nil { 192 return nil, err 193 } 194 195 // Hash the user's password. 196 hashedPassword, err := p.hashPassword(u.Password) 197 if err != nil { 198 return nil, err 199 } 200 201 // Decode the verification token. 202 token, err := hex.DecodeString(u.VerificationToken) 203 if err != nil { 204 log.Debugf("Register failure for %v: verification token could "+ 205 "not be decoded: %v", u.Email, err) 206 return nil, www.UserError{ 207 ErrorCode: www.ErrorStatusVerificationTokenInvalid, 208 } 209 } 210 211 // Check that the verification token matches. 212 if !bytes.Equal(token, existingUser.NewUserVerificationToken) { 213 log.Debugf("Register failure for %v: verification token doesn't "+ 214 "match, expected %v", u.Email, existingUser.NewUserVerificationToken) 215 return nil, www.UserError{ 216 ErrorCode: www.ErrorStatusVerificationTokenInvalid, 217 } 218 } 219 220 // Check that the token hasn't expired. 221 if time.Now().Unix() > existingUser.NewUserVerificationExpiry { 222 log.Debugf("Register failure for %v: verification token expired", 223 u.Email) 224 return nil, www.UserError{ 225 ErrorCode: www.ErrorStatusVerificationTokenExpired, 226 } 227 } 228 229 // Create a new database user with the provided information. 230 newUser := user.User{ 231 ID: existingUser.ID, 232 Email: strings.ToLower(u.Email), 233 Username: username, 234 HashedPassword: hashedPassword, 235 NewUserVerificationToken: nil, 236 NewUserVerificationExpiry: 0, 237 } 238 239 // Setup newUser's identity with the provided public key. An 240 // additional verification step to activate the identity is 241 // not needed since the registration email already serves as 242 // the verification. 243 id, err := user.NewIdentity(u.PublicKey) 244 if err != nil { 245 return nil, err 246 } 247 err = newUser.AddIdentity(*id) 248 if err != nil { 249 return nil, err 250 } 251 err = newUser.ActivateIdentity(id.Key[:]) 252 if err != nil { 253 return nil, err 254 } 255 256 // Update the user in the db. 257 err = p.db.UserUpdate(newUser) 258 if err != nil { 259 return nil, err 260 } 261 262 // Even if user is non-nil, this will bring it up-to-date 263 // with the new information inserted via newUser. 264 existingUser, err = p.userByEmail(newUser.Email) 265 if err != nil { 266 return nil, fmt.Errorf("unable to retrieve account info for %v: %v", 267 newUser.Email, err) 268 } 269 270 err = p.db.UserUpdate(newUser) 271 if err != nil { 272 return nil, err 273 } 274 return &reply, nil 275 } 276 277 // processCMSUsers returns a list of cms users given a set of filters. If 278 // either domain or contractor is non-zero then they are used as matching 279 // criteria, otherwise the full list will be returned. 280 func (p *Politeiawww) processCMSUsers(users *cms.CMSUsers) (*cms.CMSUsersReply, error) { 281 log.Tracef("processCMSUsers") 282 283 domain := int(users.Domain) 284 contractortype := int(users.ContractorType) 285 286 matchedUsers := make([]cms.AbridgedCMSUser, 0, www.UserListPageSize) 287 288 if domain != 0 { 289 // Setup plugin command 290 cu := user.CMSUsersByDomain{ 291 Domain: domain, 292 } 293 payload, err := user.EncodeCMSUsersByDomain(cu) 294 if err != nil { 295 return nil, err 296 } 297 pc := user.PluginCommand{ 298 ID: user.CMSPluginID, 299 Command: user.CmdCMSUsersByDomain, 300 Payload: string(payload), 301 } 302 303 // Execute plugin command 304 pcr, err := p.db.PluginExec(pc) 305 if err != nil { 306 return nil, err 307 } 308 309 // Decode reply 310 reply, err := user.DecodeCMSUsersByDomainReply([]byte(pcr.Payload)) 311 if err != nil { 312 return nil, err 313 } 314 for _, u := range reply.Users { 315 // Only add matched users if contractor type is 0 or it matches 316 // the request 317 if contractortype == 0 || 318 (contractortype != 0 && u.ContractorType == contractortype) { 319 matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{ 320 Domain: cms.DomainTypeT(u.Domain), 321 Username: u.Username, 322 ContractorType: cms.ContractorTypeT(u.ContractorType), 323 ID: u.ID.String(), 324 }) 325 } 326 } 327 } else if contractortype != 0 { 328 // Setup plugin command 329 cu := user.CMSUsersByContractorType{ 330 ContractorType: contractortype, 331 } 332 payload, err := user.EncodeCMSUsersByContractorType(cu) 333 if err != nil { 334 return nil, err 335 } 336 pc := user.PluginCommand{ 337 ID: user.CMSPluginID, 338 Command: user.CmdCMSUsersByContractorType, 339 Payload: string(payload), 340 } 341 342 // Execute plugin command 343 pcr, err := p.db.PluginExec(pc) 344 if err != nil { 345 return nil, err 346 } 347 348 // Decode reply 349 reply, err := user.DecodeCMSUsersByContractorTypeReply( 350 []byte(pcr.Payload)) 351 if err != nil { 352 return nil, err 353 } 354 for _, u := range reply.Users { 355 // We already know domain is 0 if here so no need to check. 356 matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{ 357 Domain: cms.DomainTypeT(u.Domain), 358 Username: u.Username, 359 ContractorType: cms.ContractorTypeT(u.ContractorType), 360 ID: u.ID.String(), 361 }) 362 } 363 } else { 364 // Both contractor type and domain are 0 so just return all users. 365 err := p.db.AllUsers(func(u *user.User) { 366 // Setup plugin command 367 cu := user.CMSUserByID{ 368 ID: u.ID.String(), 369 } 370 payload, err := user.EncodeCMSUserByID(cu) 371 if err != nil { 372 log.Error(err) 373 return 374 } 375 pc := user.PluginCommand{ 376 ID: user.CMSPluginID, 377 Command: user.CmdCMSUserByID, 378 Payload: string(payload), 379 } 380 381 // Execute plugin command 382 pcr, err := p.db.PluginExec(pc) 383 if err != nil { 384 log.Error(err) 385 return 386 } 387 388 // Decode reply 389 reply, err := user.DecodeCMSUserByIDReply([]byte(pcr.Payload)) 390 if err != nil { 391 log.Error(err) 392 return 393 } 394 matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{ 395 ID: u.ID.String(), 396 Username: u.Username, 397 Domain: cms.DomainTypeT(reply.User.Domain), 398 ContractorType: cms.ContractorTypeT(reply.User.ContractorType), 399 }) 400 }) 401 if err != nil { 402 return nil, err 403 } 404 } 405 406 // Sort results alphabetically. 407 sort.Slice(matchedUsers, func(i, j int) bool { 408 return matchedUsers[i].Username < matchedUsers[j].Username 409 }) 410 411 return &cms.CMSUsersReply{ 412 Users: matchedUsers, 413 }, nil 414 } 415 416 func (p *Politeiawww) processEditCMSUser(ecu cms.EditUser, u *user.User) (*cms.EditUserReply, error) { 417 log.Tracef("processEditCMSUser: %v", u.Email) 418 419 reply := cms.EditUserReply{} 420 421 err := validateUserInformation(ecu) 422 if err != nil { 423 return nil, err 424 } 425 426 uu := user.UpdateCMSUser{ 427 ID: u.ID, 428 } 429 430 if ecu.GitHubName != "" { 431 uu.GitHubName = ecu.GitHubName 432 } 433 if ecu.MatrixName != "" { 434 uu.MatrixName = ecu.MatrixName 435 } 436 if ecu.ContractorName != "" { 437 uu.ContractorName = ecu.ContractorName 438 } 439 if ecu.ContractorLocation != "" { 440 uu.ContractorLocation = ecu.ContractorLocation 441 } 442 if ecu.ContractorContact != "" { 443 uu.ContractorContact = ecu.ContractorContact 444 } 445 payload, err := user.EncodeUpdateCMSUser(uu) 446 if err != nil { 447 return nil, err 448 } 449 pc := user.PluginCommand{ 450 ID: user.CMSPluginID, 451 Command: user.CmdUpdateCMSUser, 452 Payload: string(payload), 453 } 454 _, err = p.db.PluginExec(pc) 455 if err != nil { 456 return nil, err 457 } 458 return &reply, nil 459 } 460 461 func (p *Politeiawww) processManageCMSUser(mu cms.CMSManageUser) (*cms.CMSManageUserReply, error) { 462 log.Tracef("processManageCMSUser: %v", mu.UserID) 463 464 editUser, err := p.userByIDStr(mu.UserID) 465 if err != nil { 466 return nil, err 467 } 468 469 uu := user.UpdateCMSUser{ 470 ID: editUser.ID, 471 } 472 473 if mu.Domain != 0 { 474 uu.Domain = int(mu.Domain) 475 } 476 if mu.ContractorType != 0 { 477 uu.ContractorType = int(mu.ContractorType) 478 } 479 if len(mu.SupervisorUserIDs) > 0 { 480 // Validate SupervisorUserID input 481 parseSuperUserIds := make([]uuid.UUID, 0, len(mu.SupervisorUserIDs)) 482 for _, super := range mu.SupervisorUserIDs { 483 parseUUID, err := uuid.Parse(super) 484 if err != nil { 485 e := fmt.Sprintf("invalid uuid: %v", super) 486 return nil, www.UserError{ 487 ErrorCode: cms.ErrorStatusInvalidSupervisorUser, 488 ErrorContext: []string{e}, 489 } 490 } 491 u, err := p.getCMSUserByID(super) 492 if err != nil { 493 e := fmt.Sprintf("user not found: %v", super) 494 return nil, www.UserError{ 495 ErrorCode: cms.ErrorStatusInvalidSupervisorUser, 496 ErrorContext: []string{e}, 497 } 498 } 499 if u.ContractorType != cms.ContractorTypeSupervisor { 500 e := fmt.Sprintf("user not a supervisor: %v", super) 501 return nil, www.UserError{ 502 ErrorCode: cms.ErrorStatusInvalidSupervisorUser, 503 ErrorContext: []string{e}, 504 } 505 } 506 parseSuperUserIds = append(parseSuperUserIds, parseUUID) 507 } 508 uu.SupervisorUserIDs = parseSuperUserIds 509 } 510 511 if len(mu.ProposalsOwned) > 0 { 512 uu.ProposalsOwned = mu.ProposalsOwned 513 } 514 515 payload, err := user.EncodeUpdateCMSUser(uu) 516 if err != nil { 517 return nil, err 518 } 519 pc := user.PluginCommand{ 520 ID: user.CMSPluginID, 521 Command: user.CmdUpdateCMSUser, 522 Payload: string(payload), 523 } 524 _, err = p.db.PluginExec(pc) 525 if err != nil { 526 return nil, err 527 } 528 return &cms.CMSManageUserReply{}, nil 529 } 530 531 func (p *Politeiawww) processCMSUserDetails(ud *cms.UserDetails, isCurrentUser bool, isAdmin bool) (*cms.UserDetailsReply, error) { 532 reply := cms.UserDetailsReply{} 533 u, err := p.getCMSUserByID(ud.UserID) 534 if err != nil { 535 return nil, err 536 } 537 538 // Filter returned fields in case the user isn't the admin or the current user 539 if !isAdmin && !isCurrentUser { 540 reply.User = filterCMSUserPublicFields(*u) 541 } else { 542 reply.User = *u 543 } 544 545 return &reply, nil 546 } 547 548 // cmsUsersByDomain returns all cms user within the provided contractor domain. 549 func (p *Politeiawww) cmsUsersByDomain(d cms.DomainTypeT) ([]user.CMSUser, error) { 550 // Setup plugin command 551 cu := user.CMSUsersByDomain{ 552 Domain: int(d), 553 } 554 payload, err := user.EncodeCMSUsersByDomain(cu) 555 if err != nil { 556 return nil, err 557 } 558 pc := user.PluginCommand{ 559 ID: user.CMSPluginID, 560 Command: user.CmdCMSUsersByDomain, 561 Payload: string(payload), 562 } 563 564 // Execute plugin command 565 pcr, err := p.db.PluginExec(pc) 566 if err != nil { 567 return nil, err 568 } 569 570 // Decode reply 571 reply, err := user.DecodeCMSUsersByDomainReply([]byte(pcr.Payload)) 572 if err != nil { 573 return nil, err 574 } 575 576 return reply.Users, nil 577 } 578 579 // filterCMSUserPublicFields creates a filtered copy of a cms User that only 580 // contains public information. 581 func filterCMSUserPublicFields(user cms.User) cms.User { 582 return cms.User{ 583 ID: user.ID, 584 Admin: user.Admin, 585 Username: user.Username, 586 Identities: user.Identities, 587 588 // CMS User Details 589 ContractorType: user.ContractorType, 590 GitHubName: user.GitHubName, 591 MatrixName: user.MatrixName, 592 Domain: user.Domain, 593 } 594 } 595 596 func validateUserInformation(userInfo cms.EditUser) error { 597 var err error 598 if userInfo.GitHubName != "" { 599 contact := formatContact(userInfo.GitHubName) 600 err = validateContact(contact) 601 if err != nil { 602 return www.UserError{ 603 ErrorCode: cms.ErrorStatusInvoiceMalformedContact, 604 } 605 } 606 } 607 if userInfo.MatrixName != "" { 608 contact := formatContact(userInfo.MatrixName) 609 err = validateContact(contact) 610 if err != nil { 611 return www.UserError{ 612 ErrorCode: cms.ErrorStatusInvoiceMalformedContact, 613 } 614 } 615 } 616 if userInfo.ContractorName != "" { 617 name := formatName(userInfo.ContractorName) 618 err = validateName(name) 619 if err != nil { 620 return www.UserError{ 621 ErrorCode: cms.ErrorStatusMalformedName, 622 } 623 } 624 } 625 if userInfo.ContractorLocation != "" { 626 location := formatLocation(userInfo.ContractorLocation) 627 err = validateLocation(location) 628 if err != nil { 629 return www.UserError{ 630 ErrorCode: cms.ErrorStatusMalformedLocation, 631 } 632 } 633 } 634 if userInfo.ContractorContact != "" { 635 contact := formatContact(userInfo.ContractorContact) 636 err = validateContact(contact) 637 if err != nil { 638 return www.UserError{ 639 ErrorCode: cms.ErrorStatusInvoiceMalformedContact, 640 } 641 } 642 } 643 return nil 644 } 645 func (p *Politeiawww) getCMSUserByID(id string) (*cms.User, error) { 646 ubi := user.CMSUserByID{ 647 ID: id, 648 } 649 payload, err := user.EncodeCMSUserByID(ubi) 650 if err != nil { 651 return nil, err 652 } 653 pc := user.PluginCommand{ 654 ID: user.CMSPluginID, 655 Command: user.CmdCMSUserByID, 656 Payload: string(payload), 657 } 658 payloadReply, err := p.db.PluginExec(pc) 659 if err != nil { 660 return nil, err 661 } 662 ubir, err := user.DecodeCMSUserByIDReply([]byte(payloadReply.Payload)) 663 if err != nil { 664 return nil, err 665 } 666 u := convertCMSUserFromDatabaseUser(ubir.User) 667 return &u, nil 668 } 669 670 func (p *Politeiawww) getCMSUserByIDRaw(id string) (*user.CMSUser, error) { 671 ubi := user.CMSUserByID{ 672 ID: id, 673 } 674 payload, err := user.EncodeCMSUserByID(ubi) 675 if err != nil { 676 return nil, err 677 } 678 pc := user.PluginCommand{ 679 ID: user.CMSPluginID, 680 Command: user.CmdCMSUserByID, 681 Payload: string(payload), 682 } 683 payloadReply, err := p.db.PluginExec(pc) 684 if err != nil { 685 return nil, err 686 } 687 ubir, err := user.DecodeCMSUserByIDReply([]byte(payloadReply.Payload)) 688 if err != nil { 689 return nil, err 690 } 691 return ubir.User, nil 692 } 693 694 func (p *Politeiawww) getCMSUserWeights() (map[string]int64, error) { 695 userWeights := make(map[string]int64, 1080) 696 697 /* 698 1) Determine most recent payout month 699 2) For each user 700 1) Look back for 6 months of invoices (that were paid out) 701 2) Add up all minutes billed over that time period. 702 3) Set user weight as total number of billed minutes. 703 */ 704 705 weightEnd := time.Now() 706 weightMonthEnd := uint(weightEnd.Month()) 707 weightYearEnd := uint(weightEnd.Year()) 708 709 weightStart := time.Now().AddDate(0, -1*userWeightMonthLookback, 0) 710 weightMonthStart := uint(weightStart.Month()) 711 weightYearStart := uint(weightStart.Year()) 712 713 // Subtract one nano second from start date and add one to end date to avoid having equal times. 714 startDate := time.Date(int(weightYearStart), time.Month(weightMonthStart), 0, 0, 0, 0, -1, time.UTC) 715 endDate := time.Date(int(weightYearEnd), time.Month(weightMonthEnd), 0, 0, 0, 0, 1, time.UTC) 716 717 err := p.db.AllUsers(func(user *user.User) { 718 cmsUser, err := p.getCMSUserByID(user.ID.String()) 719 if err != nil { 720 log.Errorf("getCMSUserWeights: getCMSUserByID %v %v", 721 user.ID.String(), err) 722 return 723 } 724 725 if cmsUser.ContractorType != cms.ContractorTypeDirect && 726 cmsUser.ContractorType != cms.ContractorTypeSubContractor && 727 cmsUser.ContractorType != cms.ContractorTypeSupervisor { 728 return 729 } 730 731 var billedMinutes int64 732 // Calculate sub contractor weight here 733 if cmsUser.ContractorType == cms.ContractorTypeSubContractor { 734 for _, superID := range cmsUser.SupervisorUserIDs { 735 superUser, err := p.getCMSUserByID(superID) 736 if err != nil { 737 log.Errorf("getCMSUserWeights: getCMSUserByID %v %v", 738 superID, err) 739 return 740 } 741 superInvoices, err := p.cmsDB.InvoicesByUserID(superUser.ID) 742 if err != nil { 743 log.Errorf("getCMSUserWeights: InvoicesByUserID %v", err) 744 } 745 for _, i := range superInvoices { 746 invoiceDate := time.Date(int(i.Year), time.Month(i.Month), 0, 0, 0, 0, 0, time.UTC) 747 if invoiceDate.After(startDate) && endDate.After(invoiceDate) { 748 for _, li := range i.LineItems { 749 // Only take into account billed minutes if the line 750 // item matches their userID 751 if li.Type == cms.LineItemTypeSubHours && 752 li.SubUserID == user.ID.String() { 753 billedMinutes += int64(li.Labor) 754 } 755 } 756 } 757 } 758 } 759 } else { 760 userInvoices, err := p.cmsDB.InvoicesByUserID(cmsUser.ID) 761 if err != nil { 762 log.Errorf("getCMSUserWeights: InvoicesByUserID %v", err) 763 return 764 } 765 for _, i := range userInvoices { 766 invoiceDate := time.Date(int(i.Year), time.Month(i.Month), 0, 0, 0, 0, 0, time.UTC) 767 if invoiceDate.After(startDate) && endDate.After(invoiceDate) { 768 // now look at the lineitems within that invoice and 769 // tabulate billed hours 770 for _, li := range i.LineItems { 771 billedMinutes += int64(li.Labor) 772 } 773 } 774 } 775 } 776 userWeights[cmsUser.ID] = billedMinutes 777 }) 778 if err != nil { 779 return nil, err 780 } 781 782 return userWeights, nil 783 784 } 785 786 // convertCMSUserFromDatabaseUser converts a user User to a cms User. 787 func convertCMSUserFromDatabaseUser(user *user.CMSUser) cms.User { 788 superUserIDs := make([]string, 0, len(user.SupervisorUserIDs)) 789 for _, userIDs := range user.SupervisorUserIDs { 790 superUserIDs = append(superUserIDs, userIDs.String()) 791 } 792 return cms.User{ 793 ID: user.User.ID.String(), 794 Admin: user.User.Admin, 795 Email: user.User.Email, 796 Username: user.User.Username, 797 NewUserVerificationToken: user.User.NewUserVerificationToken, 798 NewUserVerificationExpiry: user.User.NewUserVerificationExpiry, 799 UpdateKeyVerificationToken: user.User.UpdateKeyVerificationToken, 800 UpdateKeyVerificationExpiry: user.User.UpdateKeyVerificationExpiry, 801 ResetPasswordVerificationToken: user.User.ResetPasswordVerificationToken, 802 ResetPasswordVerificationExpiry: user.User.ResetPasswordVerificationExpiry, 803 LastLoginTime: user.User.LastLoginTime, 804 FailedLoginAttempts: user.User.FailedLoginAttempts, 805 Deactivated: user.User.Deactivated, 806 Locked: userIsLocked(user.User.FailedLoginAttempts), 807 Identities: convertWWWIdentitiesFromDatabaseIdentities(user.User.Identities), 808 EmailNotifications: user.User.EmailNotifications, 809 Domain: cms.DomainTypeT(user.Domain), 810 ContractorType: cms.ContractorTypeT(user.ContractorType), 811 ContractorName: user.ContractorName, 812 ContractorLocation: user.ContractorLocation, 813 ContractorContact: user.ContractorContact, 814 MatrixName: user.MatrixName, 815 GitHubName: user.GitHubName, 816 SupervisorUserIDs: superUserIDs, 817 ProposalsOwned: user.ProposalsOwned, 818 } 819 } 820 821 // issuanceDCCUser does the processing to move a nominated user to a fully 822 // approved and invite them onto CMS. 823 func (p *Politeiawww) issuanceDCCUser(userid, sponsorUserID string, domain, contractorType int) error { 824 nominatedUser, err := p.userByIDStr(userid) 825 if err != nil { 826 return err 827 } 828 829 if nominatedUser == nil { 830 return err 831 } 832 833 nomineeUserID, err := uuid.Parse(userid) 834 if err != nil { 835 return err 836 } 837 uu := user.UpdateCMSUser{ 838 ID: nomineeUserID, 839 ContractorType: contractorType, 840 Domain: domain, 841 } 842 843 // If the nominee was an approved Subcontractor, then use the sponsor user 844 // ID as the SupervisorUserID 845 superVisorUserIDs := make([]uuid.UUID, 1) 846 if contractorType == int(cms.ContractorTypeSubContractor) { 847 parsed, err := uuid.Parse(sponsorUserID) 848 if err != nil { 849 return err 850 } 851 superVisorUserIDs[0] = parsed 852 uu.SupervisorUserIDs = superVisorUserIDs 853 } 854 855 payload, err := user.EncodeUpdateCMSUser(uu) 856 if err != nil { 857 return err 858 } 859 pc := user.PluginCommand{ 860 ID: user.CMSPluginID, 861 Command: user.CmdUpdateCMSUser, 862 Payload: string(payload), 863 } 864 _, err = p.db.PluginExec(pc) 865 if err != nil { 866 return err 867 } 868 869 // Try to email the verification link first; if it fails, then 870 // the new user won't be created. 871 // 872 // This is conditional on the email server being setup. 873 err = p.emailUserDCCApproved(nominatedUser.Email) 874 if err != nil { 875 log.Errorf("processApproveDCC: verification email "+ 876 "failed for '%v': %v", nominatedUser.Email, err) 877 return err 878 } 879 880 return nil 881 } 882 883 func (p *Politeiawww) revokeDCCUser(userid string) error { 884 // Do full userdb update and reject user creds 885 nomineeUserID, err := uuid.Parse(userid) 886 if err != nil { 887 return err 888 } 889 uu := user.UpdateCMSUser{ 890 ID: nomineeUserID, 891 ContractorType: int(cms.ContractorTypeRevoked), 892 } 893 payload, err := user.EncodeUpdateCMSUser(uu) 894 if err != nil { 895 return err 896 } 897 pc := user.PluginCommand{ 898 ID: user.CMSPluginID, 899 Command: user.CmdUpdateCMSUser, 900 Payload: string(payload), 901 } 902 _, err = p.db.PluginExec(pc) 903 if err != nil { 904 return err 905 } 906 return nil 907 } 908 909 func (p *Politeiawww) processUserSubContractors(u *user.User) (*cms.UserSubContractorsReply, error) { 910 usc := user.CMSUserSubContractors{ 911 ID: u.ID.String(), 912 } 913 payload, err := user.EncodeCMSUserSubContractors(usc) 914 if err != nil { 915 return nil, err 916 } 917 pc := user.PluginCommand{ 918 ID: user.CMSPluginID, 919 Command: user.CmdCMSUserSubContractors, 920 Payload: string(payload), 921 } 922 payloadReply, err := p.db.PluginExec(pc) 923 if err != nil { 924 return nil, err 925 } 926 cmsUsers, err := user.DecodeCMSUserSubContractorsReply([]byte(payloadReply.Payload)) 927 if err != nil { 928 return nil, err 929 } 930 convertedCMSUsers := make([]cms.User, 0, len(cmsUsers.Users)) 931 for _, uu := range cmsUsers.Users { 932 converted := convertCMSUserFromDatabaseUser(&uu) 933 convertedCMSUsers = append(convertedCMSUsers, converted) 934 } 935 uscr := &cms.UserSubContractorsReply{ 936 Users: convertedCMSUsers, 937 } 938 return uscr, nil 939 } 940 941 // processProposalOwner returns a list of cms users given a proposal token. 942 // If the user is set to have ownership of this proposal then they will be 943 // added to the list. 944 func (p *Politeiawww) processProposalOwner(po cms.ProposalOwner) (*cms.ProposalOwnerReply, error) { 945 log.Tracef("processProposalOwner") 946 947 // Setup plugin command 948 cupt := user.CMSUsersByProposalToken{ 949 Token: po.ProposalToken, 950 } 951 payload, err := user.EncodeCMSUsersByProposalToken(cupt) 952 if err != nil { 953 return nil, err 954 } 955 pc := user.PluginCommand{ 956 ID: user.CMSPluginID, 957 Command: user.CmdCMSUsersByProposalToken, 958 Payload: string(payload), 959 } 960 961 // Execute plugin command 962 pcr, err := p.db.PluginExec(pc) 963 if err != nil { 964 return nil, err 965 } 966 967 // Decode reply 968 reply, err := user.DecodeCMSUsersByProposalTokenReply([]byte(pcr.Payload)) 969 if err != nil { 970 return nil, err 971 } 972 973 matchedUsers := make([]cms.AbridgedCMSUser, 0, len(reply.Users)) 974 for _, u := range reply.Users { 975 matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{ 976 ID: u.ID.String(), 977 Username: u.Username, 978 Domain: cms.DomainTypeT(u.Domain), 979 ContractorType: cms.ContractorTypeT(u.ContractorType), 980 }) 981 } 982 // Sort results alphabetically. 983 sort.Slice(matchedUsers, func(i, j int) bool { 984 return matchedUsers[i].Username < matchedUsers[j].Username 985 }) 986 987 return &cms.ProposalOwnerReply{ 988 Users: matchedUsers, 989 }, nil 990 }