From fcf02e4961beb98cf1bc0f60537589e41a871369 Mon Sep 17 00:00:00 2001
From: Ethan Koenig <etk39@cornell.edu>
Date: Thu, 19 Jan 2017 19:31:46 -0700
Subject: [PATCH] API Endpoints for organization members (#645)

---
 models/user.go               |   6 +-
 routers/api/v1/api.go        |  10 +++
 routers/api/v1/org/member.go | 141 +++++++++++++++++++++++++++++++++++
 3 files changed, 155 insertions(+), 2 deletions(-)
 create mode 100644 routers/api/v1/org/member.go

diff --git a/models/user.go b/models/user.go
index e6ba2fa6f3..8b20d7e0ba 100644
--- a/models/user.go
+++ b/models/user.go
@@ -1052,8 +1052,10 @@ func GetUserEmailsByNames(names []string) []string {
 // GetUsersByIDs returns all resolved users from a list of Ids.
 func GetUsersByIDs(ids []int64) ([]*User, error) {
 	ous := make([]*User, 0, len(ids))
-	err := x.
-		In("id", ids).
+	if len(ids) == 0 {
+		return ous, nil
+	}
+	err := x.In("id", ids).
 		Asc("name").
 		Find(&ous)
 	return ous, err
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c30db1a33a..29f268d6bc 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -395,6 +395,16 @@ func RegisterRoutes(m *macaron.Macaron) {
 		m.Get("/users/:username/orgs", org.ListUserOrgs)
 		m.Group("/orgs/:orgname", func() {
 			m.Combo("").Get(org.Get).Patch(bind(api.EditOrgOption{}), org.Edit)
+			m.Group("/members", func() {
+				m.Get("", org.ListMembers)
+				m.Combo("/:username").Get(org.IsMember).Delete(org.DeleteMember)
+			})
+			m.Group("/public_members", func() {
+				m.Get("", org.ListPublicMembers)
+				m.Combo("/:username").Get(org.IsPublicMember).
+					Put(org.PublicizeMember).
+					Delete(org.ConcealMember)
+			})
 			m.Combo("/teams").Get(org.ListTeams)
 			m.Group("/hooks", func() {
 				m.Combo("").Get(org.ListHooks).
diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go
new file mode 100644
index 0000000000..2420b9b541
--- /dev/null
+++ b/routers/api/v1/org/member.go
@@ -0,0 +1,141 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package org
+
+import (
+	"fmt"
+
+	api "code.gitea.io/sdk/gitea"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/routers/api/v1/user"
+)
+
+// listMembers list an organization's members
+func listMembers(ctx *context.APIContext, publicOnly bool) {
+	var members []*models.User
+	if publicOnly {
+		orgUsers, err := models.GetOrgUsersByOrgID(ctx.Org.Organization.ID)
+		if err != nil {
+			ctx.Error(500, "GetOrgUsersByOrgID", err)
+			return
+		}
+
+		memberIDs := make([]int64, 0, len(orgUsers))
+		for _, orgUser := range orgUsers {
+			if orgUser.IsPublic {
+				memberIDs = append(memberIDs, orgUser.UID)
+			}
+		}
+
+		if members, err = models.GetUsersByIDs(memberIDs); err != nil {
+			ctx.Error(500, "GetUsersByIDs", err)
+			return
+		}
+	} else {
+		if err := ctx.Org.Organization.GetMembers(); err != nil {
+			ctx.Error(500, "GetMembers", err)
+			return
+		}
+		members = ctx.Org.Organization.Members
+	}
+
+	apiMembers := make([]*api.User, len(members))
+	for i, member := range members {
+		apiMembers[i] = member.APIFormat()
+	}
+	ctx.JSON(200, apiMembers)
+}
+
+// ListMembers list an organization's members
+func ListMembers(ctx *context.APIContext) {
+	listMembers(ctx, !ctx.Org.Organization.IsOrgMember(ctx.User.ID))
+}
+
+// ListPublicMembers list an organization's public members
+func ListPublicMembers(ctx *context.APIContext) {
+	listMembers(ctx, true)
+}
+
+// IsMember check if a user is a member of an organization
+func IsMember(ctx *context.APIContext) {
+	org := ctx.Org.Organization
+	requester := ctx.User
+	userToCheck := user.GetUserByParams(ctx)
+	if org.IsOrgMember(requester.ID) {
+		if org.IsOrgMember(userToCheck.ID) {
+			ctx.Status(204)
+		} else {
+			ctx.Status(404)
+		}
+	} else if requester.ID == userToCheck.ID {
+		ctx.Status(404)
+	} else {
+		redirectURL := fmt.Sprintf("%sapi/v1/orgs/%s/public_members/%s",
+			setting.AppURL, org.Name, userToCheck.Name)
+		ctx.Redirect(redirectURL, 302)
+	}
+}
+
+// IsPublicMember check if a user is a public member of an organization
+func IsPublicMember(ctx *context.APIContext) {
+	userToCheck := user.GetUserByParams(ctx)
+	if userToCheck.IsPublicMember(ctx.Org.Organization.ID) {
+		ctx.Status(204)
+	} else {
+		ctx.Status(404)
+	}
+}
+
+// PublicizeMember make a member's membership public
+func PublicizeMember(ctx *context.APIContext) {
+	userToPublicize := user.GetUserByParams(ctx)
+	if userToPublicize.ID != ctx.User.ID {
+		ctx.Error(403, "", "Cannot publicize another member")
+		return
+	} else if !ctx.Org.Organization.IsOrgMember(userToPublicize.ID) {
+		ctx.Error(403, "", "Must be a member of the organization")
+		return
+	}
+	err := models.ChangeOrgUserStatus(ctx.Org.Organization.ID, userToPublicize.ID, true)
+	if err != nil {
+		ctx.Error(500, "ChangeOrgUserStatus", err)
+		return
+	}
+	ctx.Status(204)
+}
+
+// ConcealMember make a member's membership not public
+func ConcealMember(ctx *context.APIContext) {
+	userToConceal := user.GetUserByParams(ctx)
+	if userToConceal.ID != ctx.User.ID {
+		ctx.Error(403, "", "Cannot conceal another member")
+		return
+	} else if !ctx.Org.Organization.IsOrgMember(userToConceal.ID) {
+		ctx.Error(403, "", "Must be a member of the organization")
+		return
+	}
+	err := models.ChangeOrgUserStatus(ctx.Org.Organization.ID, userToConceal.ID, false)
+	if err != nil {
+		ctx.Error(500, "ChangeOrgUserStatus", err)
+		return
+	}
+	ctx.Status(204)
+}
+
+// DeleteMember remove a member from an organization
+func DeleteMember(ctx *context.APIContext) {
+	org := ctx.Org.Organization
+	if !org.IsOwnedBy(ctx.User.ID) {
+		ctx.Error(403, "", "You must be an owner of the organization.")
+		return
+	}
+	if err := org.RemoveMember(user.GetUserByParams(ctx).ID); err != nil {
+		ctx.Error(500, "RemoveMember", err)
+	}
+	ctx.Status(204)
+}