blob: 8322933c73a1c42361887e6cfdff58b446254a6e [file] [log] [blame]
package main
import (
"database/sql"
"embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
"sync"
"github.com/gorilla/mux"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
var port = flag.Int("port", 8080, "Port to listen on")
var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests")
var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
//go:embed memberships-tmpl/*
var tmpls embed.FS
//go:embed static/*
var staticResources embed.FS
type cachingHandler struct {
h http.Handler
}
func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=604800")
h.h.ServeHTTP(w, r)
}
type Server struct {
store Store
syncAddresses map[string]struct{}
mu sync.Mutex
}
type Group struct {
Id string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
}
type User struct {
Id string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
func getLoggedInUser(r *http.Request) (string, error) {
if user := r.Header.Get("X-Forwarded-UserId"); user != "" {
return user, nil
} else {
return "", fmt.Errorf("unauthenticated")
}
// return "0063f4b6-29cb-4bd6-b8ce-1f6b203d490f", nil
}
type Status int
const (
Owner Status = iota
Member
)
func (s *Server) Start() error {
e := make(chan error)
go func() {
r := mux.NewRouter()
r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticResources))})
r.HandleFunc("/group/{groupId:.*}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
r.HandleFunc("/group/{groupId:.*}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
r.HandleFunc("/group/{groupId:.*}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
r.HandleFunc("/group/{groupId:.*}/remove-child-group/{otherId:.*}", s.removeChildGroupHandler).Methods(http.MethodPost)
r.HandleFunc("/group/{groupId:.*}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
r.HandleFunc("/group/{groupId:.*}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
r.HandleFunc("/group/{groupId:.*}", s.groupHandler)
r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
r.HandleFunc("/user/{username}", s.userHandler)
r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
r.HandleFunc("/", s.homePageHandler)
e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
}()
go func() {
r := mux.NewRouter()
r.HandleFunc("/api/init", s.apiInitHandler)
// TODO(gio): change to /api/users/{username}
r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
r.HandleFunc("/api/group", s.apiCreateGroup).Methods(http.MethodPost)
r.HandleFunc("/api/add-group-user", s.apiAddUserToGroup).Methods(http.MethodPost)
r.HandleFunc("/api/remove-group-user", s.apiRemoveUserFromGroup).Methods(http.MethodPost)
r.HandleFunc("/api/add-group-group", s.apiAddGroupToGroup).Methods(http.MethodPost)
r.HandleFunc("/api/remove-group-group", s.apiRemoveGroupFromGroup).Methods(http.MethodPost)
r.HandleFunc("/api/get-group", s.apiGetGroup).Methods(http.MethodPost)
e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
}()
return <-e
}
type GroupData struct {
Group Group
Membership string
}
func (s *Server) checkIsOwner(w http.ResponseWriter, userId, groupId string) error {
ownerUsers, err := s.store.GetOwnerUsers(nil, groupId)
if err != nil {
return err
}
if slices.ContainsFunc(ownerUsers, func(u User) bool {
return u.Id == userId
}) {
return nil
}
ownerIds, err := s.store.GetOwnerGroups(nil, groupId)
if err != nil {
return err
}
canActAs, err := s.store.GetGroupsUserCanActAs(nil, userId)
if err != nil {
return err
}
for _, g := range canActAs {
if slices.Index(ownerIds, g) != -1 {
return nil
}
}
return fmt.Errorf("not an owner")
}
type templates struct {
group *template.Template
user *template.Template
}
func parseTemplates(fs embed.FS) (templates, error) {
base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
if err != nil {
return templates{}, err
}
parse := func(path string) (*template.Template, error) {
if b, err := base.Clone(); err != nil {
return nil, err
} else {
return b.ParseFS(fs, path)
}
}
user, err := parse("memberships-tmpl/user.html")
if err != nil {
return templates{}, err
}
group, err := parse("memberships-tmpl/group.html")
if err != nil {
return templates{}, err
}
return templates{group, user}, nil
}
func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
}
type UserPageData struct {
User User
OwnerGroups []Group
MembershipGroups []Group
TransitiveGroups []Group
LoggedInUserPage bool
SSHPublicKeys []string
ErrorMessage string
}
func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
errorMsg := r.URL.Query().Get("errorMessage")
vars := mux.Vars(r)
user := strings.ToLower(vars["username"])
// TODO(dtabidze): should check if username exists or not.
loggedInUserPage := loggedInUser == user
ownerGroups, err := s.store.GetGroupsUserOwns(nil, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
membershipGroups, err := s.store.GetGroupsUserIsMemberOf(nil, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
userInfo, err := s.store.GetUser(nil, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sshPublicKeys, err := s.store.GetUserPublicKeys(nil, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := UserPageData{
User: userInfo,
OwnerGroups: ownerGroups,
MembershipGroups: membershipGroups,
TransitiveGroups: transitiveGroups,
LoggedInUserPage: loggedInUserPage,
SSHPublicKeys: sshPublicKeys,
ErrorMessage: errorMsg,
}
templates, err := parseTemplates(tmpls)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := templates.user.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var group Group
group.Id = r.PostFormValue("id")
group.Title = r.PostFormValue("title")
group.Description = r.PostFormValue("description")
if err := isValidGroupId(group.Id); err != nil {
// http.Error(w, err.Error(), http.StatusBadRequest)
redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
if err := s.store.CreateGroup(nil, loggedInUser, group); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
type GroupPageData struct {
GroupId string
Title string
Description string
Owners []User
Members []User
AllGroups []Group
AllUsers []User
TransitiveGroups []Group
ChildGroups []Group
OwnerGroups []Group
ErrorMessage string
}
func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
_, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
errorMsg := r.URL.Query().Get("errorMessage")
vars := mux.Vars(r)
groupId := normalizeGroupId(vars["groupId"])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
g, err := s.store.GetGroup(nil, groupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
owners, err := s.store.GetOwnerUsers(nil, groupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
members, err := s.store.GetMemberUsers(nil, groupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
allUsers, err := s.store.GetAllUsers(nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
allGroups, err := s.store.GetAllGroups(nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
transitiveGroups, err := s.store.GetGroupsGroupCanActAs(nil, groupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
childGroups, err := s.store.GetMemberGroups(nil, groupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ownerGroups, err := s.store.GetOwnerGroups(nil, groupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := GroupPageData{
GroupId: groupId,
Title: g.Title,
Description: g.Description,
Owners: owners,
Members: members,
AllGroups: allGroups,
AllUsers: allUsers,
TransitiveGroups: transitiveGroups,
ChildGroups: childGroups,
OwnerGroups: ownerGroups,
ErrorMessage: errorMsg,
}
templates, err := parseTemplates(tmpls)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := templates.group.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
groupId := normalizeGroupId(vars["groupId"])
otherId := vars["otherId"]
if err := isValidGroupId(groupId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := isValidGroupId(otherId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
err = s.store.RemoveMemberGroup(nil, groupId, otherId)
if err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}
func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
username := vars["username"]
groupId := normalizeGroupId(vars["groupId"])
if err := isValidGroupId(groupId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
err = s.store.RemoveOwnerUser(nil, groupId, username)
if err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}
func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
username := vars["username"]
groupId := normalizeGroupId(vars["groupId"])
if err := isValidGroupId(groupId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
err = s.store.RemoveMemberUser(nil, groupId, username)
if err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}
func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
groupId := normalizeGroupId(vars["groupId"])
if err := isValidGroupId(groupId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
userId := strings.ToLower(r.FormValue("userId"))
if userId == "" {
http.Error(w, "Username parameter is required", http.StatusBadRequest)
return
}
status, err := convertStatus(r.FormValue("status"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
switch status {
case Owner:
err = s.store.AddOwnerUser(nil, groupId, userId)
case Member:
err = s.store.AddMemberUser(nil, groupId, userId)
default:
http.Error(w, "Invalid status", http.StatusBadRequest)
return
}
if err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}
func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
// TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
groupId := normalizeGroupId(vars["groupId"])
otherId := r.FormValue("otherId")
if err := isValidGroupId(groupId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := isValidGroupId(otherId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
if err := s.store.AddMemberGroup(nil, groupId, otherId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}
func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
groupId := normalizeGroupId(vars["groupId"])
otherId := r.FormValue("otherId")
if err := isValidGroupId(groupId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := isValidGroupId(otherId); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
if err := s.store.AddOwnerGroup(nil, groupId, otherId); err != nil {
redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
}
func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
defer s.pingAllSyncAddresses()
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
username := vars["username"]
if loggedInUser != username {
http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
return
}
sshKey := r.FormValue("ssh-key")
if sshKey == "" {
http.Error(w, "SSH key not present", http.StatusBadRequest)
return
}
if err := s.store.AddUserPublicKey(nil, strings.ToLower(username), sshKey); err != nil {
redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
}
func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
defer s.pingAllSyncAddresses()
loggedInUser, err := getLoggedInUser(r)
if err != nil {
http.Error(w, "User Not Logged In", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
username := vars["username"]
if loggedInUser != username {
http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
sshKey := r.FormValue("ssh-key")
if sshKey == "" {
http.Error(w, "SSH key not present", http.StatusBadRequest)
return
}
if err := s.store.RemoveUserPublicKey(nil, username, sshKey); err != nil {
redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
}
type initRequest struct {
User User `json:"user"`
Groups []Group `json:"groups"`
}
func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
var req initRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.store.Init(req.User, req.Groups); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type userInfo struct {
CanActAs []Group `json:"canActAs"`
OwnerOf []Group `json:"ownerOf"`
}
func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
user, ok := vars["username"]
if !ok || user == "" {
http.Error(w, "Username parameter is required", http.StatusBadRequest)
return
}
user = strings.ToLower(user)
transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
owned, err := s.store.GetGroupsUserOwns(nil, user)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(userInfo{transitiveGroups, owned}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
s.addSyncAddress(r.FormValue("selfAddress"))
var users []User
var err error
groups := r.FormValue("groups")
if groups == "" {
users, err = s.store.GetAllUsers(nil)
} else {
uniqueUsers := make(map[string]struct{})
g := strings.Split(groups, ",")
uniqueTG := make(map[string]struct{})
for _, group := range g {
uniqueTG[group] = struct{}{}
trGroups, err := s.store.GetGroupsGroupCanActAs(nil, group)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, tg := range trGroups {
uniqueTG[tg.Title] = struct{}{}
}
}
for group := range uniqueTG {
u, err := s.store.GetMemberUsers(nil, group)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, user := range u {
uniqueUsers[user.Username] = struct{}{}
}
}
usernames := make([]string, 0, len(uniqueUsers))
for username := range uniqueUsers {
usernames = append(usernames, username)
}
users, err = s.store.GetUsers(nil, usernames)
}
if err != nil {
http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(users); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
defer s.pingAllSyncAddresses()
var req User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Id == "" {
http.Error(w, "Id cannot be empty", http.StatusBadRequest)
return
}
if req.Username == "" {
http.Error(w, "Username cannot be empty", http.StatusBadRequest)
return
}
if req.Email == "" {
http.Error(w, "Email cannot be empty", http.StatusBadRequest)
return
}
user := User{Id: req.Id, Username: strings.ToLower(req.Username), Email: strings.ToLower(req.Email)}
if err := s.store.CreateUser(nil, user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type createGroupRequest struct {
UserId string `json:"userId"`
GroupId string `json:"groupId"`
Title string `json:"title"`
Description string `json:"description"`
}
func (s *Server) apiCreateGroup(w http.ResponseWriter, r *http.Request) {
var req createGroupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := s.store.CreateGroup(nil, req.UserId, Group{req.GroupId, req.Title, req.Description}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type apiGetGroupReq struct {
GroupId string `json:"groupId"`
}
// TODO(gio): update client
type apiGetGroupResp struct {
Self Group `json:"self"`
Members []User `json:"members"`
Owners []User `json:"owners"`
MemberGroups []Group `json:"memberGroups"`
OwnerGroups []Group `json:"ownerGroups"`
}
func (s *Server) apiGetGroup(w http.ResponseWriter, r *http.Request) {
var req apiGetGroupReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "missing group name", http.StatusBadRequest)
return
}
group, err := s.store.GetGroup(nil, req.GroupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
members, err := s.store.GetMemberUsers(nil, req.GroupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
owners, err := s.store.GetOwnerUsers(nil, req.GroupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
memberGroups, err := s.store.GetMemberGroups(nil, req.GroupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ownerGroups, err := s.store.GetOwnerGroups(nil, req.GroupId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(apiGetGroupResp{group, members, owners, memberGroups, ownerGroups}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type userGroupStatus struct {
GroupId string `json:"groupId"`
UserId string `json:"userId"`
Status string `json:"status"`
}
func (s *Server) apiAddUserToGroup(w http.ResponseWriter, r *http.Request) {
var req userGroupStatus
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
status, err := convertStatus(req.Status)
if err != nil {
http.Error(w, "Invalid status", http.StatusBadRequest)
return
}
fmt.Printf("AAA %+v\n", req)
switch status {
case Member:
err = s.store.AddMemberUser(nil, req.GroupId, req.UserId)
case Owner:
err = s.store.AddOwnerUser(nil, req.GroupId, req.UserId)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) apiRemoveUserFromGroup(w http.ResponseWriter, r *http.Request) {
var req userGroupStatus
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
status, err := convertStatus(req.Status)
if err != nil {
http.Error(w, "Invalid status", http.StatusBadRequest)
return
}
switch status {
case Member:
err = s.store.RemoveMemberUser(nil, req.GroupId, req.UserId)
case Owner:
err = s.store.RemoveOwnerUser(nil, req.GroupId, req.UserId)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type groupGroupStatus struct {
GroupId string `json:"groupId"`
OtherId string `json:"otherId"`
Status string `json:"status"`
}
func (s *Server) apiAddGroupToGroup(w http.ResponseWriter, r *http.Request) {
var req groupGroupStatus
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
status, err := convertStatus(req.Status)
if err != nil {
http.Error(w, "Invalid status", http.StatusBadRequest)
return
}
switch status {
case Member:
err = s.store.AddMemberGroup(nil, req.GroupId, req.OtherId)
case Owner:
err = s.store.AddOwnerGroup(nil, req.GroupId, req.OtherId)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) apiRemoveGroupFromGroup(w http.ResponseWriter, r *http.Request) {
var req groupGroupStatus
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
status, err := convertStatus(req.Status)
if err != nil {
http.Error(w, "Invalid status", http.StatusBadRequest)
return
}
switch status {
case Member:
err = s.store.RemoveMemberGroup(nil, req.GroupId, req.OtherId)
case Owner:
err = s.store.RemoveOwnerGroup(nil, req.GroupId, req.OtherId)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type getGroupsOwnedByRequest struct {
User string `json:"user"`
}
type getGroupsOwnedByResponse struct {
Groups []string `json:"groups"`
}
func (s *Server) apiGetUserOwnedGroups(w http.ResponseWriter, r *http.Request) {
var req getGroupsOwnedByRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
groups, err := s.store.GetGroupsUserOwns(nil, req.User)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var resp getGroupsOwnedByResponse
for _, g := range groups {
resp.Groups = append(resp.Groups, g.Title)
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type addUserKeyRequest struct {
User string `json:"user"`
PublicKey string `json:"publicKey"`
}
func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
var req addUserKeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.User == "" {
http.Error(w, "Username cannot be empty", http.StatusBadRequest)
return
}
if req.PublicKey == "" {
http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
return
}
if err := s.store.AddUserPublicKey(nil, strings.ToLower(req.User), req.PublicKey); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// TODO(gio): enque sync event instead of directly reaching out to clients.
// This will allow to deduplicate sync events and save resources.
func (s *Server) pingAllSyncAddresses() {
s.mu.Lock()
defer s.mu.Unlock()
for address := range s.syncAddresses {
go func(address string) {
log.Printf("Pinging %s", address)
resp, err := http.Get(address)
if err != nil {
// TODO(gio): remove sync address after N number of failures.
log.Printf("Failed to ping %s: %v", address, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
}
}(address)
}
}
func (s *Server) addSyncAddress(address string) {
if address == "" {
return
}
fmt.Printf("Adding sync address: %s\n", address)
s.mu.Lock()
defer s.mu.Unlock()
s.syncAddresses[address] = struct{}{}
}
func convertStatus(status string) (Status, error) {
switch strings.ToLower(status) {
case "owner":
return Owner, nil
case "member":
return Member, nil
default:
return Owner, fmt.Errorf("invalid status: %s", status)
}
}
func isValidGroupId(group string) error {
if strings.TrimSpace(group) == "" {
return fmt.Errorf("Group name can't be empty or contain only whitespaces")
}
validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
if !validGroupName.MatchString(group) {
return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
}
return nil
}
func main() {
flag.Parse()
db, err := sql.Open("sqlite3", *dbPath)
if err != nil {
panic(err)
}
store, err := NewSQLiteStore(db)
if err != nil {
panic(err)
}
s := Server{
store: store,
syncAddresses: make(map[string]struct{}),
mu: sync.Mutex{},
}
log.Fatal(s.Start())
}
func normalizeGroupId(groupId string) string {
if strings.Contains(groupId, "/") && !strings.HasPrefix(groupId, "/") {
return fmt.Sprintf("/%s", groupId)
}
return groupId
}