membership: api group membership endpoint (#104)

* added api group handler

* func name fixes

* changed json encoding
diff --git a/core/auth/memberships/main.go b/core/auth/memberships/main.go
index 3bb0948..0c1d104 100644
--- a/core/auth/memberships/main.go
+++ b/core/auth/memberships/main.go
@@ -3,16 +3,18 @@
 import (
 	"database/sql"
 	"embed"
+	"encoding/json"
 	"flag"
 	"fmt"
 	"html/template"
 	"log"
 	"net/http"
-	"strings"
 
 	"github.com/ncruces/go-sqlite3"
 	_ "github.com/ncruces/go-sqlite3/driver"
 	_ "github.com/ncruces/go-sqlite3/embed"
+
+	"github.com/gorilla/mux"
 )
 
 var port = flag.Int("port", 8080, "ort to listen on")
@@ -31,7 +33,7 @@
 	CreateGroup(owner string, group Group) error
 	AddChildGroup(parent, child string) error
 	GetGroupsOwnedBy(user string) ([]Group, error)
-	GetMembershipGroups(user string) ([]Group, error)
+	GetGroupsUserBelongsTo(user string) ([]Group, error)
 	IsGroupOwner(user, group string) (bool, error)
 	AddGroupMember(user, group string) error
 	AddGroupOwner(user, group string) error
@@ -39,6 +41,8 @@
 	GetGroupMembers(group string) ([]string, error)
 	GetGroupDescription(group string) (string, error)
 	GetAvailableGroupsAsChild(group string) ([]string, error)
+	GetAllTransitiveGroupsForUser(user string) ([]string, error)
+	GetGroupsGroupBelongsTo(group string) ([]string, error)
 }
 
 type Server struct {
@@ -118,7 +122,7 @@
 	return s.queryGroups(query, user)
 }
 
-func (s *SQLiteStore) GetMembershipGroups(user string) ([]Group, error) {
+func (s *SQLiteStore) GetGroupsUserBelongsTo(user string) ([]Group, error) {
 	query := `
         SELECT groups.name, groups.description
         FROM groups
@@ -311,6 +315,62 @@
 	return availableGroups, nil
 }
 
+func (s *SQLiteStore) GetAllTransitiveGroupsForUser(user string) ([]string, error) {
+	directGroups, err := s.GetGroupsUserBelongsTo(user)
+	if err != nil {
+		return nil, err
+	}
+	allGroups := make(map[string]bool)
+	for _, group := range directGroups {
+		if err := s.getParentGroups(group.Name, allGroups); err != nil {
+			return nil, err
+		}
+	}
+	var result []string
+	for group := range allGroups {
+		result = append(result, group)
+	}
+	return result, nil
+}
+
+func (s *SQLiteStore) getParentGroups(group string, allGroups map[string]bool) error {
+	if allGroups[group] {
+		return nil
+	}
+	allGroups[group] = true
+	parentGroups, err := s.GetGroupsGroupBelongsTo(group)
+	if err != nil {
+		return err
+	}
+	for _, parentGroup := range parentGroups {
+		if err := s.getParentGroups(parentGroup, allGroups); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *SQLiteStore) GetGroupsGroupBelongsTo(group string) ([]string, error) {
+	query := "SELECT parent_group FROM group_to_group WHERE child_group = ?"
+	rows, err := s.db.Query(query, group)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var parentGroups []string
+	for rows.Next() {
+		var parentGroup string
+		if err := rows.Scan(&parentGroup); err != nil {
+			return nil, err
+		}
+		parentGroups = append(parentGroups, parentGroup)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return parentGroups, nil
+}
+
 func getLoggedInUser(r *http.Request) (string, error) {
 	// TODO(dtabidze): should make a request to get loggedin user
 	return "tabo", nil
@@ -335,13 +395,15 @@
 }
 
 func (s *Server) Start() {
-	http.Handle("/static/", http.FileServer(http.FS(staticResources)))
-	http.HandleFunc("/", s.homePageHandler)
-	http.HandleFunc("/group/", s.groupHandler)
-	http.HandleFunc("/create-group", s.createGroupHandler)
-	http.HandleFunc("/add-user", s.addUserHandler)
-	http.HandleFunc("/add-child-group", s.addChildGroupHandler)
-	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+	router := mux.NewRouter()
+	router.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
+	router.HandleFunc("/group/{group-name}", s.groupHandler)
+	router.HandleFunc("/create-group", s.createGroupHandler)
+	router.HandleFunc("/add-user", s.addUserHandler)
+	router.HandleFunc("/add-child-group", s.addChildGroupHandler)
+	router.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
+	router.HandleFunc("/", s.homePageHandler)
+	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), router))
 }
 
 type GroupData struct {
@@ -373,7 +435,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	membershipGroups, err := s.store.GetMembershipGroups(loggedInUser)
+	membershipGroups, err := s.store.GetGroupsUserBelongsTo(loggedInUser)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -422,7 +484,9 @@
 }
 
 func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
-	groupName := strings.TrimPrefix(r.URL.Path, "/group/")
+	// groupName := strings.TrimPrefix(r.URL.Path, "/group/")
+	vars := mux.Vars(r)
+	groupName := vars["group-name"]
 	tmpl, err := template.New("group").Parse(groupHTML)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -526,6 +590,29 @@
 	http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
 }
 
+type UserInfo struct {
+	MemberOf []string `json:"memberOf"`
+}
+
+func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	user, ok := vars["username"]
+	if !ok {
+		http.Error(w, "Username parameter is required", http.StatusBadRequest)
+		return
+	}
+	transitiveGroups, err := s.store.GetAllTransitiveGroupsForUser(user)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(UserInfo{transitiveGroups}); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func main() {
 	flag.Parse()
 	db, err := NewSQLiteStore(*dbPath)