diff options
Diffstat (limited to '')
-rw-r--r-- | routers/web/org/teams.go | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go new file mode 100644 index 0000000..45c3674 --- /dev/null +++ b/routers/web/org/teams.go @@ -0,0 +1,628 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "fmt" + "net/http" + "net/url" + "path" + "strconv" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/forms" + org_service "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" +) + +const ( + // tplTeams template path for teams list page + tplTeams base.TplName = "org/team/teams" + // tplTeamNew template path for create new team page + tplTeamNew base.TplName = "org/team/new" + // tplTeamMembers template path for showing team members page + tplTeamMembers base.TplName = "org/team/members" + // tplTeamRepositories template path for showing team repositories page + tplTeamRepositories base.TplName = "org/team/repositories" + // tplTeamInvite template path for team invites page + tplTeamInvite base.TplName = "org/team/invite" +) + +// Teams render teams list page +func Teams(ctx *context.Context) { + org := ctx.Org.Organization + ctx.Data["Title"] = org.FullName + ctx.Data["PageIsOrgTeams"] = true + + for _, t := range ctx.Org.Teams { + if err := t.LoadMembers(ctx); err != nil { + ctx.ServerError("GetMembers", err) + return + } + } + ctx.Data["Teams"] = ctx.Org.Teams + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + ctx.HTML(http.StatusOK, tplTeams) +} + +// TeamsAction response for join, leave, remove, add operations to team +func TeamsAction(ctx *context.Context) { + page := ctx.FormString("page") + var err error + switch ctx.Params(":action") { + case "join": + if !ctx.Org.IsOwner { + ctx.Error(http.StatusNotFound) + return + } + err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID) + case "leave": + err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID) + if err != nil { + if org_model.IsErrLastOrgOwner(err) { + ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + } else { + log.Error("Action(%s): %v", ctx.Params(":action"), err) + ctx.JSON(http.StatusOK, map[string]any{ + "ok": false, + "err": err.Error(), + }) + return + } + } + checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/") + return + case "remove": + if !ctx.Org.IsOwner { + ctx.Error(http.StatusNotFound) + return + } + + uid := ctx.FormInt64("uid") + if uid == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/teams") + return + } + + err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid) + if err != nil { + if org_model.IsErrLastOrgOwner(err) { + ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + } else { + log.Error("Action(%s): %v", ctx.Params(":action"), err) + ctx.JSON(http.StatusOK, map[string]any{ + "ok": false, + "err": err.Error(), + }) + return + } + } + checkIsOrgMemberAndRedirect(ctx, ctx.Org.OrgLink+"/teams/"+url.PathEscape(ctx.Org.Team.LowerName)) + return + case "add": + if !ctx.Org.IsOwner { + ctx.Error(http.StatusNotFound) + return + } + uname := strings.ToLower(ctx.FormString("uname")) + var u *user_model.User + u, err = user_model.GetUserByName(ctx, uname) + if err != nil { + if user_model.IsErrUserNotExist(err) { + if setting.MailService != nil && user_model.ValidateEmail(uname) == nil { + if err := org_service.CreateTeamInvite(ctx, ctx.Doer, ctx.Org.Team, uname); err != nil { + if org_model.IsErrTeamInviteAlreadyExist(err) { + ctx.Flash.Error(ctx.Tr("form.duplicate_invite_to_team")) + } else if org_model.IsErrUserEmailAlreadyAdded(err) { + ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users")) + } else { + ctx.ServerError("CreateTeamInvite", err) + return + } + } + } else { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + } + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) + } else { + ctx.ServerError("GetUserByName", err) + } + return + } + + if u.IsOrganization() { + ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team")) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) + return + } + + if ctx.Org.Team.IsMember(ctx, u.ID) { + ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users")) + } else { + err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID) + } + + page = "team" + case "remove_invite": + if !ctx.Org.IsOwner { + ctx.Error(http.StatusNotFound) + return + } + + iid := ctx.FormInt64("iid") + if iid == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) + return + } + + if err := org_model.RemoveInviteByID(ctx, iid, ctx.Org.Team.ID); err != nil { + log.Error("Action(%s): %v", ctx.Params(":action"), err) + ctx.ServerError("RemoveInviteByID", err) + return + } + + page = "team" + } + + if err != nil { + if org_model.IsErrLastOrgOwner(err) { + ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + } else { + log.Error("Action(%s): %v", ctx.Params(":action"), err) + ctx.JSON(http.StatusOK, map[string]any{ + "ok": false, + "err": err.Error(), + }) + return + } + } + + switch page { + case "team": + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) + case "home": + ctx.Redirect(ctx.Org.Organization.AsUser().HomeLink()) + default: + ctx.Redirect(ctx.Org.OrgLink + "/teams") + } +} + +func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) { + if isOrgMember, err := org_model.IsOrganizationMember(ctx, ctx.Org.Organization.ID, ctx.Doer.ID); err != nil { + ctx.ServerError("IsOrganizationMember", err) + return + } else if !isOrgMember { + if ctx.Org.Organization.Visibility.IsPrivate() { + defaultRedirect = setting.AppSubURL + "/" + } else { + defaultRedirect = ctx.Org.Organization.HomeLink() + } + } + ctx.JSONRedirect(defaultRedirect) +} + +// TeamsRepoAction operate team's repository +func TeamsRepoAction(ctx *context.Context) { + if !ctx.Org.IsOwner { + ctx.Error(http.StatusNotFound) + return + } + + var err error + action := ctx.Params(":action") + switch action { + case "add": + repoName := path.Base(ctx.FormString("repo_name")) + var repo *repo_model.Repository + repo, err = repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, repoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo")) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") + return + } + ctx.ServerError("GetRepositoryByName", err) + return + } + err = org_service.TeamAddRepository(ctx, ctx.Org.Team, repo) + case "remove": + err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, ctx.FormInt64("repoid")) + case "addall": + err = models.AddAllRepositories(ctx, ctx.Org.Team) + case "removeall": + err = models.RemoveAllRepositories(ctx, ctx.Org.Team) + } + + if err != nil { + log.Error("Action(%s): '%s' %v", ctx.Params(":action"), ctx.Org.Team.Name, err) + ctx.ServerError("TeamsRepoAction", err) + return + } + + if action == "addall" || action == "removeall" { + ctx.JSONRedirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") + return + } + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName) + "/repositories") +} + +// NewTeam render create new team page +func NewTeam(ctx *context.Context) { + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["PageIsOrgTeamsNew"] = true + ctx.Data["Team"] = &org_model.Team{} + ctx.Data["Units"] = unit_model.Units + if err := shared_user.LoadHeaderCount(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplTeamNew) +} + +func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode { + unitPerms := make(map[unit_model.Type]perm.AccessMode) + for _, ut := range unit_model.AllRepoUnitTypes { + // Default accessmode is none + unitPerms[ut] = perm.AccessModeNone + + v, ok := forms[fmt.Sprintf("unit_%d", ut)] + if ok { + vv, _ := strconv.Atoi(v[0]) + if teamPermission >= perm.AccessModeAdmin { + unitPerms[ut] = teamPermission + // Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms. + if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki { + unitPerms[ut] = perm.AccessModeRead + } + } else { + unitPerms[ut] = perm.AccessMode(vv) + if unitPerms[ut] >= perm.AccessModeAdmin { + unitPerms[ut] = perm.AccessModeWrite + } + } + } + } + return unitPerms +} + +// NewTeamPost response for create new team +func NewTeamPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CreateTeamForm) + includesAllRepositories := form.RepoAccess == "all" + p := perm.ParseAccessMode(form.Permission) + unitPerms := getUnitPerms(ctx.Req.Form, p) + if p < perm.AccessModeAdmin { + // if p is less than admin accessmode, then it should be general accessmode, + // so we should calculate the minial accessmode from units accessmodes. + p = unit_model.MinUnitAccessMode(unitPerms) + } + + t := &org_model.Team{ + OrgID: ctx.Org.Organization.ID, + Name: form.TeamName, + Description: form.Description, + AccessMode: p, + IncludesAllRepositories: includesAllRepositories, + CanCreateOrgRepo: form.CanCreateOrgRepo, + } + + units := make([]*org_model.TeamUnit, 0, len(unitPerms)) + for tp, perm := range unitPerms { + units = append(units, &org_model.TeamUnit{ + OrgID: ctx.Org.Organization.ID, + Type: tp, + AccessMode: perm, + }) + } + t.Units = units + + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["PageIsOrgTeamsNew"] = true + ctx.Data["Units"] = unit_model.Units + ctx.Data["Team"] = t + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTeamNew) + return + } + + if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { + ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) + return + } + + if err := models.NewTeam(ctx, t); err != nil { + ctx.Data["Err_TeamName"] = true + switch { + case org_model.IsErrTeamAlreadyExist(err): + ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) + default: + ctx.ServerError("NewTeam", err) + } + return + } + log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) +} + +// TeamMembers render team members page +func TeamMembers(ctx *context.Context) { + ctx.Data["Title"] = ctx.Org.Team.Name + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["PageIsOrgTeamMembers"] = true + + if err := shared_user.LoadHeaderCount(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + if err := ctx.Org.Team.LoadMembers(ctx); err != nil { + ctx.ServerError("GetMembers", err) + return + } + ctx.Data["Units"] = unit_model.Units + + invites, err := org_model.GetInvitesByTeamID(ctx, ctx.Org.Team.ID) + if err != nil { + ctx.ServerError("GetInvitesByTeamID", err) + return + } + ctx.Data["Invites"] = invites + ctx.Data["IsEmailInviteEnabled"] = setting.MailService != nil + + ctx.HTML(http.StatusOK, tplTeamMembers) +} + +// TeamRepositories show the repositories of team +func TeamRepositories(ctx *context.Context) { + ctx.Data["Title"] = ctx.Org.Team.Name + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["PageIsOrgTeamRepos"] = true + + if err := shared_user.LoadHeaderCount(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + if err := ctx.Org.Team.LoadRepositories(ctx); err != nil { + ctx.ServerError("GetRepositories", err) + return + } + ctx.Data["Units"] = unit_model.Units + ctx.HTML(http.StatusOK, tplTeamRepositories) +} + +// SearchTeam api for searching teams +func SearchTeam(ctx *context.Context) { + listOptions := db.ListOptions{ + Page: ctx.FormInt("page"), + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + } + + opts := &org_model.SearchTeamOptions{ + // 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 + Keyword: ctx.FormTrim("q"), + OrgID: ctx.Org.Organization.ID, + IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"), + ListOptions: listOptions, + } + + teams, maxResults, err := org_model.SearchTeam(ctx, opts) + if err != nil { + log.Error("SearchTeam failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]any{ + "ok": false, + "error": "SearchTeam internal failure", + }) + return + } + + apiTeams, err := convert.ToTeams(ctx, teams, false) + if err != nil { + log.Error("convert ToTeams failed: %v", err) + ctx.JSON(http.StatusInternalServerError, map[string]any{ + "ok": false, + "error": "SearchTeam failed to get units", + }) + return + } + + ctx.SetTotalCountHeader(maxResults) + ctx.JSON(http.StatusOK, map[string]any{ + "ok": true, + "data": apiTeams, + }) +} + +// EditTeam render team edit page +func EditTeam(ctx *context.Context) { + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + if err := ctx.Org.Team.LoadUnits(ctx); err != nil { + ctx.ServerError("LoadUnits", err) + return + } + if err := shared_user.LoadHeaderCount(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.Data["Team"] = ctx.Org.Team + ctx.Data["Units"] = unit_model.Units + ctx.HTML(http.StatusOK, tplTeamNew) +} + +// EditTeamPost response for modify team information +func EditTeamPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CreateTeamForm) + t := ctx.Org.Team + newAccessMode := perm.ParseAccessMode(form.Permission) + unitPerms := getUnitPerms(ctx.Req.Form, newAccessMode) + if newAccessMode < perm.AccessModeAdmin { + // if newAccessMode is less than admin accessmode, then it should be general accessmode, + // so we should calculate the minial accessmode from units accessmodes. + newAccessMode = unit_model.MinUnitAccessMode(unitPerms) + } + isAuthChanged := false + isIncludeAllChanged := false + includesAllRepositories := form.RepoAccess == "all" + + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["Team"] = t + ctx.Data["Units"] = unit_model.Units + + if !t.IsOwnerTeam() { + t.Name = form.TeamName + if t.AccessMode != newAccessMode { + isAuthChanged = true + t.AccessMode = newAccessMode + } + + if t.IncludesAllRepositories != includesAllRepositories { + isIncludeAllChanged = true + t.IncludesAllRepositories = includesAllRepositories + } + t.CanCreateOrgRepo = form.CanCreateOrgRepo + + units := make([]*org_model.TeamUnit, 0, len(unitPerms)) + for tp, perm := range unitPerms { + units = append(units, &org_model.TeamUnit{ + OrgID: t.OrgID, + TeamID: t.ID, + Type: tp, + AccessMode: perm, + }) + } + t.Units = units + } else { + t.CanCreateOrgRepo = true + } + + t.Description = form.Description + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTeamNew) + return + } + + if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { + ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) + return + } + + if err := models.UpdateTeam(ctx, t, isAuthChanged, isIncludeAllChanged); err != nil { + ctx.Data["Err_TeamName"] = true + switch { + case org_model.IsErrTeamAlreadyExist(err): + ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) + default: + ctx.ServerError("UpdateTeam", err) + } + return + } + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(t.LowerName)) +} + +// DeleteTeam response for the delete team request +func DeleteTeam(ctx *context.Context) { + if err := models.DeleteTeam(ctx, ctx.Org.Team); err != nil { + ctx.Flash.Error("DeleteTeam: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success")) + } + + ctx.JSONRedirect(ctx.Org.OrgLink + "/teams") +} + +// TeamInvite renders the team invite page +func TeamInvite(ctx *context.Context) { + invite, org, team, inviter, err := getTeamInviteFromContext(ctx) + if err != nil { + if org_model.IsErrTeamInviteNotFound(err) { + ctx.NotFound("ErrTeamInviteNotFound", err) + } else { + ctx.ServerError("getTeamInviteFromContext", err) + } + return + } + + ctx.Data["Title"] = ctx.Tr("org.teams.invite_team_member", team.Name) + ctx.Data["Invite"] = invite + ctx.Data["Organization"] = org + ctx.Data["Team"] = team + ctx.Data["Inviter"] = inviter + + ctx.HTML(http.StatusOK, tplTeamInvite) +} + +// TeamInvitePost handles the team invitation +func TeamInvitePost(ctx *context.Context) { + invite, org, team, _, err := getTeamInviteFromContext(ctx) + if err != nil { + if org_model.IsErrTeamInviteNotFound(err) { + ctx.NotFound("ErrTeamInviteNotFound", err) + } else { + ctx.ServerError("getTeamInviteFromContext", err) + } + return + } + + if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil { + ctx.ServerError("AddTeamMember", err) + return + } + + if err := org_model.RemoveInviteByID(ctx, invite.ID, team.ID); err != nil { + log.Error("RemoveInviteByID: %v", err) + } + + ctx.Redirect(org.OrganisationLink() + "/teams/" + url.PathEscape(team.LowerName)) +} + +func getTeamInviteFromContext(ctx *context.Context) (*org_model.TeamInvite, *org_model.Organization, *org_model.Team, *user_model.User, error) { + invite, err := org_model.GetInviteByToken(ctx, ctx.Params("token")) + if err != nil { + return nil, nil, nil, nil, err + } + + inviter, err := user_model.GetUserByID(ctx, invite.InviterID) + if err != nil { + return nil, nil, nil, nil, err + } + + team, err := org_model.GetTeamByID(ctx, invite.TeamID) + if err != nil { + return nil, nil, nil, nil, err + } + + org, err := user_model.GetUserByID(ctx, team.OrgID) + if err != nil { + return nil, nil, nil, nil, err + } + + return invite, org_model.OrgFromUser(org), team, inviter, nil +} |