blob: 942846f05bcc3eb09a00e95bc72e9be57c103fc4 [file] [log] [blame]
DTabidze0d802592024-03-19 17:42:45 +04001package main
2
3import (
4 "database/sql"
5 "embed"
DTabidzed7744a62024-03-20 14:09:15 +04006 "encoding/json"
DTabidze0d802592024-03-19 17:42:45 +04007 "flag"
8 "fmt"
9 "html/template"
10 "log"
11 "net/http"
DTabidze4b44ff42024-04-02 03:16:26 +040012 "net/url"
DTabidze908bb852024-03-25 20:07:57 +040013 "regexp"
gio134be722025-07-20 19:01:17 +040014 "slices"
DTabidze908bb852024-03-25 20:07:57 +040015 "strings"
Davit Tabidze75d57c32024-07-19 19:17:55 +040016 "sync"
DTabidze0d802592024-03-19 17:42:45 +040017
gio134be722025-07-20 19:01:17 +040018 "github.com/gorilla/mux"
DTabidze0d802592024-03-19 17:42:45 +040019 _ "github.com/ncruces/go-sqlite3/driver"
20 _ "github.com/ncruces/go-sqlite3/embed"
21)
22
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040023var port = flag.Int("port", 8080, "Port to listen on")
24var apiPort = flag.Int("api-port", 8081, "Port to listen on for API requests")
DTabidze0d802592024-03-19 17:42:45 +040025var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
26
DTabidze4b44ff42024-04-02 03:16:26 +040027//go:embed memberships-tmpl/*
28var tmpls embed.FS
DTabidze0d802592024-03-19 17:42:45 +040029
gio4a297752025-07-15 13:24:57 +040030//go:embed static/*
DTabidze0d802592024-03-19 17:42:45 +040031var staticResources embed.FS
32
gio4a297752025-07-15 13:24:57 +040033type cachingHandler struct {
34 h http.Handler
35}
36
37func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
38 w.Header().Set("Cache-Control", "max-age=604800")
39 h.h.ServeHTTP(w, r)
40}
41
DTabidze0d802592024-03-19 17:42:45 +040042type Server struct {
Davit Tabidze75d57c32024-07-19 19:17:55 +040043 store Store
44 syncAddresses map[string]struct{}
45 mu sync.Mutex
DTabidze0d802592024-03-19 17:42:45 +040046}
47
48type Group struct {
gio134be722025-07-20 19:01:17 +040049 Id string `json:"id"`
50 Title string `json:"title"`
51 Description string `json:"description"`
DTabidze0d802592024-03-19 17:42:45 +040052}
53
Davit Tabidze75d57c32024-07-19 19:17:55 +040054type User struct {
gio134be722025-07-20 19:01:17 +040055 Id string `json:"id"`
56 Username string `json:"username"`
57 Email string `json:"email"`
gio2e6a87b2026-07-29 14:00:05 +040058 SSHPublicKeys []string `json:"sshPublicKeys,omitempty"`
Davit Tabidze75d57c32024-07-19 19:17:55 +040059}
60
DTabidze0d802592024-03-19 17:42:45 +040061func getLoggedInUser(r *http.Request) (string, error) {
gio134be722025-07-20 19:01:17 +040062 if user := r.Header.Get("X-Forwarded-UserId"); user != "" {
DTabidzec0b4d8f2024-03-22 17:25:10 +040063 return user, nil
64 } else {
65 return "", fmt.Errorf("unauthenticated")
66 }
gio134be722025-07-20 19:01:17 +040067 // return "0063f4b6-29cb-4bd6-b8ce-1f6b203d490f", nil
DTabidze0d802592024-03-19 17:42:45 +040068}
69
70type Status int
71
72const (
73 Owner Status = iota
74 Member
75)
76
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040077func (s *Server) Start() error {
78 e := make(chan error)
79 go func() {
80 r := mux.NewRouter()
gio4a297752025-07-15 13:24:57 +040081 r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticResources))})
gio134be722025-07-20 19:01:17 +040082 r.HandleFunc("/group/{groupId:.*}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
83 r.HandleFunc("/group/{groupId:.*}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
84 r.HandleFunc("/group/{groupId:.*}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
85 r.HandleFunc("/group/{groupId:.*}/remove-child-group/{otherId:.*}", s.removeChildGroupHandler).Methods(http.MethodPost)
86 r.HandleFunc("/group/{groupId:.*}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
87 r.HandleFunc("/group/{groupId:.*}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
88 r.HandleFunc("/group/{groupId:.*}", s.groupHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +040089 r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
90 r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
DTabidze5d735e32024-03-26 16:01:06 +040091 r.HandleFunc("/user/{username}", s.userHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +040092 r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040093 r.HandleFunc("/", s.homePageHandler)
94 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
95 }()
96 go func() {
97 r := mux.NewRouter()
98 r.HandleFunc("/api/init", s.apiInitHandler)
gio7fbd4ad2024-08-27 10:06:39 +040099 // TODO(gio): change to /api/users/{username}
100 r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400101 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400102 r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
103 r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
gio134be722025-07-20 19:01:17 +0400104 r.HandleFunc("/api/group", s.apiCreateGroup).Methods(http.MethodPost)
105 r.HandleFunc("/api/add-group-user", s.apiAddUserToGroup).Methods(http.MethodPost)
106 r.HandleFunc("/api/remove-group-user", s.apiRemoveUserFromGroup).Methods(http.MethodPost)
107 r.HandleFunc("/api/add-group-group", s.apiAddGroupToGroup).Methods(http.MethodPost)
108 r.HandleFunc("/api/remove-group-group", s.apiRemoveGroupFromGroup).Methods(http.MethodPost)
109 r.HandleFunc("/api/get-group", s.apiGetGroup).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400110 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
111 }()
112 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400113}
114
115type GroupData struct {
116 Group Group
117 Membership string
118}
119
gio134be722025-07-20 19:01:17 +0400120func (s *Server) checkIsOwner(w http.ResponseWriter, userId, groupId string) error {
121 ownerUsers, err := s.store.GetOwnerUsers(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400122 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400123 return err
DTabidze0d802592024-03-19 17:42:45 +0400124 }
gio134be722025-07-20 19:01:17 +0400125 if slices.ContainsFunc(ownerUsers, func(u User) bool {
126 return u.Id == userId
127 }) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400128 return nil
DTabidze0d802592024-03-19 17:42:45 +0400129 }
gio134be722025-07-20 19:01:17 +0400130 ownerIds, err := s.store.GetOwnerGroups(nil, groupId)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400131 if err != nil {
132 return err
133 }
gio134be722025-07-20 19:01:17 +0400134 canActAs, err := s.store.GetGroupsUserCanActAs(nil, userId)
135 if err != nil {
136 return err
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400137 }
gio134be722025-07-20 19:01:17 +0400138 for _, g := range canActAs {
139 if slices.Index(ownerIds, g) != -1 {
140 return nil
141 }
142 }
143 return fmt.Errorf("not an owner")
DTabidze0d802592024-03-19 17:42:45 +0400144}
145
DTabidze4b44ff42024-04-02 03:16:26 +0400146type templates struct {
147 group *template.Template
148 user *template.Template
149}
150
151func parseTemplates(fs embed.FS) (templates, error) {
152 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
153 if err != nil {
154 return templates{}, err
155 }
156 parse := func(path string) (*template.Template, error) {
157 if b, err := base.Clone(); err != nil {
158 return nil, err
159 } else {
160 return b.ParseFS(fs, path)
161 }
162 }
163 user, err := parse("memberships-tmpl/user.html")
164 if err != nil {
165 return templates{}, err
166 }
167 group, err := parse("memberships-tmpl/group.html")
168 if err != nil {
169 return templates{}, err
170 }
171 return templates{group, user}, nil
172}
173
DTabidze0d802592024-03-19 17:42:45 +0400174func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
175 loggedInUser, err := getLoggedInUser(r)
176 if err != nil {
177 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
178 return
179 }
DTabidze5d735e32024-03-26 16:01:06 +0400180 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
181}
182
Davit Tabidze75d57c32024-07-19 19:17:55 +0400183type UserPageData struct {
gio134be722025-07-20 19:01:17 +0400184 User User
Davit Tabidze75d57c32024-07-19 19:17:55 +0400185 OwnerGroups []Group
186 MembershipGroups []Group
187 TransitiveGroups []Group
188 LoggedInUserPage bool
Davit Tabidze75d57c32024-07-19 19:17:55 +0400189 SSHPublicKeys []string
Davit Tabidze75d57c32024-07-19 19:17:55 +0400190 ErrorMessage string
191}
192
DTabidze5d735e32024-03-26 16:01:06 +0400193func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
194 loggedInUser, err := getLoggedInUser(r)
195 if err != nil {
196 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
197 return
198 }
DTabidze4b44ff42024-04-02 03:16:26 +0400199 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400200 vars := mux.Vars(r)
201 user := strings.ToLower(vars["username"])
202 // TODO(dtabidze): should check if username exists or not.
203 loggedInUserPage := loggedInUser == user
gio134be722025-07-20 19:01:17 +0400204 ownerGroups, err := s.store.GetGroupsUserOwns(nil, user)
DTabidze0d802592024-03-19 17:42:45 +0400205 if err != nil {
206 http.Error(w, err.Error(), http.StatusInternalServerError)
207 return
208 }
gio134be722025-07-20 19:01:17 +0400209 membershipGroups, err := s.store.GetGroupsUserIsMemberOf(nil, user)
DTabidze0d802592024-03-19 17:42:45 +0400210 if err != nil {
211 http.Error(w, err.Error(), http.StatusInternalServerError)
212 return
213 }
gio134be722025-07-20 19:01:17 +0400214 transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400215 if err != nil {
216 http.Error(w, err.Error(), http.StatusInternalServerError)
217 return
218 }
gio134be722025-07-20 19:01:17 +0400219 userInfo, err := s.store.GetUser(nil, user)
220 if err != nil {
221 http.Error(w, err.Error(), http.StatusInternalServerError)
222 return
223 }
224 sshPublicKeys, err := s.store.GetUserPublicKeys(nil, user)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400225 if err != nil {
226 http.Error(w, err.Error(), http.StatusInternalServerError)
227 return
228 }
229 data := UserPageData{
gio134be722025-07-20 19:01:17 +0400230 User: userInfo,
DTabidze0d802592024-03-19 17:42:45 +0400231 OwnerGroups: ownerGroups,
232 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400233 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400234 LoggedInUserPage: loggedInUserPage,
gio134be722025-07-20 19:01:17 +0400235 SSHPublicKeys: sshPublicKeys,
DTabidze4b44ff42024-04-02 03:16:26 +0400236 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400237 }
DTabidze4b44ff42024-04-02 03:16:26 +0400238 templates, err := parseTemplates(tmpls)
239 if err != nil {
240 http.Error(w, err.Error(), http.StatusInternalServerError)
241 return
242 }
243 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400244 http.Error(w, err.Error(), http.StatusInternalServerError)
245 return
246 }
247}
248
249func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
250 loggedInUser, err := getLoggedInUser(r)
251 if err != nil {
252 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
253 return
254 }
DTabidze0d802592024-03-19 17:42:45 +0400255 if err := r.ParseForm(); err != nil {
256 http.Error(w, err.Error(), http.StatusInternalServerError)
257 return
258 }
259 var group Group
gio134be722025-07-20 19:01:17 +0400260 group.Id = r.PostFormValue("id")
261 group.Title = r.PostFormValue("title")
262 group.Description = r.PostFormValue("description")
263 if err := isValidGroupId(group.Id); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400264 // http.Error(w, err.Error(), http.StatusBadRequest)
265 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
266 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400267 return
268 }
gio134be722025-07-20 19:01:17 +0400269 if err := s.store.CreateGroup(nil, loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400270 // http.Error(w, err.Error(), http.StatusInternalServerError)
271 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
272 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400273 return
274 }
275 http.Redirect(w, r, "/", http.StatusSeeOther)
276}
277
Davit Tabidze75d57c32024-07-19 19:17:55 +0400278type GroupPageData struct {
gio134be722025-07-20 19:01:17 +0400279 GroupId string
280 Title string
Davit Tabidze75d57c32024-07-19 19:17:55 +0400281 Description string
gio134be722025-07-20 19:01:17 +0400282 Owners []User
283 Members []User
Davit Tabidze75d57c32024-07-19 19:17:55 +0400284 AllGroups []Group
gio134be722025-07-20 19:01:17 +0400285 AllUsers []User
Davit Tabidze75d57c32024-07-19 19:17:55 +0400286 TransitiveGroups []Group
287 ChildGroups []Group
288 OwnerGroups []Group
289 ErrorMessage string
290}
291
DTabidze0d802592024-03-19 17:42:45 +0400292func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400293 _, err := getLoggedInUser(r)
294 if err != nil {
295 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
296 return
297 }
DTabidze4b44ff42024-04-02 03:16:26 +0400298 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400299 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400300 groupId := normalizeGroupId(vars["groupId"])
DTabidze908bb852024-03-25 20:07:57 +0400301 if err != nil {
302 http.Error(w, err.Error(), http.StatusInternalServerError)
303 return
304 }
gio134be722025-07-20 19:01:17 +0400305 g, err := s.store.GetGroup(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400306 if err != nil {
307 http.Error(w, err.Error(), http.StatusInternalServerError)
308 return
309 }
gio134be722025-07-20 19:01:17 +0400310 owners, err := s.store.GetOwnerUsers(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400311 if err != nil {
312 http.Error(w, err.Error(), http.StatusInternalServerError)
313 return
314 }
gio134be722025-07-20 19:01:17 +0400315 members, err := s.store.GetMemberUsers(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400316 if err != nil {
317 http.Error(w, err.Error(), http.StatusInternalServerError)
318 return
319 }
gio134be722025-07-20 19:01:17 +0400320 allUsers, err := s.store.GetAllUsers(nil)
DTabidze0d802592024-03-19 17:42:45 +0400321 if err != nil {
322 http.Error(w, err.Error(), http.StatusInternalServerError)
323 return
324 }
gio134be722025-07-20 19:01:17 +0400325 allGroups, err := s.store.GetAllGroups(nil)
DTabidze0d802592024-03-19 17:42:45 +0400326 if err != nil {
327 http.Error(w, err.Error(), http.StatusInternalServerError)
328 return
329 }
gio134be722025-07-20 19:01:17 +0400330 transitiveGroups, err := s.store.GetGroupsGroupCanActAs(nil, groupId)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400331 if err != nil {
332 http.Error(w, err.Error(), http.StatusInternalServerError)
333 return
334 }
gio134be722025-07-20 19:01:17 +0400335 childGroups, err := s.store.GetMemberGroups(nil, groupId)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400336 if err != nil {
337 http.Error(w, err.Error(), http.StatusInternalServerError)
338 return
339 }
gio134be722025-07-20 19:01:17 +0400340 ownerGroups, err := s.store.GetOwnerGroups(nil, groupId)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400341 if err != nil {
342 http.Error(w, err.Error(), http.StatusInternalServerError)
343 return
344 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400345 data := GroupPageData{
gio134be722025-07-20 19:01:17 +0400346 GroupId: groupId,
347 Title: g.Title,
348 Description: g.Description,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400349 Owners: owners,
350 Members: members,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400351 AllGroups: allGroups,
gio134be722025-07-20 19:01:17 +0400352 AllUsers: allUsers,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400353 TransitiveGroups: transitiveGroups,
354 ChildGroups: childGroups,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400355 OwnerGroups: ownerGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400356 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400357 }
DTabidze4b44ff42024-04-02 03:16:26 +0400358 templates, err := parseTemplates(tmpls)
359 if err != nil {
360 http.Error(w, err.Error(), http.StatusInternalServerError)
361 return
362 }
363 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400364 http.Error(w, err.Error(), http.StatusInternalServerError)
365 return
366 }
367}
368
DTabidze2b224bf2024-03-27 13:25:49 +0400369func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
370 loggedInUser, err := getLoggedInUser(r)
371 if err != nil {
372 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
373 return
374 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400375 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400376 groupId := normalizeGroupId(vars["groupId"])
377 otherId := vars["otherId"]
378 if err := isValidGroupId(groupId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400379 http.Error(w, err.Error(), http.StatusBadRequest)
380 return
DTabidze2b224bf2024-03-27 13:25:49 +0400381 }
gio134be722025-07-20 19:01:17 +0400382 if err := isValidGroupId(otherId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400383 http.Error(w, err.Error(), http.StatusBadRequest)
384 return
385 }
gio134be722025-07-20 19:01:17 +0400386 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
387 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400388 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
389 return
390 }
gio134be722025-07-20 19:01:17 +0400391 err = s.store.RemoveMemberGroup(nil, groupId, otherId)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400392 if err != nil {
gio134be722025-07-20 19:01:17 +0400393 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400394 http.Redirect(w, r, redirectURL, http.StatusFound)
395 return
396 }
gio134be722025-07-20 19:01:17 +0400397 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400398}
399
DTabidze078385f2024-03-27 14:49:05 +0400400func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400401 loggedInUser, err := getLoggedInUser(r)
402 if err != nil {
403 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
404 return
405 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400406 vars := mux.Vars(r)
407 username := vars["username"]
gio134be722025-07-20 19:01:17 +0400408 groupId := normalizeGroupId(vars["groupId"])
409 if err := isValidGroupId(groupId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400410 http.Error(w, err.Error(), http.StatusBadRequest)
411 return
DTabidze2b224bf2024-03-27 13:25:49 +0400412 }
gio134be722025-07-20 19:01:17 +0400413 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
414 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400415 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
416 return
417 }
gio134be722025-07-20 19:01:17 +0400418 err = s.store.RemoveOwnerUser(nil, groupId, username)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400419 if err != nil {
gio134be722025-07-20 19:01:17 +0400420 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400421 http.Redirect(w, r, redirectURL, http.StatusFound)
422 return
423 }
gio134be722025-07-20 19:01:17 +0400424 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400425}
426
DTabidze078385f2024-03-27 14:49:05 +0400427func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
428 loggedInUser, err := getLoggedInUser(r)
429 if err != nil {
430 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
431 return
432 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400433 vars := mux.Vars(r)
434 username := vars["username"]
gio134be722025-07-20 19:01:17 +0400435 groupId := normalizeGroupId(vars["groupId"])
436 if err := isValidGroupId(groupId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400437 http.Error(w, err.Error(), http.StatusBadRequest)
438 return
DTabidze078385f2024-03-27 14:49:05 +0400439 }
gio134be722025-07-20 19:01:17 +0400440 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
441 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400442 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
443 return
444 }
gio134be722025-07-20 19:01:17 +0400445 err = s.store.RemoveMemberUser(nil, groupId, username)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400446 if err != nil {
gio134be722025-07-20 19:01:17 +0400447 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400448 http.Redirect(w, r, redirectURL, http.StatusFound)
449 return
450 }
gio134be722025-07-20 19:01:17 +0400451 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze078385f2024-03-27 14:49:05 +0400452}
453
454func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +0400455 loggedInUser, err := getLoggedInUser(r)
456 if err != nil {
457 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
458 return
459 }
DTabidze078385f2024-03-27 14:49:05 +0400460 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400461 groupId := normalizeGroupId(vars["groupId"])
462 if err := isValidGroupId(groupId); err != nil {
DTabidze908bb852024-03-25 20:07:57 +0400463 http.Error(w, err.Error(), http.StatusBadRequest)
464 return
465 }
gio134be722025-07-20 19:01:17 +0400466 userId := strings.ToLower(r.FormValue("userId"))
467 if userId == "" {
DTabidze908bb852024-03-25 20:07:57 +0400468 http.Error(w, "Username parameter is required", http.StatusBadRequest)
469 return
470 }
DTabidze0d802592024-03-19 17:42:45 +0400471 status, err := convertStatus(r.FormValue("status"))
472 if err != nil {
473 http.Error(w, err.Error(), http.StatusBadRequest)
474 return
475 }
gio134be722025-07-20 19:01:17 +0400476 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
477 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400478 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400479 return
480 }
481 switch status {
482 case Owner:
gio134be722025-07-20 19:01:17 +0400483 err = s.store.AddOwnerUser(nil, groupId, userId)
DTabidze0d802592024-03-19 17:42:45 +0400484 case Member:
gio134be722025-07-20 19:01:17 +0400485 err = s.store.AddMemberUser(nil, groupId, userId)
DTabidze0d802592024-03-19 17:42:45 +0400486 default:
487 http.Error(w, "Invalid status", http.StatusBadRequest)
488 return
489 }
490 if err != nil {
gio134be722025-07-20 19:01:17 +0400491 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
DTabidze4b44ff42024-04-02 03:16:26 +0400492 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400493 return
494 }
gio134be722025-07-20 19:01:17 +0400495 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400496}
497
498func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
499 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
DTabidze0d802592024-03-19 17:42:45 +0400500 loggedInUser, err := getLoggedInUser(r)
501 if err != nil {
502 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
503 return
504 }
DTabidze078385f2024-03-27 14:49:05 +0400505 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400506 groupId := normalizeGroupId(vars["groupId"])
507 otherId := r.FormValue("otherId")
508 if err := isValidGroupId(groupId); err != nil {
DTabidze908bb852024-03-25 20:07:57 +0400509 http.Error(w, err.Error(), http.StatusBadRequest)
510 return
511 }
gio134be722025-07-20 19:01:17 +0400512 if err := isValidGroupId(otherId); err != nil {
DTabidze908bb852024-03-25 20:07:57 +0400513 http.Error(w, err.Error(), http.StatusBadRequest)
514 return
515 }
gio134be722025-07-20 19:01:17 +0400516 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
517 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400518 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400519 return
520 }
gio134be722025-07-20 19:01:17 +0400521 if err := s.store.AddMemberGroup(nil, groupId, otherId); err != nil {
522 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
DTabidze4b44ff42024-04-02 03:16:26 +0400523 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400524 return
525 }
gio134be722025-07-20 19:01:17 +0400526 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400527}
528
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400529func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400530 loggedInUser, err := getLoggedInUser(r)
531 if err != nil {
532 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
533 return
534 }
535 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400536 groupId := normalizeGroupId(vars["groupId"])
537 otherId := r.FormValue("otherId")
538 if err := isValidGroupId(groupId); err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400539 http.Error(w, err.Error(), http.StatusBadRequest)
540 return
541 }
gio134be722025-07-20 19:01:17 +0400542 if err := isValidGroupId(otherId); err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400543 http.Error(w, err.Error(), http.StatusBadRequest)
544 return
545 }
gio134be722025-07-20 19:01:17 +0400546 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
547 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400548 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
549 return
550 }
gio134be722025-07-20 19:01:17 +0400551 if err := s.store.AddOwnerGroup(nil, groupId, otherId); err != nil {
552 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400553 http.Redirect(w, r, redirectURL, http.StatusFound)
554 return
555 }
gio134be722025-07-20 19:01:17 +0400556 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400557}
558
Davit Tabidze75d57c32024-07-19 19:17:55 +0400559func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
560 defer s.pingAllSyncAddresses()
561 loggedInUser, err := getLoggedInUser(r)
562 if err != nil {
563 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
564 return
565 }
566 vars := mux.Vars(r)
567 username := vars["username"]
568 if loggedInUser != username {
569 http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
570 return
571 }
572 sshKey := r.FormValue("ssh-key")
573 if sshKey == "" {
574 http.Error(w, "SSH key not present", http.StatusBadRequest)
575 return
576 }
gio134be722025-07-20 19:01:17 +0400577 if err := s.store.AddUserPublicKey(nil, strings.ToLower(username), sshKey); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400578 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
579 http.Redirect(w, r, redirectURL, http.StatusFound)
580 return
581 }
582 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
583}
584
585func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
586 defer s.pingAllSyncAddresses()
587 loggedInUser, err := getLoggedInUser(r)
588 if err != nil {
589 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
590 return
591 }
592 vars := mux.Vars(r)
593 username := vars["username"]
594 if loggedInUser != username {
595 http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
596 return
597 }
598 if err := r.ParseForm(); err != nil {
599 http.Error(w, "Invalid request body", http.StatusBadRequest)
600 return
601 }
602 sshKey := r.FormValue("ssh-key")
603 if sshKey == "" {
604 http.Error(w, "SSH key not present", http.StatusBadRequest)
605 return
606 }
gio134be722025-07-20 19:01:17 +0400607 if err := s.store.RemoveUserPublicKey(nil, username, sshKey); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400608 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
609 http.Redirect(w, r, redirectURL, http.StatusFound)
610 return
611 }
612 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
613}
614
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400615type initRequest struct {
gio134be722025-07-20 19:01:17 +0400616 User User `json:"user"`
617 Groups []Group `json:"groups"`
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400618}
619
620func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
621 var req initRequest
622 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
623 http.Error(w, err.Error(), http.StatusBadRequest)
624 return
625 }
gio134be722025-07-20 19:01:17 +0400626 if err := s.store.Init(req.User, req.Groups); err != nil {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400627 http.Error(w, err.Error(), http.StatusInternalServerError)
628 return
629 }
630}
631
632type userInfo struct {
gio134be722025-07-20 19:01:17 +0400633 CanActAs []Group `json:"canActAs"`
634 OwnerOf []Group `json:"ownerOf"`
DTabidzed7744a62024-03-20 14:09:15 +0400635}
636
637func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
638 vars := mux.Vars(r)
639 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400640 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400641 http.Error(w, "Username parameter is required", http.StatusBadRequest)
642 return
643 }
DTabidze908bb852024-03-25 20:07:57 +0400644 user = strings.ToLower(user)
gio134be722025-07-20 19:01:17 +0400645 transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
DTabidzed7744a62024-03-20 14:09:15 +0400646 if err != nil {
647 http.Error(w, err.Error(), http.StatusInternalServerError)
648 return
649 }
gio134be722025-07-20 19:01:17 +0400650 owned, err := s.store.GetGroupsUserOwns(nil, user)
DTabidzed7744a62024-03-20 14:09:15 +0400651 w.Header().Set("Content-Type", "application/json")
gio134be722025-07-20 19:01:17 +0400652 if err := json.NewEncoder(w).Encode(userInfo{transitiveGroups, owned}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400653 http.Error(w, err.Error(), http.StatusInternalServerError)
654 return
655 }
656}
657
Davit Tabidze75d57c32024-07-19 19:17:55 +0400658func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400659 s.addSyncAddress(r.FormValue("selfAddress"))
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400660 var users []User
661 var err error
662 groups := r.FormValue("groups")
663 if groups == "" {
gio134be722025-07-20 19:01:17 +0400664 users, err = s.store.GetAllUsers(nil)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400665 } else {
666 uniqueUsers := make(map[string]struct{})
667 g := strings.Split(groups, ",")
668 uniqueTG := make(map[string]struct{})
669 for _, group := range g {
670 uniqueTG[group] = struct{}{}
gio134be722025-07-20 19:01:17 +0400671 trGroups, err := s.store.GetGroupsGroupCanActAs(nil, group)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400672 if err != nil {
673 http.Error(w, err.Error(), http.StatusInternalServerError)
674 return
675 }
676 for _, tg := range trGroups {
gio134be722025-07-20 19:01:17 +0400677 uniqueTG[tg.Title] = struct{}{}
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400678 }
679 }
680 for group := range uniqueTG {
gio134be722025-07-20 19:01:17 +0400681 u, err := s.store.GetMemberUsers(nil, group)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400682 if err != nil {
683 http.Error(w, err.Error(), http.StatusInternalServerError)
684 return
685 }
686 for _, user := range u {
gio134be722025-07-20 19:01:17 +0400687 uniqueUsers[user.Username] = struct{}{}
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400688 }
689 }
690 usernames := make([]string, 0, len(uniqueUsers))
691 for username := range uniqueUsers {
692 usernames = append(usernames, username)
693 }
gio134be722025-07-20 19:01:17 +0400694 users, err = s.store.GetUsers(nil, usernames)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400695 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400696 if err != nil {
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400697 http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400698 return
699 }
gio2e6a87b2026-07-29 14:00:05 +0400700 for i, user := range users {
701 sshKeys, err := s.store.GetUserPublicKeys(nil, user.Id)
702 if err != nil {
703 http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
704 return
705 }
706 users[i].SSHPublicKeys = sshKeys
707 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400708 w.Header().Set("Content-Type", "application/json")
709 if err := json.NewEncoder(w).Encode(users); err != nil {
710 http.Error(w, err.Error(), http.StatusInternalServerError)
711 return
712 }
713}
714
715func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
716 defer s.pingAllSyncAddresses()
gio134be722025-07-20 19:01:17 +0400717 var req User
gio2728e402024-08-01 18:14:21 +0400718 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400719 http.Error(w, "Invalid request body", http.StatusBadRequest)
720 return
721 }
gio134be722025-07-20 19:01:17 +0400722 if req.Id == "" {
723 http.Error(w, "Id cannot be empty", http.StatusBadRequest)
724 return
725 }
726 if req.Username == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400727 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
728 return
729 }
gio2728e402024-08-01 18:14:21 +0400730 if req.Email == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400731 http.Error(w, "Email cannot be empty", http.StatusBadRequest)
732 return
733 }
gio134be722025-07-20 19:01:17 +0400734 user := User{Id: req.Id, Username: strings.ToLower(req.Username), Email: strings.ToLower(req.Email)}
735 if err := s.store.CreateUser(nil, user); err != nil {
736 http.Error(w, err.Error(), http.StatusInternalServerError)
737 return
738 }
739}
740
741type createGroupRequest struct {
742 UserId string `json:"userId"`
743 GroupId string `json:"groupId"`
744 Title string `json:"title"`
745 Description string `json:"description"`
746}
747
748func (s *Server) apiCreateGroup(w http.ResponseWriter, r *http.Request) {
749 var req createGroupRequest
750 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
751 http.Error(w, "Invalid request body", http.StatusBadRequest)
752 return
753 }
754 if err := s.store.CreateGroup(nil, req.UserId, Group{req.GroupId, req.Title, req.Description}); err != nil {
755 http.Error(w, err.Error(), http.StatusInternalServerError)
756 return
757 }
758}
759
760type apiGetGroupReq struct {
761 GroupId string `json:"groupId"`
762}
763
764// TODO(gio): update client
765type apiGetGroupResp struct {
766 Self Group `json:"self"`
767 Members []User `json:"members"`
768 Owners []User `json:"owners"`
769 MemberGroups []Group `json:"memberGroups"`
770 OwnerGroups []Group `json:"ownerGroups"`
771}
772
773func (s *Server) apiGetGroup(w http.ResponseWriter, r *http.Request) {
774 var req apiGetGroupReq
775 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
776 http.Error(w, "missing group name", http.StatusBadRequest)
777 return
778 }
779 group, err := s.store.GetGroup(nil, req.GroupId)
780 if err != nil {
781 http.Error(w, err.Error(), http.StatusInternalServerError)
782 return
783 }
784 members, err := s.store.GetMemberUsers(nil, req.GroupId)
785 if err != nil {
786 http.Error(w, err.Error(), http.StatusInternalServerError)
787 return
788 }
789 owners, err := s.store.GetOwnerUsers(nil, req.GroupId)
790 if err != nil {
791 http.Error(w, err.Error(), http.StatusInternalServerError)
792 return
793 }
794 memberGroups, err := s.store.GetMemberGroups(nil, req.GroupId)
795 if err != nil {
796 http.Error(w, err.Error(), http.StatusInternalServerError)
797 return
798 }
799 ownerGroups, err := s.store.GetOwnerGroups(nil, req.GroupId)
800 if err != nil {
801 http.Error(w, err.Error(), http.StatusInternalServerError)
802 return
803 }
804 if err := json.NewEncoder(w).Encode(apiGetGroupResp{group, members, owners, memberGroups, ownerGroups}); err != nil {
805 http.Error(w, err.Error(), http.StatusInternalServerError)
806 return
807 }
808}
809
810type userGroupStatus struct {
811 GroupId string `json:"groupId"`
812 UserId string `json:"userId"`
813 Status string `json:"status"`
814}
815
816func (s *Server) apiAddUserToGroup(w http.ResponseWriter, r *http.Request) {
817 var req userGroupStatus
818 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
819 http.Error(w, "Invalid request body", http.StatusBadRequest)
820 return
821 }
822 status, err := convertStatus(req.Status)
823 if err != nil {
824 http.Error(w, "Invalid status", http.StatusBadRequest)
825 return
826 }
827 fmt.Printf("AAA %+v\n", req)
828 switch status {
829 case Member:
830 err = s.store.AddMemberUser(nil, req.GroupId, req.UserId)
831 case Owner:
832 err = s.store.AddOwnerUser(nil, req.GroupId, req.UserId)
833 }
834 if err != nil {
835 http.Error(w, err.Error(), http.StatusInternalServerError)
836 return
837 }
838}
839
840func (s *Server) apiRemoveUserFromGroup(w http.ResponseWriter, r *http.Request) {
841 var req userGroupStatus
842 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
843 http.Error(w, "Invalid request body", http.StatusBadRequest)
844 return
845 }
846 status, err := convertStatus(req.Status)
847 if err != nil {
848 http.Error(w, "Invalid status", http.StatusBadRequest)
849 return
850 }
851 switch status {
852 case Member:
853 err = s.store.RemoveMemberUser(nil, req.GroupId, req.UserId)
854 case Owner:
855 err = s.store.RemoveOwnerUser(nil, req.GroupId, req.UserId)
856 }
857 if err != nil {
858 http.Error(w, err.Error(), http.StatusInternalServerError)
859 return
860 }
861}
862
863type groupGroupStatus struct {
864 GroupId string `json:"groupId"`
865 OtherId string `json:"otherId"`
866 Status string `json:"status"`
867}
868
869func (s *Server) apiAddGroupToGroup(w http.ResponseWriter, r *http.Request) {
870 var req groupGroupStatus
871 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
872 http.Error(w, "Invalid request body", http.StatusBadRequest)
873 return
874 }
875 status, err := convertStatus(req.Status)
876 if err != nil {
877 http.Error(w, "Invalid status", http.StatusBadRequest)
878 return
879 }
880 switch status {
881 case Member:
882 err = s.store.AddMemberGroup(nil, req.GroupId, req.OtherId)
883 case Owner:
884 err = s.store.AddOwnerGroup(nil, req.GroupId, req.OtherId)
885 }
886 if err != nil {
887 http.Error(w, err.Error(), http.StatusInternalServerError)
888 return
889 }
890}
891
892func (s *Server) apiRemoveGroupFromGroup(w http.ResponseWriter, r *http.Request) {
893 var req groupGroupStatus
894 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
895 http.Error(w, "Invalid request body", http.StatusBadRequest)
896 return
897 }
898 status, err := convertStatus(req.Status)
899 if err != nil {
900 http.Error(w, "Invalid status", http.StatusBadRequest)
901 return
902 }
903 switch status {
904 case Member:
905 err = s.store.RemoveMemberGroup(nil, req.GroupId, req.OtherId)
906 case Owner:
907 err = s.store.RemoveOwnerGroup(nil, req.GroupId, req.OtherId)
908 }
909 if err != nil {
910 http.Error(w, err.Error(), http.StatusInternalServerError)
911 return
912 }
913}
914
915type getGroupsOwnedByRequest struct {
916 User string `json:"user"`
917}
918
919type getGroupsOwnedByResponse struct {
920 Groups []string `json:"groups"`
921}
922
923func (s *Server) apiGetUserOwnedGroups(w http.ResponseWriter, r *http.Request) {
924 var req getGroupsOwnedByRequest
925 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
926 http.Error(w, err.Error(), http.StatusBadRequest)
927 return
928 }
929 groups, err := s.store.GetGroupsUserOwns(nil, req.User)
930 if err != nil {
931 http.Error(w, err.Error(), http.StatusInternalServerError)
932 return
933 }
934 var resp getGroupsOwnedByResponse
935 for _, g := range groups {
936 resp.Groups = append(resp.Groups, g.Title)
937 }
938 if err := json.NewEncoder(w).Encode(resp); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400939 http.Error(w, err.Error(), http.StatusInternalServerError)
940 return
941 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400942}
943
gio7fbd4ad2024-08-27 10:06:39 +0400944type addUserKeyRequest struct {
945 User string `json:"user"`
946 PublicKey string `json:"publicKey"`
947}
948
949func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
950 var req addUserKeyRequest
951 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
952 http.Error(w, "Invalid request body", http.StatusBadRequest)
953 return
954 }
955 if req.User == "" {
956 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
957 return
958 }
959 if req.PublicKey == "" {
960 http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
961 return
962 }
gio134be722025-07-20 19:01:17 +0400963 if err := s.store.AddUserPublicKey(nil, strings.ToLower(req.User), req.PublicKey); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +0400964 http.Error(w, err.Error(), http.StatusInternalServerError)
965 return
966 }
967}
968
969// TODO(gio): enque sync event instead of directly reaching out to clients.
970// This will allow to deduplicate sync events and save resources.
Davit Tabidze75d57c32024-07-19 19:17:55 +0400971func (s *Server) pingAllSyncAddresses() {
972 s.mu.Lock()
973 defer s.mu.Unlock()
974 for address := range s.syncAddresses {
gio7fbd4ad2024-08-27 10:06:39 +0400975 go func(address string) {
976 log.Printf("Pinging %s", address)
977 resp, err := http.Get(address)
978 if err != nil {
979 // TODO(gio): remove sync address after N number of failures.
980 log.Printf("Failed to ping %s: %v", address, err)
981 return
982 }
983 defer resp.Body.Close()
984 if resp.StatusCode != http.StatusOK {
985 log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
986 }
987 }(address)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400988 }
989}
990
991func (s *Server) addSyncAddress(address string) {
gio7fbd4ad2024-08-27 10:06:39 +0400992 if address == "" {
993 return
994 }
995 fmt.Printf("Adding sync address: %s\n", address)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400996 s.mu.Lock()
997 defer s.mu.Unlock()
998 s.syncAddresses[address] = struct{}{}
999}
1000
DTabidze908bb852024-03-25 20:07:57 +04001001func convertStatus(status string) (Status, error) {
gio134be722025-07-20 19:01:17 +04001002 switch strings.ToLower(status) {
1003 case "owner":
DTabidze908bb852024-03-25 20:07:57 +04001004 return Owner, nil
gio134be722025-07-20 19:01:17 +04001005 case "member":
DTabidze908bb852024-03-25 20:07:57 +04001006 return Member, nil
1007 default:
1008 return Owner, fmt.Errorf("invalid status: %s", status)
1009 }
1010}
1011
gio134be722025-07-20 19:01:17 +04001012func isValidGroupId(group string) error {
DTabidze908bb852024-03-25 20:07:57 +04001013 if strings.TrimSpace(group) == "" {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001014 return fmt.Errorf("Group name can't be empty or contain only whitespaces")
DTabidze908bb852024-03-25 20:07:57 +04001015 }
1016 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1017 if !validGroupName.MatchString(group) {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001018 return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
DTabidze908bb852024-03-25 20:07:57 +04001019 }
1020 return nil
1021}
1022
DTabidze0d802592024-03-19 17:42:45 +04001023func main() {
1024 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001025 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001026 if err != nil {
1027 panic(err)
1028 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001029 store, err := NewSQLiteStore(db)
1030 if err != nil {
1031 panic(err)
1032 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001033 s := Server{
1034 store: store,
1035 syncAddresses: make(map[string]struct{}),
1036 mu: sync.Mutex{},
1037 }
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001038 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001039}
gio134be722025-07-20 19:01:17 +04001040
1041func normalizeGroupId(groupId string) string {
1042 if strings.Contains(groupId, "/") && !strings.HasPrefix(groupId, "/") {
1043 return fmt.Sprintf("/%s", groupId)
1044 }
1045 return groupId
1046}