diff --git a/routers/web/group/edit.go b/routers/web/group/edit.go new file mode 100644 index 0000000000..d5a5cec27e --- /dev/null +++ b/routers/web/group/edit.go @@ -0,0 +1,45 @@ +package group + +import ( + group_model "code.gitea.io/gitea/models/group" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + group_service "code.gitea.io/gitea/services/group" +) + +func MoveGroupItem(ctx *context.Context) { + form := &forms.MovedGroupItemForm{} + if err := json.NewDecoder(ctx.Req.Body).Decode(form); err != nil { + ctx.ServerError("DecodeMovedGroupItemForm", err) + return + } + if form.IsGroup { + group, err := group_model.GetGroupByID(ctx, form.ItemID) + if err != nil { + ctx.ServerError("GetGroupByID", err) + return + } + if group.ParentGroupID != form.NewParent { + if err = group_model.MoveGroup(ctx, group, form.NewParent, form.NewPos); err != nil { + ctx.ServerError("MoveGroup", err) + return + } + if err = group_service.RecalculateGroupAccess(ctx, group, false); err != nil { + ctx.ServerError("RecalculateGroupAccess", err) + } + } + } else { + repo, err := repo_model.GetRepositoryByID(ctx, form.ItemID) + if err != nil { + ctx.ServerError("GetRepositoryByID", err) + } + if repo.GroupID != form.NewParent { + if err = group_service.MoveRepositoryToGroup(ctx, repo, form.NewParent, form.NewPos); err != nil { + ctx.ServerError("MoveRepositoryToGroup", err) + } + } + } + ctx.JSONOK() +} diff --git a/routers/web/group/home.go b/routers/web/group/home.go new file mode 100644 index 0000000000..feb6ccb9ff --- /dev/null +++ b/routers/web/group/home.go @@ -0,0 +1,117 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + shared_group_model "code.gitea.io/gitea/models/shared/group" + "code.gitea.io/gitea/modules/setting" + shared_group "code.gitea.io/gitea/routers/web/shared/group" + "code.gitea.io/gitea/services/context" + "net/http" +) + +const ( + tplGroupHome = "group/home" +) + +func Home(ctx *context.Context) { + org := ctx.Org.Organization + + ctx.Data["PageIsUserProfile"] = true + ctx.Data["Title"] = org.DisplayName() + + var orderBy db.SearchOrderBy + sortOrder := ctx.FormString("sort") + if _, ok := repo_model.OrderByFlatMap[sortOrder]; !ok { + sortOrder = setting.UI.ExploreDefaultSort + } + ctx.Data["SortType"] = sortOrder + orderBy = repo_model.OrderByFlatMap[sortOrder] + + keyword := ctx.FormTrim("q") + ctx.Data["Keyword"] = keyword + + language := ctx.FormTrim("language") + ctx.Data["Language"] = language + + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + archived := ctx.FormOptionalBool("archived") + ctx.Data["IsArchived"] = archived + + fork := ctx.FormOptionalBool("fork") + ctx.Data["IsFork"] = fork + + mirror := ctx.FormOptionalBool("mirror") + ctx.Data["IsMirror"] = mirror + + template := ctx.FormOptionalBool("template") + ctx.Data["IsTemplate"] = template + + private := ctx.FormOptionalBool("private") + ctx.Data["IsPrivate"] = private + + err := shared_group.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + opts := &organization.FindOrgMembersOpts{ + Doer: ctx.Doer, + OrgID: org.ID, + IsDoerMember: ctx.Org.IsMember, + ListOptions: db.ListOptions{Page: 1, PageSize: 25}, + } + + members, err := shared_group_model.FindGroupMembers(ctx, ctx.RepoGroup.Group.ID, opts) + if err != nil { + ctx.ServerError("FindOrgMembers", err) + return + } + ctx.Data["Members"] = members + ctx.Data["Teams"] = ctx.RepoGroup.Teams + ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull + ctx.Data["ShowMemberAndTeamTab"] = ctx.RepoGroup.IsMember || len(members) > 0 + ctx.Data["PageIsViewRepositories"] = true + + var ( + repos []*repo_model.Repository + count int64 + ) + repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + PageSize: setting.UI.User.RepoPagingNum, + Page: page, + }, + Keyword: keyword, + OwnerID: org.ID, + OrderBy: orderBy, + Private: ctx.IsSigned, + Actor: ctx.Doer, + Language: language, + IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, + GroupID: ctx.RepoGroup.Group.ID, + }) + if err != nil { + ctx.ServerError("SearchRepository", err) + return + } + + ctx.Data["Repos"] = repos + ctx.Data["Total"] = count + + pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5) + pager.AddParamFromRequest(ctx.Req) + ctx.Data["Page"] = pager + ctx.HTML(http.StatusOK, tplGroupHome) +} diff --git a/routers/web/group/new.go b/routers/web/group/new.go new file mode 100644 index 0000000000..34bb8fbe29 --- /dev/null +++ b/routers/web/group/new.go @@ -0,0 +1,89 @@ +package group + +import ( + "code.gitea.io/gitea/models/perm" + "net/http" + + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + "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/forms" + group_service "code.gitea.io/gitea/services/group" +) + +const tplGroupNew = "group/create" + +func NewGroup(ctx *context.Context) { + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsNewGroup"] = true + if ctx.RepoGroup.Group != nil { + ctx.Data["Group"] = &group_model.Group{ParentGroupID: ctx.RepoGroup.Group.ID} + } else { + ctx.Data["Group"] = &group_model.Group{} + } + ctx.Data["Units"] = unit_model.Units + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + opts := group_model.FindGroupsOptions{ + ActorID: ctx.Doer.ID, + CanCreateIn: optional.Some(true), + OwnerID: ctx.Org.Organization.ID, + } + cond := group_model.AccessibleGroupCondition(ctx.Doer, unit_model.TypeInvalid, perm.AccessModeWrite) + cond = cond.And(opts.ToConds()) + groups, err := group_model.FindGroupsByCond(ctx, &group_model.FindGroupsOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + ParentGroupID: -1, + }, cond) + for _, g := range groups { + err = g.LoadAccessibleSubgroups(ctx, true, ctx.Doer) + if err != nil { + ctx.ServerError("LoadAccessibleSubgroups", err) + return + } + } + if err != nil { + ctx.ServerError("FindGroupsByCond", err) + return + } + ctx.Data["Groups"] = groups + ctx.HTML(http.StatusOK, tplGroupNew) +} +func NewGroupPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CreateGroupForm) + log.GetLogger(log.DEFAULT).Info("what? %+v", form) + g := &group_model.Group{ + OwnerID: ctx.Org.Organization.ID, + Name: form.GroupName, + Description: form.Description, + OwnerName: ctx.Org.Organization.Name, + ParentGroupID: form.ParentGroupID, + } + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsGroupNew"] = true + ctx.Data["Units"] = unit_model.Units + ctx.Data["Group"] = g + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplGroupNew) + return + } + + if err := group_service.NewGroup(ctx, g); err != nil { + ctx.Data["Err_GroupName"] = true + ctx.ServerError("NewGroup", err) + return + } + log.Trace("Group created: %s/%s", ctx.Org.Organization.Name, g.Name) + ctx.Redirect(g.GroupLink()) +} diff --git a/routers/web/group/search.go b/routers/web/group/search.go new file mode 100644 index 0000000000..783e4d497c --- /dev/null +++ b/routers/web/group/search.go @@ -0,0 +1,155 @@ +package group + +import ( + "errors" + "fmt" + "net/http" + + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + group_service "code.gitea.io/gitea/services/group" +) + +func toSearchRepoOptions(ctx *context.Context) (opts *repo_model.SearchRepoOptions) { + page := ctx.FormInt("page") + opts = &repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + }, + Actor: ctx.Doer, + Keyword: ctx.FormTrim("q"), + OwnerID: ctx.FormInt64("uid"), + PriorityOwnerID: ctx.FormInt64("priority_owner_id"), + TeamID: ctx.FormInt64("team_id"), + TopicOnly: ctx.FormBool("topic"), + Collaborate: optional.None[bool](), + Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")), + Template: optional.None[bool](), + StarredByID: ctx.FormInt64("starredBy"), + IncludeDescription: ctx.FormBool("includeDesc"), + } + if ctx.FormString("template") != "" { + opts.Template = optional.Some(ctx.FormBool("template")) + } + + if ctx.FormBool("exclusive") { + opts.Collaborate = optional.Some(false) + } + + mode := ctx.FormString("mode") + switch mode { + case "source": + opts.Fork = optional.Some(false) + opts.Mirror = optional.Some(false) + case "fork": + opts.Fork = optional.Some(true) + case "mirror": + opts.Mirror = optional.Some(true) + case "collaborative": + opts.Mirror = optional.Some(false) + opts.Collaborate = optional.Some(true) + case "": + default: + estr := fmt.Sprintf("Invalid search mode: \"%s\"", mode) + ctx.Status(http.StatusUnprocessableEntity) + ctx.ServerError("toSearchRepoOptions", errors.New(estr)) + return nil + } + + if ctx.FormString("archived") != "" { + opts.Archived = optional.Some(ctx.FormBool("archived")) + } + + if ctx.FormString("is_private") != "" { + opts.IsPrivate = optional.Some(ctx.FormBool("is_private")) + } + + sortMode := ctx.FormString("sort") + if len(sortMode) > 0 { + sortOrder := ctx.FormString("order") + if len(sortOrder) == 0 { + sortOrder = "asc" + } + if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok { + if orderBy, ok := searchModeMap[sortMode]; ok { + opts.OrderBy = orderBy + } else { + estr := fmt.Errorf("Invalid sort mode: \"%s\"", sortMode) + ctx.Status(http.StatusUnprocessableEntity) + ctx.ServerError("toSearchRepoOptions", estr) + return nil + } + } else { + estr := fmt.Errorf("Invalid sort order: \"%s\"", sortOrder) + ctx.Status(http.StatusUnprocessableEntity) + ctx.ServerError("toSearchRepoOptions", estr) + return nil + } + } + return +} + +func SearchGroup(ctx *context.Context) { + gid := ctx.FormInt64("group_id") + var ( + group *group_model.Group + err error + canSee bool = true + oid int64 = ctx.FormInt64("uid") + ) + if gid > 0 { + group, err = group_model.GetGroupByID(ctx, gid) + if err != nil && !group_model.IsErrGroupNotExist(err) { + ctx.ServerError("GetGroupByID", err) + return + } + } + if group != nil { + canSee, err = group.CanAccess(ctx, ctx.Doer) + if err != nil { + ctx.ServerError("GroupCanAccess", err) + return + } + oid = group.OwnerID + } + if !canSee { + ctx.NotFound(nil) + return + } + + subgroupOpts := &group_model.FindGroupsOptions{ + ParentGroupID: gid, + ActorID: ctx.Doer.ID, + OwnerID: oid, + } + if gid == 0 { + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + subgroupOpts.ListOptions = db.ListOptions{ + Page: page, + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + } + } + sro := toSearchRepoOptions(ctx) + rgw, err := group_service.SearchRepoGroupWeb(group, &group_service.WebSearchOptions{ + OrgID: oid, + GroupOpts: subgroupOpts, + RepoOpts: *sro, + Actor: ctx.Doer, + Recurse: ctx.FormBool("recurse"), + Ctx: ctx, + Locale: ctx.Locale, + }) + if err != nil { + ctx.ServerError("SearchRepoGroupWeb", err) + return + } + ctx.JSON(http.StatusOK, rgw) +} diff --git a/routers/web/group/setting.go b/routers/web/group/setting.go new file mode 100644 index 0000000000..d80f61a1d4 --- /dev/null +++ b/routers/web/group/setting.go @@ -0,0 +1,143 @@ +package group + +import ( + group_model "code.gitea.io/gitea/models/group" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/typesniffer" + "code.gitea.io/gitea/modules/web" + shared_group "code.gitea.io/gitea/routers/web/shared/group" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + group_service "code.gitea.io/gitea/services/group" + repo_service "code.gitea.io/gitea/services/repository" + user_service "code.gitea.io/gitea/services/user" + "errors" + "fmt" + "io" + "net/http" +) + +const ( + tplSettingsOptions templates.TplName = "group/settings/options" +) + +func RedirectToDefaultSetting(ctx *context.Context) { + ctx.Redirect(ctx.RepoGroup.OrgGroupLink + "/settings/actions/runners") +} + +// Settings render the main settings page +func Settings(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("group.settings") + ctx.Data["PageIsGroupSettings"] = true + ctx.Data["PageIsSettingsOptions"] = true + ctx.Data["CurrentVisibility"] = ctx.RepoGroup.Group.Visibility + ctx.Data["ContextUser"] = ctx.ContextUser + + err := shared_group.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + ctx.HTML(http.StatusOK, tplSettingsOptions) +} + +func SettingsPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.UpdateGroupSettingForm) + ctx.Data["Title"] = ctx.Tr("group.settings") + ctx.Data["PageIsGroupSettings"] = true + ctx.Data["PageIsSettingsOptions"] = true + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplSettingsOptions) + return + } + group := ctx.RepoGroup.Group + + opts := &group_service.UpdateOptions{ + Description: optional.Some(form.Description), + Visibility: optional.Some(form.Visibility), + } + if form.Name != group.Name { + opts.Name = optional.Some(form.Name) + } + visibilityChanged := group.Visibility != form.Visibility + if err := group_service.UpdateGroup(ctx, group, opts); err != nil { + ctx.ServerError("UpdateGroup", err) + return + } + if visibilityChanged { + repos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ + Actor: ctx.ContextUser, + Private: true, + GroupID: group.ID, + }) + if err != nil { + ctx.ServerError("SearchRepositories", err) + return + } + for _, repo := range repos { + if err = repo_service.UpdateRepository(ctx, repo, true); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } + } + } + log.Trace("Group setting updated: '%s'", group.Name) + ctx.Flash.Success(ctx.Tr("group.settings.update_setting_success")) + ctx.Redirect(ctx.RepoGroup.OrgGroupLink + "/settings") +} + +// SettingsAvatar response for change avatar on settings page +func SettingsAvatar(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.AvatarForm) + form.Source = forms.AvatarLocal + if err := updateAvatarSetting(ctx, form, ctx.RepoGroup.Group); err != nil { + ctx.Flash.Error(err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("group.settings.update_avatar_success")) + } + + ctx.Redirect(ctx.Org.OrgLink + "/settings") +} + +// SettingsDeleteAvatar response for delete avatar on settings page +func SettingsDeleteAvatar(ctx *context.Context) { + if err := user_service.DeleteAvatar(ctx, ctx.Org.Organization.AsUser()); err != nil { + ctx.Flash.Error(err.Error()) + } + + ctx.JSONRedirect(ctx.RepoGroup.OrgGroupLink + "/settings") +} + +func updateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, group *group_model.Group) error { + if form.Avatar != nil && form.Avatar.Filename != "" { + fr, err := form.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %w", err) + } + defer fr.Close() + + if form.Avatar.Size > setting.Avatar.MaxFileSize { + return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) + } + + data, err := io.ReadAll(fr) + if err != nil { + return fmt.Errorf("io.ReadAll: %w", err) + } + + st := typesniffer.DetectContentType(data) + if !(st.IsImage() && !st.IsSvgImage()) { + return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image")) + } + if err = group_service.UploadAvatar(ctx, group, data); err != nil { + return fmt.Errorf("UploadAvatar: %w", err) + } + } + return nil +} diff --git a/routers/web/group/team.go b/routers/web/group/team.go new file mode 100644 index 0000000000..415933830f --- /dev/null +++ b/routers/web/group/team.go @@ -0,0 +1,216 @@ +package group + +import ( + "code.gitea.io/gitea/models/db" + group_model "code.gitea.io/gitea/models/group" + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + web_org "code.gitea.io/gitea/routers/web/org" + shared_group "code.gitea.io/gitea/routers/web/shared/group" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/forms" + group_service "code.gitea.io/gitea/services/group" + "net/http" + "strings" +) + +const ( + tplTeamEdit = "group/team/new" + tplTeams = "group/team/teams" +) + +func MinUnitAccessMode(unitsMap map[unit_model.Type]perm.AccessMode) perm.AccessMode { + res := perm.AccessModeNone + for t, mode := range unitsMap { + // Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms. + if t == unit_model.TypeExternalTracker || t == unit_model.TypeExternalWiki { + continue + } + + // get the minial permission great than AccessModeNone except all are AccessModeNone + if mode > perm.AccessModeNone && (res == perm.AccessModeNone || mode < res) { + res = mode + } + } + return res +} + +func SearchTeamCandidates(ctx *context.Context) { + teams, _, err := org_model.SearchTeam(ctx, &org_model.SearchTeamOptions{ + OrgID: ctx.Org.Organization.ID, + Keyword: ctx.FormTrim("q"), + ListOptions: db.ListOptions{ + PageSize: setting.UI.MembersPagingNum, + }, + }) + if err != nil { + ctx.ServerError("Unable to search teams", err) + return + } + apiTeams, err := convert.ToTeams(ctx, teams, true) + if err != nil { + ctx.ServerError("Unable to search teams", err) + return + } + ctx.JSON(http.StatusOK, map[string]any{"data": apiTeams}) +} + +func Teams(ctx *context.Context) { + group := ctx.RepoGroup.Group + ctx.Data["Title"] = group.Name + ctx.Data["PageIsGroupTeams"] = true + for _, t := range ctx.RepoGroup.Teams { + if err := t.LoadMembers(ctx); err != nil { + ctx.ServerError("GetMembers", err) + return + } + } + ctx.Data["Teams"] = ctx.RepoGroup.Teams + + err := shared_group.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("RenderOrgHeader", err) + return + } + + ctx.HTML(http.StatusOK, tplTeams) +} + +func EditTeam(ctx *context.Context) { + ctx.Data["Title"] = ctx.RepoGroup.Group.Name + ctx.Data["PageIsGroupTeams"] = true + if err := ctx.RepoGroup.Team.LoadUnits(ctx); err != nil { + ctx.ServerError("LoadUnits", err) + return + } + if err := shared_group.LoadHeaderCount(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.Data["GroupTeam"] = ctx.RepoGroup.GroupTeam + ctx.Data["Team"] = ctx.RepoGroup.Team + ctx.Data["Units"] = unit_model.Units + ctx.HTML(http.StatusOK, tplTeamEdit) +} + +func EditTeamPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CreateGroupTeamForm) + t := ctx.RepoGroup.Team + gt := ctx.RepoGroup.GroupTeam + newAccessMode := perm.ParseAccessMode(form.Permission) + unitPerms := web_org.GetUnitPerms(ctx.Req.Form, newAccessMode) + if newAccessMode < perm.AccessModeAdmin { + newAccessMode = MinUnitAccessMode(unitPerms) + } + ctx.Data["Title"] = ctx.RepoGroup.Group.Name + ctx.Data["PageIsGroupTeams"] = true + ctx.Data["Team"] = t + ctx.Data["GroupTeam"] = gt + ctx.Data["Units"] = unit_model.Units + if !t.IsOwnerTeam() { + if gt.AccessMode != newAccessMode { + gt.AccessMode = newAccessMode + } + if gt.CanCreateIn != form.CanCreateRepoOrSubGroup { + gt.CanCreateIn = form.CanCreateRepoOrSubGroup + } + } else { + gt.CanCreateIn = true + } + units := make([]*group_model.RepoGroupUnit, 0, len(unitPerms)) + for tp, perm := range unitPerms { + units = append(units, &group_model.RepoGroupUnit{ + GroupID: gt.GroupID, + TeamID: t.ID, + Type: tp, + AccessMode: perm, + }) + } + gt.Units = units + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTeamEdit) + return + } + if gt.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { + ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamEdit, &form) + return + } + if err := group_service.UpdateGroupTeam(ctx, gt); err != nil { + ctx.ServerError("UpdateGroupTeam", err) + return + } + ctx.Redirect(ctx.Org.OrgLink + "/teams/") +} + +func TeamAddPost(ctx *context.Context) { + if !ctx.RepoGroup.IsGroupAdmin || !ctx.RepoGroup.IsOwner { + ctx.NotFound(nil) + return + } + group := ctx.RepoGroup.Group + tname := strings.ToLower(ctx.FormTrim("tname")) + t, err := org_model.GetTeam(ctx, group.OwnerID, tname) + if err != nil { + if org_model.IsErrTeamNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.team_not_exist")) + ctx.Redirect(ctx.RepoGroup.OrgGroupLink + "/teams") + } else { + ctx.ServerError("GetTeam", err) + } + return + } + has := group_model.HasTeamGroup(ctx, group.OwnerID, t.ID, group.ID) + if has { + ctx.Flash.Error(ctx.Tr("org.group.add_duplicate_team")) + } else { + parentGroup, err := group_model.FindGroupTeamByTeamID(ctx, group.ID, t.ID) + if err != nil { + ctx.ServerError("FindGroupTeamByTeamID", err) + return + } + mode := t.AccessMode + canCreateIn := t.CanCreateOrgRepo + if parentGroup != nil { + mode = max(t.AccessMode, parentGroup.AccessMode) + canCreateIn = parentGroup.CanCreateIn || t.CanCreateOrgRepo + } + if err = group.LoadParentGroup(ctx); err != nil { + ctx.ServerError("LoadParentGroup", err) + return + } + err = group_model.AddTeamGroup(ctx, ctx.RepoGroup.Group.OwnerID, t.ID, ctx.RepoGroup.Group.ID, mode, canCreateIn) + if err != nil { + ctx.ServerError("AddTeamGroup", err) + return + } + } + ctx.Redirect(group.OrgGroupLink() + "/teams") +} + +func TeamRemove(ctx *context.Context) { + if !ctx.RepoGroup.IsGroupAdmin || !ctx.RepoGroup.IsOwner { + ctx.NotFound(nil) + return + } + org := ctx.Org.Organization.ID + group := ctx.RepoGroup.Group + team, err := org_model.GetTeam(ctx, org, ctx.PathParam("team")) + if err != nil { + if org_model.IsErrTeamNotExist(err) { + ctx.NotFound(err) + } else { + ctx.ServerError("GetTeam", err) + } + return + } + + if err = group_model.RemoveTeamGroup(ctx, org, team.ID, group.ID); err != nil { + ctx.ServerError("RemoveTeamGroup", err) + return + } + ctx.JSONRedirect(ctx.RepoGroup.OrgGroupLink + "/teams") +} diff --git a/routers/web/web.go b/routers/web/web.go index ab3546c11d..84e2ed2f27 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -4,6 +4,7 @@ package web import ( + "code.gitea.io/gitea/routers/web/group" "net/http" "strings" @@ -922,6 +923,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Group("/org", func() { m.Group("/{org}", func() { + m.Get("/-/search_team_candidates", optExploreSignIn, group.SearchTeamCandidates) m.Get("/members", org.Members) }, context.OrgAssignment(context.OrgAssignmentOptions{})) }, optSignIn) @@ -951,6 +953,31 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones) m.Post("/members/action/{action}", org.MembersAction) m.Get("/teams", org.Teams) + + m.Group("/groups", func() { + m.Combo("/new"). + Get(group.NewGroup). + Post(web.Bind(forms.CreateGroupForm{}), group.NewGroupPost) + m.Group("/{group_id}", func() { + m.Get("/teams", group.Teams) + m.Post("/teams/add", group.TeamAddPost) + m.Combo("/teams/{team}/edit"). + Get(group.EditTeam). + Post(web.Bind(forms.CreateGroupTeamForm{}), group.EditTeamPost) + m.Post("/teams/{team}/remove", group.TeamRemove) + m.Group("/settings", func() { + m.Combo(""). + Get(group.Settings). + Post(web.Bind(forms.UpdateGroupSettingForm{}), group.SettingsPost) + m.Post("/avatar", web.Bind(forms.AvatarForm{}), group.SettingsAvatar) + m.Post("/avatar/delete", group.SettingsDeleteAvatar) + }, ctxDataSet("PageIsGroupSettings", true)) + }, context.GroupAssignment(context.GroupAssignmentOptions{ + RequireMember: true, + RequireOwner: false, + RequireGroupAdmin: true, + })) + }) }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true})) m.Group("/{org}", func() { @@ -1053,6 +1080,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { }, reqSignIn) // end "/org": most org routes + m.Group("/group", func() { + m.Get("/search", group.SearchGroup) + }, reqSignIn) + // end "/group": search + m.Group("/repo", func() { m.Get("/create", repo.Create) m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost) @@ -1126,6 +1158,18 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker) }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{})) // end "/{username}/-": packages, projects, code + m.Group("/{username}/groups", func() { + m.Group("/{group_id}", func() { + m.Get("", group.Home) + }, context.GroupAssignment(context.GroupAssignmentOptions{})) + }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{})) + + m.Group("/{username}/groups", func() { + m.Post("/items/move", group.MoveGroupItem) + }, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{ + RequireMember: true, + })) + // end "/{username}/groups" repoDashFn := func() { m.Group("/migrate", func() {