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  }