code.gitea.io/gitea@v1.21.7/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 "fmt" 9 "net/http" 10 "net/url" 11 "path" 12 "strconv" 13 "strings" 14 15 "code.gitea.io/gitea/models" 16 "code.gitea.io/gitea/models/db" 17 org_model "code.gitea.io/gitea/models/organization" 18 "code.gitea.io/gitea/models/perm" 19 repo_model "code.gitea.io/gitea/models/repo" 20 unit_model "code.gitea.io/gitea/models/unit" 21 user_model "code.gitea.io/gitea/models/user" 22 "code.gitea.io/gitea/modules/base" 23 "code.gitea.io/gitea/modules/context" 24 "code.gitea.io/gitea/modules/log" 25 "code.gitea.io/gitea/modules/setting" 26 "code.gitea.io/gitea/modules/web" 27 "code.gitea.io/gitea/routers/utils" 28 shared_user "code.gitea.io/gitea/routers/web/shared/user" 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.ID) 82 case "leave": 83 err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID) 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 uid := ctx.FormInt64("uid") 105 if uid == 0 { 106 ctx.Redirect(ctx.Org.OrgLink + "/teams") 107 return 108 } 109 110 err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid) 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 := utils.RemoveUsernameParameterSuffix(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(u.ID) { 163 ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users")) 164 } else { 165 err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID) 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 { 194 log.Error("Action(%s): %v", ctx.Params(":action"), err) 195 ctx.JSON(http.StatusOK, map[string]any{ 196 "ok": false, 197 "err": err.Error(), 198 }) 199 return 200 } 201 } 202 203 switch page { 204 case "team": 205 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) 206 case "home": 207 ctx.Redirect(ctx.Org.Organization.AsUser().HomeLink()) 208 default: 209 ctx.Redirect(ctx.Org.OrgLink + "/teams") 210 } 211 } 212 213 func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) { 214 if isOrgMember, err := org_model.IsOrganizationMember(ctx, ctx.Org.Organization.ID, ctx.Doer.ID); err != nil { 215 ctx.ServerError("IsOrganizationMember", err) 216 return 217 } else if !isOrgMember { 218 if ctx.Org.Organization.Visibility.IsPrivate() { 219 defaultRedirect = setting.AppSubURL + "/" 220 } else { 221 defaultRedirect = ctx.Org.Organization.HomeLink() 222 } 223 } 224 ctx.JSONRedirect(defaultRedirect) 225 } 226 227 // TeamsRepoAction operate team's repository 228 func TeamsRepoAction(ctx *context.Context) { 229 if !ctx.Org.IsOwner { 230 ctx.Error(http.StatusNotFound) 231 return 232 } 233 234 var err error 235 action := ctx.Params(":action") 236 switch action { 237 case "add": 238 repoName := path.Base(ctx.FormString("repo_name")) 239 var repo *repo_model.Repository 240 repo, err = repo_model.GetRepositoryByName(ctx.Org.Organization.ID, repoName) 241 if err != nil { 242 if repo_model.IsErrRepoNotExist(err) { 243 ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo")) 244 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") 245 return 246 } 247 ctx.ServerError("GetRepositoryByName", err) 248 return 249 } 250 err = org_service.TeamAddRepository(ctx, ctx.Org.Team, repo) 251 case "remove": 252 err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, ctx.FormInt64("repoid")) 253 case "addall": 254 err = models.AddAllRepositories(ctx, ctx.Org.Team) 255 case "removeall": 256 err = models.RemoveAllRepositories(ctx, ctx.Org.Team) 257 } 258 259 if err != nil { 260 log.Error("Action(%s): '%s' %v", ctx.Params(":action"), ctx.Org.Team.Name, err) 261 ctx.ServerError("TeamsRepoAction", err) 262 return 263 } 264 265 if action == "addall" || action == "removeall" { 266 ctx.JSONRedirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") 267 return 268 } 269 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") 270 } 271 272 // NewTeam render create new team page 273 func NewTeam(ctx *context.Context) { 274 ctx.Data["Title"] = ctx.Org.Organization.FullName 275 ctx.Data["PageIsOrgTeams"] = true 276 ctx.Data["PageIsOrgTeamsNew"] = true 277 ctx.Data["Team"] = &org_model.Team{} 278 ctx.Data["Units"] = unit_model.Units 279 ctx.HTML(http.StatusOK, tplTeamNew) 280 } 281 282 func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode { 283 unitPerms := make(map[unit_model.Type]perm.AccessMode) 284 for _, ut := range unit_model.AllRepoUnitTypes { 285 // Default accessmode is none 286 unitPerms[ut] = perm.AccessModeNone 287 288 v, ok := forms[fmt.Sprintf("unit_%d", ut)] 289 if ok { 290 vv, _ := strconv.Atoi(v[0]) 291 if teamPermission >= perm.AccessModeAdmin { 292 unitPerms[ut] = teamPermission 293 // Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms. 294 if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki { 295 unitPerms[ut] = perm.AccessModeRead 296 } 297 } else { 298 unitPerms[ut] = perm.AccessMode(vv) 299 if unitPerms[ut] >= perm.AccessModeAdmin { 300 unitPerms[ut] = perm.AccessModeWrite 301 } 302 } 303 } 304 } 305 return unitPerms 306 } 307 308 // NewTeamPost response for create new team 309 func NewTeamPost(ctx *context.Context) { 310 form := web.GetForm(ctx).(*forms.CreateTeamForm) 311 includesAllRepositories := form.RepoAccess == "all" 312 p := perm.ParseAccessMode(form.Permission) 313 unitPerms := getUnitPerms(ctx.Req.Form, p) 314 if p < perm.AccessModeAdmin { 315 // if p is less than admin accessmode, then it should be general accessmode, 316 // so we should calculate the minial accessmode from units accessmodes. 317 p = unit_model.MinUnitAccessMode(unitPerms) 318 } 319 320 t := &org_model.Team{ 321 OrgID: ctx.Org.Organization.ID, 322 Name: form.TeamName, 323 Description: form.Description, 324 AccessMode: p, 325 IncludesAllRepositories: includesAllRepositories, 326 CanCreateOrgRepo: form.CanCreateOrgRepo, 327 } 328 329 units := make([]*org_model.TeamUnit, 0, len(unitPerms)) 330 for tp, perm := range unitPerms { 331 units = append(units, &org_model.TeamUnit{ 332 OrgID: ctx.Org.Organization.ID, 333 Type: tp, 334 AccessMode: perm, 335 }) 336 } 337 t.Units = units 338 339 ctx.Data["Title"] = ctx.Org.Organization.FullName 340 ctx.Data["PageIsOrgTeams"] = true 341 ctx.Data["PageIsOrgTeamsNew"] = true 342 ctx.Data["Units"] = unit_model.Units 343 ctx.Data["Team"] = t 344 345 if ctx.HasError() { 346 ctx.HTML(http.StatusOK, tplTeamNew) 347 return 348 } 349 350 if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { 351 ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) 352 return 353 } 354 355 if err := models.NewTeam(ctx, t); err != nil { 356 ctx.Data["Err_TeamName"] = true 357 switch { 358 case org_model.IsErrTeamAlreadyExist(err): 359 ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) 360 default: 361 ctx.ServerError("NewTeam", err) 362 } 363 return 364 } 365 log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name) 366 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) 367 } 368 369 // TeamMembers render team members page 370 func TeamMembers(ctx *context.Context) { 371 ctx.Data["Title"] = ctx.Org.Team.Name 372 ctx.Data["PageIsOrgTeams"] = true 373 ctx.Data["PageIsOrgTeamMembers"] = true 374 375 if err := shared_user.LoadHeaderCount(ctx); err != nil { 376 ctx.ServerError("LoadHeaderCount", err) 377 return 378 } 379 380 if err := ctx.Org.Team.LoadMembers(ctx); err != nil { 381 ctx.ServerError("GetMembers", err) 382 return 383 } 384 ctx.Data["Units"] = unit_model.Units 385 386 invites, err := org_model.GetInvitesByTeamID(ctx, ctx.Org.Team.ID) 387 if err != nil { 388 ctx.ServerError("GetInvitesByTeamID", err) 389 return 390 } 391 ctx.Data["Invites"] = invites 392 ctx.Data["IsEmailInviteEnabled"] = setting.MailService != nil 393 394 ctx.HTML(http.StatusOK, tplTeamMembers) 395 } 396 397 // TeamRepositories show the repositories of team 398 func TeamRepositories(ctx *context.Context) { 399 ctx.Data["Title"] = ctx.Org.Team.Name 400 ctx.Data["PageIsOrgTeams"] = true 401 ctx.Data["PageIsOrgTeamRepos"] = true 402 403 if err := shared_user.LoadHeaderCount(ctx); err != nil { 404 ctx.ServerError("LoadHeaderCount", err) 405 return 406 } 407 408 if err := ctx.Org.Team.LoadRepositories(ctx); err != nil { 409 ctx.ServerError("GetRepositories", err) 410 return 411 } 412 ctx.Data["Units"] = unit_model.Units 413 ctx.HTML(http.StatusOK, tplTeamRepositories) 414 } 415 416 // SearchTeam api for searching teams 417 func SearchTeam(ctx *context.Context) { 418 listOptions := db.ListOptions{ 419 Page: ctx.FormInt("page"), 420 PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), 421 } 422 423 opts := &org_model.SearchTeamOptions{ 424 // 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 425 Keyword: ctx.FormTrim("q"), 426 OrgID: ctx.Org.Organization.ID, 427 IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"), 428 ListOptions: listOptions, 429 } 430 431 teams, maxResults, err := org_model.SearchTeam(ctx, opts) 432 if err != nil { 433 log.Error("SearchTeam failed: %v", err) 434 ctx.JSON(http.StatusInternalServerError, map[string]any{ 435 "ok": false, 436 "error": "SearchTeam internal failure", 437 }) 438 return 439 } 440 441 apiTeams, err := convert.ToTeams(ctx, teams, false) 442 if err != nil { 443 log.Error("convert ToTeams failed: %v", err) 444 ctx.JSON(http.StatusInternalServerError, map[string]any{ 445 "ok": false, 446 "error": "SearchTeam failed to get units", 447 }) 448 return 449 } 450 451 ctx.SetTotalCountHeader(maxResults) 452 ctx.JSON(http.StatusOK, map[string]any{ 453 "ok": true, 454 "data": apiTeams, 455 }) 456 } 457 458 // EditTeam render team edit page 459 func EditTeam(ctx *context.Context) { 460 ctx.Data["Title"] = ctx.Org.Organization.FullName 461 ctx.Data["PageIsOrgTeams"] = true 462 if err := ctx.Org.Team.LoadUnits(ctx); err != nil { 463 ctx.ServerError("LoadUnits", err) 464 return 465 } 466 ctx.Data["Team"] = ctx.Org.Team 467 ctx.Data["Units"] = unit_model.Units 468 ctx.HTML(http.StatusOK, tplTeamNew) 469 } 470 471 // EditTeamPost response for modify team information 472 func EditTeamPost(ctx *context.Context) { 473 form := web.GetForm(ctx).(*forms.CreateTeamForm) 474 t := ctx.Org.Team 475 newAccessMode := perm.ParseAccessMode(form.Permission) 476 unitPerms := getUnitPerms(ctx.Req.Form, newAccessMode) 477 if newAccessMode < perm.AccessModeAdmin { 478 // if newAccessMode is less than admin accessmode, then it should be general accessmode, 479 // so we should calculate the minial accessmode from units accessmodes. 480 newAccessMode = unit_model.MinUnitAccessMode(unitPerms) 481 } 482 isAuthChanged := false 483 isIncludeAllChanged := false 484 includesAllRepositories := form.RepoAccess == "all" 485 486 ctx.Data["Title"] = ctx.Org.Organization.FullName 487 ctx.Data["PageIsOrgTeams"] = true 488 ctx.Data["Team"] = t 489 ctx.Data["Units"] = unit_model.Units 490 491 if !t.IsOwnerTeam() { 492 t.Name = form.TeamName 493 if t.AccessMode != newAccessMode { 494 isAuthChanged = true 495 t.AccessMode = newAccessMode 496 } 497 498 if t.IncludesAllRepositories != includesAllRepositories { 499 isIncludeAllChanged = true 500 t.IncludesAllRepositories = includesAllRepositories 501 } 502 t.CanCreateOrgRepo = form.CanCreateOrgRepo 503 } else { 504 t.CanCreateOrgRepo = true 505 } 506 507 t.Description = form.Description 508 units := make([]*org_model.TeamUnit, 0, len(unitPerms)) 509 for tp, perm := range unitPerms { 510 units = append(units, &org_model.TeamUnit{ 511 OrgID: t.OrgID, 512 TeamID: t.ID, 513 Type: tp, 514 AccessMode: perm, 515 }) 516 } 517 t.Units = units 518 519 if ctx.HasError() { 520 ctx.HTML(http.StatusOK, tplTeamNew) 521 return 522 } 523 524 if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { 525 ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) 526 return 527 } 528 529 if err := models.UpdateTeam(ctx, t, isAuthChanged, isIncludeAllChanged); err != nil { 530 ctx.Data["Err_TeamName"] = true 531 switch { 532 case org_model.IsErrTeamAlreadyExist(err): 533 ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) 534 default: 535 ctx.ServerError("UpdateTeam", err) 536 } 537 return 538 } 539 ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) 540 } 541 542 // DeleteTeam response for the delete team request 543 func DeleteTeam(ctx *context.Context) { 544 if err := models.DeleteTeam(ctx, ctx.Org.Team); err != nil { 545 ctx.Flash.Error("DeleteTeam: " + err.Error()) 546 } else { 547 ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success")) 548 } 549 550 ctx.JSONRedirect(ctx.Org.OrgLink + "/teams") 551 } 552 553 // TeamInvite renders the team invite page 554 func TeamInvite(ctx *context.Context) { 555 invite, org, team, inviter, err := getTeamInviteFromContext(ctx) 556 if err != nil { 557 if org_model.IsErrTeamInviteNotFound(err) { 558 ctx.NotFound("ErrTeamInviteNotFound", err) 559 } else { 560 ctx.ServerError("getTeamInviteFromContext", err) 561 } 562 return 563 } 564 565 ctx.Data["Title"] = ctx.Tr("org.teams.invite_team_member", team.Name) 566 ctx.Data["Invite"] = invite 567 ctx.Data["Organization"] = org 568 ctx.Data["Team"] = team 569 ctx.Data["Inviter"] = inviter 570 571 ctx.HTML(http.StatusOK, tplTeamInvite) 572 } 573 574 // TeamInvitePost handles the team invitation 575 func TeamInvitePost(ctx *context.Context) { 576 invite, org, team, _, err := getTeamInviteFromContext(ctx) 577 if err != nil { 578 if org_model.IsErrTeamInviteNotFound(err) { 579 ctx.NotFound("ErrTeamInviteNotFound", err) 580 } else { 581 ctx.ServerError("getTeamInviteFromContext", err) 582 } 583 return 584 } 585 586 if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil { 587 ctx.ServerError("AddTeamMember", err) 588 return 589 } 590 591 if err := org_model.RemoveInviteByID(ctx, invite.ID, team.ID); err != nil { 592 log.Error("RemoveInviteByID: %v", err) 593 } 594 595 ctx.Redirect(org.OrganisationLink() + "/teams/" + url.PathEscape(team.LowerName)) 596 } 597 598 func getTeamInviteFromContext(ctx *context.Context) (*org_model.TeamInvite, *org_model.Organization, *org_model.Team, *user_model.User, error) { 599 invite, err := org_model.GetInviteByToken(ctx, ctx.Params("token")) 600 if err != nil { 601 return nil, nil, nil, nil, err 602 } 603 604 inviter, err := user_model.GetUserByID(ctx, invite.InviterID) 605 if err != nil { 606 return nil, nil, nil, nil, err 607 } 608 609 team, err := org_model.GetTeamByID(ctx, invite.TeamID) 610 if err != nil { 611 return nil, nil, nil, nil, err 612 } 613 614 org, err := user_model.GetUserByID(ctx, team.OrgID) 615 if err != nil { 616 return nil, nil, nil, nil, err 617 } 618 619 return invite, org_model.OrgFromUser(org), team, inviter, nil 620 }