code.gitea.io/gitea@v1.22.3/routers/web/org/teams.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package org 6 7 import ( 8 "errors" 9 "fmt" 10 "net/http" 11 "net/url" 12 "path" 13 "strconv" 14 "strings" 15 16 "code.gitea.io/gitea/models" 17 "code.gitea.io/gitea/models/db" 18 org_model "code.gitea.io/gitea/models/organization" 19 "code.gitea.io/gitea/models/perm" 20 repo_model "code.gitea.io/gitea/models/repo" 21 unit_model "code.gitea.io/gitea/models/unit" 22 user_model "code.gitea.io/gitea/models/user" 23 "code.gitea.io/gitea/modules/base" 24 "code.gitea.io/gitea/modules/log" 25 "code.gitea.io/gitea/modules/setting" 26 "code.gitea.io/gitea/modules/web" 27 shared_user "code.gitea.io/gitea/routers/web/shared/user" 28 "code.gitea.io/gitea/services/context" 29 "code.gitea.io/gitea/services/convert" 30 "code.gitea.io/gitea/services/forms" 31 org_service "code.gitea.io/gitea/services/org" 32 repo_service "code.gitea.io/gitea/services/repository" 33 ) 34 35 const ( 36 // tplTeams template path for teams list page 37 tplTeams base.TplName = "org/team/teams" 38 // tplTeamNew template path for create new team page 39 tplTeamNew base.TplName = "org/team/new" 40 // tplTeamMembers template path for showing team members page 41 tplTeamMembers base.TplName = "org/team/members" 42 // tplTeamRepositories template path for showing team repositories page 43 tplTeamRepositories base.TplName = "org/team/repositories" 44 // tplTeamInvite template path for team invites page 45 tplTeamInvite base.TplName = "org/team/invite" 46 ) 47 48 // Teams render teams list page 49 func Teams(ctx *context.Context) { 50 org := ctx.Org.Organization 51 ctx.Data["Title"] = org.FullName 52 ctx.Data["PageIsOrgTeams"] = true 53 54 for _, t := range ctx.Org.Teams { 55 if err := t.LoadMembers(ctx); err != nil { 56 ctx.ServerError("GetMembers", err) 57 return 58 } 59 } 60 ctx.Data["Teams"] = ctx.Org.Teams 61 62 err := shared_user.LoadHeaderCount(ctx) 63 if err != nil { 64 ctx.ServerError("LoadHeaderCount", err) 65 return 66 } 67 68 ctx.HTML(http.StatusOK, tplTeams) 69 } 70 71 // TeamsAction response for join, leave, remove, add operations to team 72 func TeamsAction(ctx *context.Context) { 73 page := ctx.FormString("page") 74 var err error 75 switch ctx.Params(":action") { 76 case "join": 77 if !ctx.Org.IsOwner { 78 ctx.Error(http.StatusNotFound) 79 return 80 } 81 err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer) 82 case "leave": 83 err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer) 84 if err != nil { 85 if org_model.IsErrLastOrgOwner(err) { 86 ctx.Flash.Error(ctx.Tr("form.last_org_owner")) 87 } else { 88 log.Error("Action(%s): %v", ctx.Params(":action"), err) 89 ctx.JSON(http.StatusOK, map[string]any{ 90 "ok": false, 91 "err": err.Error(), 92 }) 93 return 94 } 95 } 96 checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/") 97 return 98 case "remove": 99 if !ctx.Org.IsOwner { 100 ctx.Error(http.StatusNotFound) 101 return 102 } 103 104 user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid")) 105 if user == nil { 106 ctx.Redirect(ctx.Org.OrgLink + "/teams") 107 return 108 } 109 110 err = models.RemoveTeamMember(ctx, ctx.Org.Team, user) 111 if err != nil { 112 if org_model.IsErrLastOrgOwner(err) { 113 ctx.Flash.Error(ctx.Tr("form.last_org_owner")) 114 } else { 115 log.Error("Action(%s): %v", ctx.Params(":action"), err) 116 ctx.JSON(http.StatusOK, map[string]any{ 117 "ok": false, 118 "err": err.Error(), 119 }) 120 return 121 } 122 } 123 checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/"+url.PathEscape(ctx.Org.Team.LowerName)) 124 return 125 case "add": 126 if !ctx.Org.IsOwner { 127 ctx.Error(http.StatusNotFound) 128 return 129 } 130 uname := strings.ToLower(ctx.FormString("uname")) 131 var u *user_model.User 132 u, err = user_model.GetUserByName(ctx, uname) 133 if err != nil { 134 if user_model.IsErrUserNotExist(err) { 135 if setting.MailService != nil && user_model.ValidateEmail(uname) == nil { 136 if err := org_service.CreateTeamInvite(ctx, ctx.Doer, ctx.Org.Team, uname); err != nil { 137 if org_model.IsErrTeamInviteAlreadyExist(err) { 138 ctx.Flash.Error(ctx.Tr("form.duplicate_invite_to_team")) 139 } else if org_model.IsErrUserEmailAlreadyAdded(err) { 140 ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users")) 141 } else { 142 ctx.ServerError("CreateTeamInvite", err) 143 return 144 } 145 } 146 } else { 147 ctx.Flash.Error(ctx.Tr("form.user_not_exist")) 148 } 149 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) 150 } else { 151 ctx.ServerError("GetUserByName", err) 152 } 153 return 154 } 155 156 if u.IsOrganization() { 157 ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team")) 158 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) 159 return 160 } 161 162 if ctx.Org.Team.IsMember(ctx, u.ID) { 163 ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users")) 164 } else { 165 err = models.AddTeamMember(ctx, ctx.Org.Team, u) 166 } 167 168 page = "team" 169 case "remove_invite": 170 if !ctx.Org.IsOwner { 171 ctx.Error(http.StatusNotFound) 172 return 173 } 174 175 iid := ctx.FormInt64("iid") 176 if iid == 0 { 177 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) 178 return 179 } 180 181 if err := org_model.RemoveInviteByID(ctx, iid, ctx.Org.Team.ID); err != nil { 182 log.Error("Action(%s): %v", ctx.Params(":action"), err) 183 ctx.ServerError("RemoveInviteByID", err) 184 return 185 } 186 187 page = "team" 188 } 189 190 if err != nil { 191 if org_model.IsErrLastOrgOwner(err) { 192 ctx.Flash.Error(ctx.Tr("form.last_org_owner")) 193 } else if errors.Is(err, user_model.ErrBlockedUser) { 194 ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user")) 195 } else { 196 log.Error("Action(%s): %v", ctx.Params(":action"), err) 197 ctx.JSON(http.StatusOK, map[string]any{ 198 "ok": false, 199 "err": err.Error(), 200 }) 201 return 202 } 203 } 204 205 switch page { 206 case "team": 207 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) 208 case "home": 209 ctx.Redirect(ctx.Org.Organization.AsUser().HomeLink()) 210 default: 211 ctx.Redirect(ctx.Org.OrgLink + "/teams") 212 } 213 } 214 215 func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) { 216 if isOrgMember, err := org_model.IsOrganizationMember(ctx, ctx.Org.Organization.ID, ctx.Doer.ID); err != nil { 217 ctx.ServerError("IsOrganizationMember", err) 218 return 219 } else if !isOrgMember { 220 if ctx.Org.Organization.Visibility.IsPrivate() { 221 defaultRedirect = setting.AppSubURL + "/" 222 } else { 223 defaultRedirect = ctx.Org.Organization.HomeLink() 224 } 225 } 226 ctx.JSONRedirect(defaultRedirect) 227 } 228 229 // TeamsRepoAction operate team's repository 230 func TeamsRepoAction(ctx *context.Context) { 231 if !ctx.Org.IsOwner { 232 ctx.Error(http.StatusNotFound) 233 return 234 } 235 236 var err error 237 action := ctx.Params(":action") 238 switch action { 239 case "add": 240 repoName := path.Base(ctx.FormString("repo_name")) 241 var repo *repo_model.Repository 242 repo, err = repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, repoName) 243 if err != nil { 244 if repo_model.IsErrRepoNotExist(err) { 245 ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo")) 246 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") 247 return 248 } 249 ctx.ServerError("GetRepositoryByName", err) 250 return 251 } 252 err = org_service.TeamAddRepository(ctx, ctx.Org.Team, repo) 253 case "remove": 254 err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, ctx.FormInt64("repoid")) 255 case "addall": 256 err = models.AddAllRepositories(ctx, ctx.Org.Team) 257 case "removeall": 258 err = models.RemoveAllRepositories(ctx, ctx.Org.Team) 259 } 260 261 if err != nil { 262 log.Error("Action(%s): '%s' %v", ctx.Params(":action"), ctx.Org.Team.Name, err) 263 ctx.ServerError("TeamsRepoAction", err) 264 return 265 } 266 267 if action == "addall" || action == "removeall" { 268 ctx.JSONRedirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") 269 return 270 } 271 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") 272 } 273 274 // NewTeam render create new team page 275 func NewTeam(ctx *context.Context) { 276 ctx.Data["Title"] = ctx.Org.Organization.FullName 277 ctx.Data["PageIsOrgTeams"] = true 278 ctx.Data["PageIsOrgTeamsNew"] = true 279 ctx.Data["Team"] = &org_model.Team{} 280 ctx.Data["Units"] = unit_model.Units 281 if err := shared_user.LoadHeaderCount(ctx); err != nil { 282 ctx.ServerError("LoadHeaderCount", err) 283 return 284 } 285 ctx.HTML(http.StatusOK, tplTeamNew) 286 } 287 288 func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode { 289 unitPerms := make(map[unit_model.Type]perm.AccessMode) 290 for _, ut := range unit_model.AllRepoUnitTypes { 291 // Default accessmode is none 292 unitPerms[ut] = perm.AccessModeNone 293 294 v, ok := forms[fmt.Sprintf("unit_%d", ut)] 295 if ok { 296 vv, _ := strconv.Atoi(v[0]) 297 if teamPermission >= perm.AccessModeAdmin { 298 unitPerms[ut] = teamPermission 299 // Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms. 300 if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki { 301 unitPerms[ut] = perm.AccessModeRead 302 } 303 } else { 304 unitPerms[ut] = perm.AccessMode(vv) 305 if unitPerms[ut] >= perm.AccessModeAdmin { 306 unitPerms[ut] = perm.AccessModeWrite 307 } 308 } 309 } 310 } 311 return unitPerms 312 } 313 314 // NewTeamPost response for create new team 315 func NewTeamPost(ctx *context.Context) { 316 form := web.GetForm(ctx).(*forms.CreateTeamForm) 317 includesAllRepositories := form.RepoAccess == "all" 318 p := perm.ParseAccessMode(form.Permission) 319 unitPerms := getUnitPerms(ctx.Req.Form, p) 320 if p < perm.AccessModeAdmin { 321 // if p is less than admin accessmode, then it should be general accessmode, 322 // so we should calculate the minial accessmode from units accessmodes. 323 p = unit_model.MinUnitAccessMode(unitPerms) 324 } 325 326 t := &org_model.Team{ 327 OrgID: ctx.Org.Organization.ID, 328 Name: form.TeamName, 329 Description: form.Description, 330 AccessMode: p, 331 IncludesAllRepositories: includesAllRepositories, 332 CanCreateOrgRepo: form.CanCreateOrgRepo, 333 } 334 335 units := make([]*org_model.TeamUnit, 0, len(unitPerms)) 336 for tp, perm := range unitPerms { 337 units = append(units, &org_model.TeamUnit{ 338 OrgID: ctx.Org.Organization.ID, 339 Type: tp, 340 AccessMode: perm, 341 }) 342 } 343 t.Units = units 344 345 ctx.Data["Title"] = ctx.Org.Organization.FullName 346 ctx.Data["PageIsOrgTeams"] = true 347 ctx.Data["PageIsOrgTeamsNew"] = true 348 ctx.Data["Units"] = unit_model.Units 349 ctx.Data["Team"] = t 350 351 if ctx.HasError() { 352 ctx.HTML(http.StatusOK, tplTeamNew) 353 return 354 } 355 356 if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { 357 ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) 358 return 359 } 360 361 if err := models.NewTeam(ctx, t); err != nil { 362 ctx.Data["Err_TeamName"] = true 363 switch { 364 case org_model.IsErrTeamAlreadyExist(err): 365 ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) 366 default: 367 ctx.ServerError("NewTeam", err) 368 } 369 return 370 } 371 log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name) 372 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) 373 } 374 375 // TeamMembers render team members page 376 func TeamMembers(ctx *context.Context) { 377 ctx.Data["Title"] = ctx.Org.Team.Name 378 ctx.Data["PageIsOrgTeams"] = true 379 ctx.Data["PageIsOrgTeamMembers"] = true 380 381 if err := shared_user.LoadHeaderCount(ctx); err != nil { 382 ctx.ServerError("LoadHeaderCount", err) 383 return 384 } 385 386 if err := ctx.Org.Team.LoadMembers(ctx); err != nil { 387 ctx.ServerError("GetMembers", err) 388 return 389 } 390 ctx.Data["Units"] = unit_model.Units 391 392 invites, err := org_model.GetInvitesByTeamID(ctx, ctx.Org.Team.ID) 393 if err != nil { 394 ctx.ServerError("GetInvitesByTeamID", err) 395 return 396 } 397 ctx.Data["Invites"] = invites 398 ctx.Data["IsEmailInviteEnabled"] = setting.MailService != nil 399 400 ctx.HTML(http.StatusOK, tplTeamMembers) 401 } 402 403 // TeamRepositories show the repositories of team 404 func TeamRepositories(ctx *context.Context) { 405 ctx.Data["Title"] = ctx.Org.Team.Name 406 ctx.Data["PageIsOrgTeams"] = true 407 ctx.Data["PageIsOrgTeamRepos"] = true 408 409 if err := shared_user.LoadHeaderCount(ctx); err != nil { 410 ctx.ServerError("LoadHeaderCount", err) 411 return 412 } 413 414 if err := ctx.Org.Team.LoadRepositories(ctx); err != nil { 415 ctx.ServerError("GetRepositories", err) 416 return 417 } 418 ctx.Data["Units"] = unit_model.Units 419 ctx.HTML(http.StatusOK, tplTeamRepositories) 420 } 421 422 // SearchTeam api for searching teams 423 func SearchTeam(ctx *context.Context) { 424 listOptions := db.ListOptions{ 425 Page: ctx.FormInt("page"), 426 PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), 427 } 428 429 opts := &org_model.SearchTeamOptions{ 430 // UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in 431 Keyword: ctx.FormTrim("q"), 432 OrgID: ctx.Org.Organization.ID, 433 IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"), 434 ListOptions: listOptions, 435 } 436 437 teams, maxResults, err := org_model.SearchTeam(ctx, opts) 438 if err != nil { 439 log.Error("SearchTeam failed: %v", err) 440 ctx.JSON(http.StatusInternalServerError, map[string]any{ 441 "ok": false, 442 "error": "SearchTeam internal failure", 443 }) 444 return 445 } 446 447 apiTeams, err := convert.ToTeams(ctx, teams, false) 448 if err != nil { 449 log.Error("convert ToTeams failed: %v", err) 450 ctx.JSON(http.StatusInternalServerError, map[string]any{ 451 "ok": false, 452 "error": "SearchTeam failed to get units", 453 }) 454 return 455 } 456 457 ctx.SetTotalCountHeader(maxResults) 458 ctx.JSON(http.StatusOK, map[string]any{ 459 "ok": true, 460 "data": apiTeams, 461 }) 462 } 463 464 // EditTeam render team edit page 465 func EditTeam(ctx *context.Context) { 466 ctx.Data["Title"] = ctx.Org.Organization.FullName 467 ctx.Data["PageIsOrgTeams"] = true 468 if err := ctx.Org.Team.LoadUnits(ctx); err != nil { 469 ctx.ServerError("LoadUnits", err) 470 return 471 } 472 if err := shared_user.LoadHeaderCount(ctx); err != nil { 473 ctx.ServerError("LoadHeaderCount", err) 474 return 475 } 476 ctx.Data["Team"] = ctx.Org.Team 477 ctx.Data["Units"] = unit_model.Units 478 ctx.HTML(http.StatusOK, tplTeamNew) 479 } 480 481 // EditTeamPost response for modify team information 482 func EditTeamPost(ctx *context.Context) { 483 form := web.GetForm(ctx).(*forms.CreateTeamForm) 484 t := ctx.Org.Team 485 newAccessMode := perm.ParseAccessMode(form.Permission) 486 unitPerms := getUnitPerms(ctx.Req.Form, newAccessMode) 487 if newAccessMode < perm.AccessModeAdmin { 488 // if newAccessMode is less than admin accessmode, then it should be general accessmode, 489 // so we should calculate the minial accessmode from units accessmodes. 490 newAccessMode = unit_model.MinUnitAccessMode(unitPerms) 491 } 492 isAuthChanged := false 493 isIncludeAllChanged := false 494 includesAllRepositories := form.RepoAccess == "all" 495 496 ctx.Data["Title"] = ctx.Org.Organization.FullName 497 ctx.Data["PageIsOrgTeams"] = true 498 ctx.Data["Team"] = t 499 ctx.Data["Units"] = unit_model.Units 500 501 if !t.IsOwnerTeam() { 502 t.Name = form.TeamName 503 if t.AccessMode != newAccessMode { 504 isAuthChanged = true 505 t.AccessMode = newAccessMode 506 } 507 508 if t.IncludesAllRepositories != includesAllRepositories { 509 isIncludeAllChanged = true 510 t.IncludesAllRepositories = includesAllRepositories 511 } 512 t.CanCreateOrgRepo = form.CanCreateOrgRepo 513 } else { 514 t.CanCreateOrgRepo = true 515 } 516 517 t.Description = form.Description 518 units := make([]*org_model.TeamUnit, 0, len(unitPerms)) 519 for tp, perm := range unitPerms { 520 units = append(units, &org_model.TeamUnit{ 521 OrgID: t.OrgID, 522 TeamID: t.ID, 523 Type: tp, 524 AccessMode: perm, 525 }) 526 } 527 t.Units = units 528 529 if ctx.HasError() { 530 ctx.HTML(http.StatusOK, tplTeamNew) 531 return 532 } 533 534 if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { 535 ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) 536 return 537 } 538 539 if err := models.UpdateTeam(ctx, t, isAuthChanged, isIncludeAllChanged); err != nil { 540 ctx.Data["Err_TeamName"] = true 541 switch { 542 case org_model.IsErrTeamAlreadyExist(err): 543 ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) 544 default: 545 ctx.ServerError("UpdateTeam", err) 546 } 547 return 548 } 549 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) 550 } 551 552 // DeleteTeam response for the delete team request 553 func DeleteTeam(ctx *context.Context) { 554 if err := models.DeleteTeam(ctx, ctx.Org.Team); err != nil { 555 ctx.Flash.Error("DeleteTeam: " + err.Error()) 556 } else { 557 ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success")) 558 } 559 560 ctx.JSONRedirect(ctx.Org.OrgLink + "/teams") 561 } 562 563 // TeamInvite renders the team invite page 564 func TeamInvite(ctx *context.Context) { 565 invite, org, team, inviter, err := getTeamInviteFromContext(ctx) 566 if err != nil { 567 if org_model.IsErrTeamInviteNotFound(err) { 568 ctx.NotFound("ErrTeamInviteNotFound", err) 569 } else { 570 ctx.ServerError("getTeamInviteFromContext", err) 571 } 572 return 573 } 574 575 ctx.Data["Title"] = ctx.Tr("org.teams.invite_team_member", team.Name) 576 ctx.Data["Invite"] = invite 577 ctx.Data["Organization"] = org 578 ctx.Data["Team"] = team 579 ctx.Data["Inviter"] = inviter 580 581 ctx.HTML(http.StatusOK, tplTeamInvite) 582 } 583 584 // TeamInvitePost handles the team invitation 585 func TeamInvitePost(ctx *context.Context) { 586 invite, org, team, _, err := getTeamInviteFromContext(ctx) 587 if err != nil { 588 if org_model.IsErrTeamInviteNotFound(err) { 589 ctx.NotFound("ErrTeamInviteNotFound", err) 590 } else { 591 ctx.ServerError("getTeamInviteFromContext", err) 592 } 593 return 594 } 595 596 if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil { 597 ctx.ServerError("AddTeamMember", err) 598 return 599 } 600 601 if err := org_model.RemoveInviteByID(ctx, invite.ID, team.ID); err != nil { 602 log.Error("RemoveInviteByID: %v", err) 603 } 604 605 ctx.Redirect(org.OrganisationLink() + "/teams/" + url.PathEscape(team.LowerName)) 606 } 607 608 func getTeamInviteFromContext(ctx *context.Context) (*org_model.TeamInvite, *org_model.Organization, *org_model.Team, *user_model.User, error) { 609 invite, err := org_model.GetInviteByToken(ctx, ctx.Params("token")) 610 if err != nil { 611 return nil, nil, nil, nil, err 612 } 613 614 inviter, err := user_model.GetUserByID(ctx, invite.InviterID) 615 if err != nil { 616 return nil, nil, nil, nil, err 617 } 618 619 team, err := org_model.GetTeamByID(ctx, invite.TeamID) 620 if err != nil { 621 return nil, nil, nil, nil, err 622 } 623 624 org, err := user_model.GetUserByID(ctx, team.OrgID) 625 if err != nil { 626 return nil, nil, nil, nil, err 627 } 628 629 return invite, org_model.OrgFromUser(org), team, inviter, nil 630 }