blob: 8322933c73a1c42361887e6cfdff58b446254a6e [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"`
Davit Tabidze75d57c32024-07-19 19:17:55 +040058}
59
DTabidze0d802592024-03-19 17:42:45 +040060func getLoggedInUser(r *http.Request) (string, error) {
gio134be722025-07-20 19:01:17 +040061 if user := r.Header.Get("X-Forwarded-UserId"); user != "" {
DTabidzec0b4d8f2024-03-22 17:25:10 +040062 return user, nil
63 } else {
64 return "", fmt.Errorf("unauthenticated")
65 }
gio134be722025-07-20 19:01:17 +040066 // return "0063f4b6-29cb-4bd6-b8ce-1f6b203d490f", nil
DTabidze0d802592024-03-19 17:42:45 +040067}
68
69type Status int
70
71const (
72 Owner Status = iota
73 Member
74)
75
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040076func (s *Server) Start() error {
77 e := make(chan error)
78 go func() {
79 r := mux.NewRouter()
gio4a297752025-07-15 13:24:57 +040080 r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticResources))})
gio134be722025-07-20 19:01:17 +040081 r.HandleFunc("/group/{groupId:.*}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
82 r.HandleFunc("/group/{groupId:.*}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
83 r.HandleFunc("/group/{groupId:.*}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
84 r.HandleFunc("/group/{groupId:.*}/remove-child-group/{otherId:.*}", s.removeChildGroupHandler).Methods(http.MethodPost)
85 r.HandleFunc("/group/{groupId:.*}/remove-owner/{username}", s.removeOwnerFromGroupHandler).Methods(http.MethodPost)
86 r.HandleFunc("/group/{groupId:.*}/remove-member/{username}", s.removeMemberFromGroupHandler).Methods(http.MethodPost)
87 r.HandleFunc("/group/{groupId:.*}", s.groupHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +040088 r.HandleFunc("/user/{username}/ssh-key", s.addSSHKeyForUserHandler).Methods(http.MethodPost)
89 r.HandleFunc("/user/{username}/remove-ssh-key", s.removeSSHKeyForUserHandler).Methods(http.MethodPost)
DTabidze5d735e32024-03-26 16:01:06 +040090 r.HandleFunc("/user/{username}", s.userHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +040091 r.HandleFunc("/create-group", s.createGroupHandler).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +040092 r.HandleFunc("/", s.homePageHandler)
93 e <- http.ListenAndServe(fmt.Sprintf(":%d", *port), r)
94 }()
95 go func() {
96 r := mux.NewRouter()
97 r.HandleFunc("/api/init", s.apiInitHandler)
gio7fbd4ad2024-08-27 10:06:39 +040098 // TODO(gio): change to /api/users/{username}
99 r.HandleFunc("/api/users/{username}/keys", s.apiAddUserKey).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400100 r.HandleFunc("/api/user/{username}", s.apiMemberOfHandler)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400101 r.HandleFunc("/api/users", s.apiGetAllUsers).Methods(http.MethodGet)
102 r.HandleFunc("/api/users", s.apiCreateUser).Methods(http.MethodPost)
gio134be722025-07-20 19:01:17 +0400103 r.HandleFunc("/api/group", s.apiCreateGroup).Methods(http.MethodPost)
104 r.HandleFunc("/api/add-group-user", s.apiAddUserToGroup).Methods(http.MethodPost)
105 r.HandleFunc("/api/remove-group-user", s.apiRemoveUserFromGroup).Methods(http.MethodPost)
106 r.HandleFunc("/api/add-group-group", s.apiAddGroupToGroup).Methods(http.MethodPost)
107 r.HandleFunc("/api/remove-group-group", s.apiRemoveGroupFromGroup).Methods(http.MethodPost)
108 r.HandleFunc("/api/get-group", s.apiGetGroup).Methods(http.MethodPost)
Giorgi Lekveishvili329af572024-03-25 20:14:41 +0400109 e <- http.ListenAndServe(fmt.Sprintf(":%d", *apiPort), r)
110 }()
111 return <-e
DTabidze0d802592024-03-19 17:42:45 +0400112}
113
114type GroupData struct {
115 Group Group
116 Membership string
117}
118
gio134be722025-07-20 19:01:17 +0400119func (s *Server) checkIsOwner(w http.ResponseWriter, userId, groupId string) error {
120 ownerUsers, err := s.store.GetOwnerUsers(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400121 if err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400122 return err
DTabidze0d802592024-03-19 17:42:45 +0400123 }
gio134be722025-07-20 19:01:17 +0400124 if slices.ContainsFunc(ownerUsers, func(u User) bool {
125 return u.Id == userId
126 }) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400127 return nil
DTabidze0d802592024-03-19 17:42:45 +0400128 }
gio134be722025-07-20 19:01:17 +0400129 ownerIds, err := s.store.GetOwnerGroups(nil, groupId)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400130 if err != nil {
131 return err
132 }
gio134be722025-07-20 19:01:17 +0400133 canActAs, err := s.store.GetGroupsUserCanActAs(nil, userId)
134 if err != nil {
135 return err
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400136 }
gio134be722025-07-20 19:01:17 +0400137 for _, g := range canActAs {
138 if slices.Index(ownerIds, g) != -1 {
139 return nil
140 }
141 }
142 return fmt.Errorf("not an owner")
DTabidze0d802592024-03-19 17:42:45 +0400143}
144
DTabidze4b44ff42024-04-02 03:16:26 +0400145type templates struct {
146 group *template.Template
147 user *template.Template
148}
149
150func parseTemplates(fs embed.FS) (templates, error) {
151 base, err := template.ParseFS(fs, "memberships-tmpl/base.html")
152 if err != nil {
153 return templates{}, err
154 }
155 parse := func(path string) (*template.Template, error) {
156 if b, err := base.Clone(); err != nil {
157 return nil, err
158 } else {
159 return b.ParseFS(fs, path)
160 }
161 }
162 user, err := parse("memberships-tmpl/user.html")
163 if err != nil {
164 return templates{}, err
165 }
166 group, err := parse("memberships-tmpl/group.html")
167 if err != nil {
168 return templates{}, err
169 }
170 return templates{group, user}, nil
171}
172
DTabidze0d802592024-03-19 17:42:45 +0400173func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
174 loggedInUser, err := getLoggedInUser(r)
175 if err != nil {
176 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
177 return
178 }
DTabidze5d735e32024-03-26 16:01:06 +0400179 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
180}
181
Davit Tabidze75d57c32024-07-19 19:17:55 +0400182type UserPageData struct {
gio134be722025-07-20 19:01:17 +0400183 User User
Davit Tabidze75d57c32024-07-19 19:17:55 +0400184 OwnerGroups []Group
185 MembershipGroups []Group
186 TransitiveGroups []Group
187 LoggedInUserPage bool
Davit Tabidze75d57c32024-07-19 19:17:55 +0400188 SSHPublicKeys []string
Davit Tabidze75d57c32024-07-19 19:17:55 +0400189 ErrorMessage string
190}
191
DTabidze5d735e32024-03-26 16:01:06 +0400192func (s *Server) userHandler(w http.ResponseWriter, r *http.Request) {
193 loggedInUser, err := getLoggedInUser(r)
194 if err != nil {
195 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
196 return
197 }
DTabidze4b44ff42024-04-02 03:16:26 +0400198 errorMsg := r.URL.Query().Get("errorMessage")
DTabidze5d735e32024-03-26 16:01:06 +0400199 vars := mux.Vars(r)
200 user := strings.ToLower(vars["username"])
201 // TODO(dtabidze): should check if username exists or not.
202 loggedInUserPage := loggedInUser == user
gio134be722025-07-20 19:01:17 +0400203 ownerGroups, err := s.store.GetGroupsUserOwns(nil, user)
DTabidze0d802592024-03-19 17:42:45 +0400204 if err != nil {
205 http.Error(w, err.Error(), http.StatusInternalServerError)
206 return
207 }
gio134be722025-07-20 19:01:17 +0400208 membershipGroups, err := s.store.GetGroupsUserIsMemberOf(nil, user)
DTabidze0d802592024-03-19 17:42:45 +0400209 if err != nil {
210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
212 }
gio134be722025-07-20 19:01:17 +0400213 transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400214 if err != nil {
215 http.Error(w, err.Error(), http.StatusInternalServerError)
216 return
217 }
gio134be722025-07-20 19:01:17 +0400218 userInfo, err := s.store.GetUser(nil, user)
219 if err != nil {
220 http.Error(w, err.Error(), http.StatusInternalServerError)
221 return
222 }
223 sshPublicKeys, err := s.store.GetUserPublicKeys(nil, user)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400224 if err != nil {
225 http.Error(w, err.Error(), http.StatusInternalServerError)
226 return
227 }
228 data := UserPageData{
gio134be722025-07-20 19:01:17 +0400229 User: userInfo,
DTabidze0d802592024-03-19 17:42:45 +0400230 OwnerGroups: ownerGroups,
231 MembershipGroups: membershipGroups,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400232 TransitiveGroups: transitiveGroups,
DTabidze5d735e32024-03-26 16:01:06 +0400233 LoggedInUserPage: loggedInUserPage,
gio134be722025-07-20 19:01:17 +0400234 SSHPublicKeys: sshPublicKeys,
DTabidze4b44ff42024-04-02 03:16:26 +0400235 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400236 }
DTabidze4b44ff42024-04-02 03:16:26 +0400237 templates, err := parseTemplates(tmpls)
238 if err != nil {
239 http.Error(w, err.Error(), http.StatusInternalServerError)
240 return
241 }
242 if err := templates.user.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400243 http.Error(w, err.Error(), http.StatusInternalServerError)
244 return
245 }
246}
247
248func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
249 loggedInUser, err := getLoggedInUser(r)
250 if err != nil {
251 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
252 return
253 }
DTabidze0d802592024-03-19 17:42:45 +0400254 if err := r.ParseForm(); err != nil {
255 http.Error(w, err.Error(), http.StatusInternalServerError)
256 return
257 }
258 var group Group
gio134be722025-07-20 19:01:17 +0400259 group.Id = r.PostFormValue("id")
260 group.Title = r.PostFormValue("title")
261 group.Description = r.PostFormValue("description")
262 if err := isValidGroupId(group.Id); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400263 // http.Error(w, err.Error(), http.StatusBadRequest)
264 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
265 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze908bb852024-03-25 20:07:57 +0400266 return
267 }
gio134be722025-07-20 19:01:17 +0400268 if err := s.store.CreateGroup(nil, loggedInUser, group); err != nil {
DTabidze4b44ff42024-04-02 03:16:26 +0400269 // http.Error(w, err.Error(), http.StatusInternalServerError)
270 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
271 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400272 return
273 }
274 http.Redirect(w, r, "/", http.StatusSeeOther)
275}
276
Davit Tabidze75d57c32024-07-19 19:17:55 +0400277type GroupPageData struct {
gio134be722025-07-20 19:01:17 +0400278 GroupId string
279 Title string
Davit Tabidze75d57c32024-07-19 19:17:55 +0400280 Description string
gio134be722025-07-20 19:01:17 +0400281 Owners []User
282 Members []User
Davit Tabidze75d57c32024-07-19 19:17:55 +0400283 AllGroups []Group
gio134be722025-07-20 19:01:17 +0400284 AllUsers []User
Davit Tabidze75d57c32024-07-19 19:17:55 +0400285 TransitiveGroups []Group
286 ChildGroups []Group
287 OwnerGroups []Group
288 ErrorMessage string
289}
290
DTabidze0d802592024-03-19 17:42:45 +0400291func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
DTabidzec0b4d8f2024-03-22 17:25:10 +0400292 _, err := getLoggedInUser(r)
293 if err != nil {
294 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
295 return
296 }
DTabidze4b44ff42024-04-02 03:16:26 +0400297 errorMsg := r.URL.Query().Get("errorMessage")
DTabidzed7744a62024-03-20 14:09:15 +0400298 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400299 groupId := normalizeGroupId(vars["groupId"])
DTabidze908bb852024-03-25 20:07:57 +0400300 if err != nil {
301 http.Error(w, err.Error(), http.StatusInternalServerError)
302 return
303 }
gio134be722025-07-20 19:01:17 +0400304 g, err := s.store.GetGroup(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400305 if err != nil {
306 http.Error(w, err.Error(), http.StatusInternalServerError)
307 return
308 }
gio134be722025-07-20 19:01:17 +0400309 owners, err := s.store.GetOwnerUsers(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400310 if err != nil {
311 http.Error(w, err.Error(), http.StatusInternalServerError)
312 return
313 }
gio134be722025-07-20 19:01:17 +0400314 members, err := s.store.GetMemberUsers(nil, groupId)
DTabidze0d802592024-03-19 17:42:45 +0400315 if err != nil {
316 http.Error(w, err.Error(), http.StatusInternalServerError)
317 return
318 }
gio134be722025-07-20 19:01:17 +0400319 allUsers, err := s.store.GetAllUsers(nil)
DTabidze0d802592024-03-19 17:42:45 +0400320 if err != nil {
321 http.Error(w, err.Error(), http.StatusInternalServerError)
322 return
323 }
gio134be722025-07-20 19:01:17 +0400324 allGroups, err := s.store.GetAllGroups(nil)
DTabidze0d802592024-03-19 17:42:45 +0400325 if err != nil {
326 http.Error(w, err.Error(), http.StatusInternalServerError)
327 return
328 }
gio134be722025-07-20 19:01:17 +0400329 transitiveGroups, err := s.store.GetGroupsGroupCanActAs(nil, groupId)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400330 if err != nil {
331 http.Error(w, err.Error(), http.StatusInternalServerError)
332 return
333 }
gio134be722025-07-20 19:01:17 +0400334 childGroups, err := s.store.GetMemberGroups(nil, groupId)
DTabidzec0b4d8f2024-03-22 17:25:10 +0400335 if err != nil {
336 http.Error(w, err.Error(), http.StatusInternalServerError)
337 return
338 }
gio134be722025-07-20 19:01:17 +0400339 ownerGroups, err := s.store.GetOwnerGroups(nil, groupId)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400340 if err != nil {
341 http.Error(w, err.Error(), http.StatusInternalServerError)
342 return
343 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400344 data := GroupPageData{
gio134be722025-07-20 19:01:17 +0400345 GroupId: groupId,
346 Title: g.Title,
347 Description: g.Description,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400348 Owners: owners,
349 Members: members,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400350 AllGroups: allGroups,
gio134be722025-07-20 19:01:17 +0400351 AllUsers: allUsers,
DTabidzec0b4d8f2024-03-22 17:25:10 +0400352 TransitiveGroups: transitiveGroups,
353 ChildGroups: childGroups,
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400354 OwnerGroups: ownerGroups,
DTabidze4b44ff42024-04-02 03:16:26 +0400355 ErrorMessage: errorMsg,
DTabidze0d802592024-03-19 17:42:45 +0400356 }
DTabidze4b44ff42024-04-02 03:16:26 +0400357 templates, err := parseTemplates(tmpls)
358 if err != nil {
359 http.Error(w, err.Error(), http.StatusInternalServerError)
360 return
361 }
362 if err := templates.group.Execute(w, data); err != nil {
DTabidze0d802592024-03-19 17:42:45 +0400363 http.Error(w, err.Error(), http.StatusInternalServerError)
364 return
365 }
366}
367
DTabidze2b224bf2024-03-27 13:25:49 +0400368func (s *Server) removeChildGroupHandler(w http.ResponseWriter, r *http.Request) {
369 loggedInUser, err := getLoggedInUser(r)
370 if err != nil {
371 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
372 return
373 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400374 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400375 groupId := normalizeGroupId(vars["groupId"])
376 otherId := vars["otherId"]
377 if err := isValidGroupId(groupId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400378 http.Error(w, err.Error(), http.StatusBadRequest)
379 return
DTabidze2b224bf2024-03-27 13:25:49 +0400380 }
gio134be722025-07-20 19:01:17 +0400381 if err := isValidGroupId(otherId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400382 http.Error(w, err.Error(), http.StatusBadRequest)
383 return
384 }
gio134be722025-07-20 19:01:17 +0400385 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
386 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400387 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
388 return
389 }
gio134be722025-07-20 19:01:17 +0400390 err = s.store.RemoveMemberGroup(nil, groupId, otherId)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400391 if err != nil {
gio134be722025-07-20 19:01:17 +0400392 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400393 http.Redirect(w, r, redirectURL, http.StatusFound)
394 return
395 }
gio134be722025-07-20 19:01:17 +0400396 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400397}
398
DTabidze078385f2024-03-27 14:49:05 +0400399func (s *Server) removeOwnerFromGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze2b224bf2024-03-27 13:25:49 +0400400 loggedInUser, err := getLoggedInUser(r)
401 if err != nil {
402 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
403 return
404 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400405 vars := mux.Vars(r)
406 username := vars["username"]
gio134be722025-07-20 19:01:17 +0400407 groupId := normalizeGroupId(vars["groupId"])
408 if err := isValidGroupId(groupId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400409 http.Error(w, err.Error(), http.StatusBadRequest)
410 return
DTabidze2b224bf2024-03-27 13:25:49 +0400411 }
gio134be722025-07-20 19:01:17 +0400412 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
413 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400414 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
415 return
416 }
gio134be722025-07-20 19:01:17 +0400417 err = s.store.RemoveOwnerUser(nil, groupId, username)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400418 if err != nil {
gio134be722025-07-20 19:01:17 +0400419 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400420 http.Redirect(w, r, redirectURL, http.StatusFound)
421 return
422 }
gio134be722025-07-20 19:01:17 +0400423 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze2b224bf2024-03-27 13:25:49 +0400424}
425
DTabidze078385f2024-03-27 14:49:05 +0400426func (s *Server) removeMemberFromGroupHandler(w http.ResponseWriter, r *http.Request) {
427 loggedInUser, err := getLoggedInUser(r)
428 if err != nil {
429 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
430 return
431 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400432 vars := mux.Vars(r)
433 username := vars["username"]
gio134be722025-07-20 19:01:17 +0400434 groupId := normalizeGroupId(vars["groupId"])
435 if err := isValidGroupId(groupId); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400436 http.Error(w, err.Error(), http.StatusBadRequest)
437 return
DTabidze078385f2024-03-27 14:49:05 +0400438 }
gio134be722025-07-20 19:01:17 +0400439 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
440 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400441 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
442 return
443 }
gio134be722025-07-20 19:01:17 +0400444 err = s.store.RemoveMemberUser(nil, groupId, username)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400445 if err != nil {
gio134be722025-07-20 19:01:17 +0400446 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidze75d57c32024-07-19 19:17:55 +0400447 http.Redirect(w, r, redirectURL, http.StatusFound)
448 return
449 }
gio134be722025-07-20 19:01:17 +0400450 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze078385f2024-03-27 14:49:05 +0400451}
452
453func (s *Server) addUserToGroupHandler(w http.ResponseWriter, r *http.Request) {
DTabidze0d802592024-03-19 17:42:45 +0400454 loggedInUser, err := getLoggedInUser(r)
455 if err != nil {
456 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
457 return
458 }
DTabidze078385f2024-03-27 14:49:05 +0400459 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400460 groupId := normalizeGroupId(vars["groupId"])
461 if err := isValidGroupId(groupId); err != nil {
DTabidze908bb852024-03-25 20:07:57 +0400462 http.Error(w, err.Error(), http.StatusBadRequest)
463 return
464 }
gio134be722025-07-20 19:01:17 +0400465 userId := strings.ToLower(r.FormValue("userId"))
466 if userId == "" {
DTabidze908bb852024-03-25 20:07:57 +0400467 http.Error(w, "Username parameter is required", http.StatusBadRequest)
468 return
469 }
DTabidze0d802592024-03-19 17:42:45 +0400470 status, err := convertStatus(r.FormValue("status"))
471 if err != nil {
472 http.Error(w, err.Error(), http.StatusBadRequest)
473 return
474 }
gio134be722025-07-20 19:01:17 +0400475 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
476 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400477 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400478 return
479 }
480 switch status {
481 case Owner:
gio134be722025-07-20 19:01:17 +0400482 err = s.store.AddOwnerUser(nil, groupId, userId)
DTabidze0d802592024-03-19 17:42:45 +0400483 case Member:
gio134be722025-07-20 19:01:17 +0400484 err = s.store.AddMemberUser(nil, groupId, userId)
DTabidze0d802592024-03-19 17:42:45 +0400485 default:
486 http.Error(w, "Invalid status", http.StatusBadRequest)
487 return
488 }
489 if err != nil {
gio134be722025-07-20 19:01:17 +0400490 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
DTabidze4b44ff42024-04-02 03:16:26 +0400491 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400492 return
493 }
gio134be722025-07-20 19:01:17 +0400494 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400495}
496
497func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
498 // 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 +0400499 loggedInUser, err := getLoggedInUser(r)
500 if err != nil {
501 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
502 return
503 }
DTabidze078385f2024-03-27 14:49:05 +0400504 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400505 groupId := normalizeGroupId(vars["groupId"])
506 otherId := r.FormValue("otherId")
507 if err := isValidGroupId(groupId); err != nil {
DTabidze908bb852024-03-25 20:07:57 +0400508 http.Error(w, err.Error(), http.StatusBadRequest)
509 return
510 }
gio134be722025-07-20 19:01:17 +0400511 if err := isValidGroupId(otherId); err != nil {
DTabidze908bb852024-03-25 20:07:57 +0400512 http.Error(w, err.Error(), http.StatusBadRequest)
513 return
514 }
gio134be722025-07-20 19:01:17 +0400515 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
516 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400517 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400518 return
519 }
gio134be722025-07-20 19:01:17 +0400520 if err := s.store.AddMemberGroup(nil, groupId, otherId); err != nil {
521 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
DTabidze4b44ff42024-04-02 03:16:26 +0400522 http.Redirect(w, r, redirectURL, http.StatusFound)
DTabidze0d802592024-03-19 17:42:45 +0400523 return
524 }
gio134be722025-07-20 19:01:17 +0400525 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
DTabidze0d802592024-03-19 17:42:45 +0400526}
527
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400528func (s *Server) addOwnerGroupHandler(w http.ResponseWriter, r *http.Request) {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400529 loggedInUser, err := getLoggedInUser(r)
530 if err != nil {
531 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
532 return
533 }
534 vars := mux.Vars(r)
gio134be722025-07-20 19:01:17 +0400535 groupId := normalizeGroupId(vars["groupId"])
536 otherId := r.FormValue("otherId")
537 if err := isValidGroupId(groupId); err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400538 http.Error(w, err.Error(), http.StatusBadRequest)
539 return
540 }
gio134be722025-07-20 19:01:17 +0400541 if err := isValidGroupId(otherId); err != nil {
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400542 http.Error(w, err.Error(), http.StatusBadRequest)
543 return
544 }
gio134be722025-07-20 19:01:17 +0400545 if err := s.checkIsOwner(w, loggedInUser, groupId); err != nil {
546 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400547 http.Redirect(w, r, redirectURL, http.StatusSeeOther)
548 return
549 }
gio134be722025-07-20 19:01:17 +0400550 if err := s.store.AddOwnerGroup(nil, groupId, otherId); err != nil {
551 redirectURL := fmt.Sprintf("/group/%s?errorMessage=%s", groupId, url.QueryEscape(err.Error()))
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400552 http.Redirect(w, r, redirectURL, http.StatusFound)
553 return
554 }
gio134be722025-07-20 19:01:17 +0400555 http.Redirect(w, r, "/group/"+groupId, http.StatusSeeOther)
Davit Tabidzec0d2bf52024-04-03 15:39:33 +0400556}
557
Davit Tabidze75d57c32024-07-19 19:17:55 +0400558func (s *Server) addSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
559 defer s.pingAllSyncAddresses()
560 loggedInUser, err := getLoggedInUser(r)
561 if err != nil {
562 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
563 return
564 }
565 vars := mux.Vars(r)
566 username := vars["username"]
567 if loggedInUser != username {
568 http.Error(w, "You are not allowed to add SSH key for someone else", http.StatusUnauthorized)
569 return
570 }
571 sshKey := r.FormValue("ssh-key")
572 if sshKey == "" {
573 http.Error(w, "SSH key not present", http.StatusBadRequest)
574 return
575 }
gio134be722025-07-20 19:01:17 +0400576 if err := s.store.AddUserPublicKey(nil, strings.ToLower(username), sshKey); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400577 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
578 http.Redirect(w, r, redirectURL, http.StatusFound)
579 return
580 }
581 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
582}
583
584func (s *Server) removeSSHKeyForUserHandler(w http.ResponseWriter, r *http.Request) {
585 defer s.pingAllSyncAddresses()
586 loggedInUser, err := getLoggedInUser(r)
587 if err != nil {
588 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
589 return
590 }
591 vars := mux.Vars(r)
592 username := vars["username"]
593 if loggedInUser != username {
594 http.Error(w, "You are not allowed to remove SSH key for someone else", http.StatusUnauthorized)
595 return
596 }
597 if err := r.ParseForm(); err != nil {
598 http.Error(w, "Invalid request body", http.StatusBadRequest)
599 return
600 }
601 sshKey := r.FormValue("ssh-key")
602 if sshKey == "" {
603 http.Error(w, "SSH key not present", http.StatusBadRequest)
604 return
605 }
gio134be722025-07-20 19:01:17 +0400606 if err := s.store.RemoveUserPublicKey(nil, username, sshKey); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400607 redirectURL := fmt.Sprintf("/user/%s?errorMessage=%s", loggedInUser, url.QueryEscape(err.Error()))
608 http.Redirect(w, r, redirectURL, http.StatusFound)
609 return
610 }
611 http.Redirect(w, r, "/user/"+loggedInUser, http.StatusSeeOther)
612}
613
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400614type initRequest struct {
gio134be722025-07-20 19:01:17 +0400615 User User `json:"user"`
616 Groups []Group `json:"groups"`
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400617}
618
619func (s *Server) apiInitHandler(w http.ResponseWriter, r *http.Request) {
620 var req initRequest
621 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
622 http.Error(w, err.Error(), http.StatusBadRequest)
623 return
624 }
gio134be722025-07-20 19:01:17 +0400625 if err := s.store.Init(req.User, req.Groups); err != nil {
Giorgi Lekveishvili942c7612024-03-22 19:27:48 +0400626 http.Error(w, err.Error(), http.StatusInternalServerError)
627 return
628 }
629}
630
631type userInfo struct {
gio134be722025-07-20 19:01:17 +0400632 CanActAs []Group `json:"canActAs"`
633 OwnerOf []Group `json:"ownerOf"`
DTabidzed7744a62024-03-20 14:09:15 +0400634}
635
636func (s *Server) apiMemberOfHandler(w http.ResponseWriter, r *http.Request) {
637 vars := mux.Vars(r)
638 user, ok := vars["username"]
DTabidze908bb852024-03-25 20:07:57 +0400639 if !ok || user == "" {
DTabidzed7744a62024-03-20 14:09:15 +0400640 http.Error(w, "Username parameter is required", http.StatusBadRequest)
641 return
642 }
DTabidze908bb852024-03-25 20:07:57 +0400643 user = strings.ToLower(user)
gio134be722025-07-20 19:01:17 +0400644 transitiveGroups, err := s.store.GetGroupsUserCanActAs(nil, user)
DTabidzed7744a62024-03-20 14:09:15 +0400645 if err != nil {
646 http.Error(w, err.Error(), http.StatusInternalServerError)
647 return
648 }
gio134be722025-07-20 19:01:17 +0400649 owned, err := s.store.GetGroupsUserOwns(nil, user)
DTabidzed7744a62024-03-20 14:09:15 +0400650 w.Header().Set("Content-Type", "application/json")
gio134be722025-07-20 19:01:17 +0400651 if err := json.NewEncoder(w).Encode(userInfo{transitiveGroups, owned}); err != nil {
DTabidzed7744a62024-03-20 14:09:15 +0400652 http.Error(w, err.Error(), http.StatusInternalServerError)
653 return
654 }
655}
656
Davit Tabidze75d57c32024-07-19 19:17:55 +0400657func (s *Server) apiGetAllUsers(w http.ResponseWriter, r *http.Request) {
gio7fbd4ad2024-08-27 10:06:39 +0400658 s.addSyncAddress(r.FormValue("selfAddress"))
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400659 var users []User
660 var err error
661 groups := r.FormValue("groups")
662 if groups == "" {
gio134be722025-07-20 19:01:17 +0400663 users, err = s.store.GetAllUsers(nil)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400664 } else {
665 uniqueUsers := make(map[string]struct{})
666 g := strings.Split(groups, ",")
667 uniqueTG := make(map[string]struct{})
668 for _, group := range g {
669 uniqueTG[group] = struct{}{}
gio134be722025-07-20 19:01:17 +0400670 trGroups, err := s.store.GetGroupsGroupCanActAs(nil, group)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400671 if err != nil {
672 http.Error(w, err.Error(), http.StatusInternalServerError)
673 return
674 }
675 for _, tg := range trGroups {
gio134be722025-07-20 19:01:17 +0400676 uniqueTG[tg.Title] = struct{}{}
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400677 }
678 }
679 for group := range uniqueTG {
gio134be722025-07-20 19:01:17 +0400680 u, err := s.store.GetMemberUsers(nil, group)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400681 if err != nil {
682 http.Error(w, err.Error(), http.StatusInternalServerError)
683 return
684 }
685 for _, user := range u {
gio134be722025-07-20 19:01:17 +0400686 uniqueUsers[user.Username] = struct{}{}
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400687 }
688 }
689 usernames := make([]string, 0, len(uniqueUsers))
690 for username := range uniqueUsers {
691 usernames = append(usernames, username)
692 }
gio134be722025-07-20 19:01:17 +0400693 users, err = s.store.GetUsers(nil, usernames)
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400694 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400695 if err != nil {
Davit Tabidzef867f2d2024-07-24 18:06:25 +0400696 http.Error(w, "Failed to retrieve user infos", http.StatusInternalServerError)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400697 return
698 }
699 w.Header().Set("Content-Type", "application/json")
700 if err := json.NewEncoder(w).Encode(users); err != nil {
701 http.Error(w, err.Error(), http.StatusInternalServerError)
702 return
703 }
704}
705
706func (s *Server) apiCreateUser(w http.ResponseWriter, r *http.Request) {
707 defer s.pingAllSyncAddresses()
gio134be722025-07-20 19:01:17 +0400708 var req User
gio2728e402024-08-01 18:14:21 +0400709 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400710 http.Error(w, "Invalid request body", http.StatusBadRequest)
711 return
712 }
gio134be722025-07-20 19:01:17 +0400713 if req.Id == "" {
714 http.Error(w, "Id cannot be empty", http.StatusBadRequest)
715 return
716 }
717 if req.Username == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400718 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
719 return
720 }
gio2728e402024-08-01 18:14:21 +0400721 if req.Email == "" {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400722 http.Error(w, "Email cannot be empty", http.StatusBadRequest)
723 return
724 }
gio134be722025-07-20 19:01:17 +0400725 user := User{Id: req.Id, Username: strings.ToLower(req.Username), Email: strings.ToLower(req.Email)}
726 if err := s.store.CreateUser(nil, user); err != nil {
727 http.Error(w, err.Error(), http.StatusInternalServerError)
728 return
729 }
730}
731
732type createGroupRequest struct {
733 UserId string `json:"userId"`
734 GroupId string `json:"groupId"`
735 Title string `json:"title"`
736 Description string `json:"description"`
737}
738
739func (s *Server) apiCreateGroup(w http.ResponseWriter, r *http.Request) {
740 var req createGroupRequest
741 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
742 http.Error(w, "Invalid request body", http.StatusBadRequest)
743 return
744 }
745 if err := s.store.CreateGroup(nil, req.UserId, Group{req.GroupId, req.Title, req.Description}); err != nil {
746 http.Error(w, err.Error(), http.StatusInternalServerError)
747 return
748 }
749}
750
751type apiGetGroupReq struct {
752 GroupId string `json:"groupId"`
753}
754
755// TODO(gio): update client
756type apiGetGroupResp struct {
757 Self Group `json:"self"`
758 Members []User `json:"members"`
759 Owners []User `json:"owners"`
760 MemberGroups []Group `json:"memberGroups"`
761 OwnerGroups []Group `json:"ownerGroups"`
762}
763
764func (s *Server) apiGetGroup(w http.ResponseWriter, r *http.Request) {
765 var req apiGetGroupReq
766 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
767 http.Error(w, "missing group name", http.StatusBadRequest)
768 return
769 }
770 group, err := s.store.GetGroup(nil, req.GroupId)
771 if err != nil {
772 http.Error(w, err.Error(), http.StatusInternalServerError)
773 return
774 }
775 members, err := s.store.GetMemberUsers(nil, req.GroupId)
776 if err != nil {
777 http.Error(w, err.Error(), http.StatusInternalServerError)
778 return
779 }
780 owners, err := s.store.GetOwnerUsers(nil, req.GroupId)
781 if err != nil {
782 http.Error(w, err.Error(), http.StatusInternalServerError)
783 return
784 }
785 memberGroups, err := s.store.GetMemberGroups(nil, req.GroupId)
786 if err != nil {
787 http.Error(w, err.Error(), http.StatusInternalServerError)
788 return
789 }
790 ownerGroups, err := s.store.GetOwnerGroups(nil, req.GroupId)
791 if err != nil {
792 http.Error(w, err.Error(), http.StatusInternalServerError)
793 return
794 }
795 if err := json.NewEncoder(w).Encode(apiGetGroupResp{group, members, owners, memberGroups, ownerGroups}); err != nil {
796 http.Error(w, err.Error(), http.StatusInternalServerError)
797 return
798 }
799}
800
801type userGroupStatus struct {
802 GroupId string `json:"groupId"`
803 UserId string `json:"userId"`
804 Status string `json:"status"`
805}
806
807func (s *Server) apiAddUserToGroup(w http.ResponseWriter, r *http.Request) {
808 var req userGroupStatus
809 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
810 http.Error(w, "Invalid request body", http.StatusBadRequest)
811 return
812 }
813 status, err := convertStatus(req.Status)
814 if err != nil {
815 http.Error(w, "Invalid status", http.StatusBadRequest)
816 return
817 }
818 fmt.Printf("AAA %+v\n", req)
819 switch status {
820 case Member:
821 err = s.store.AddMemberUser(nil, req.GroupId, req.UserId)
822 case Owner:
823 err = s.store.AddOwnerUser(nil, req.GroupId, req.UserId)
824 }
825 if err != nil {
826 http.Error(w, err.Error(), http.StatusInternalServerError)
827 return
828 }
829}
830
831func (s *Server) apiRemoveUserFromGroup(w http.ResponseWriter, r *http.Request) {
832 var req userGroupStatus
833 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
834 http.Error(w, "Invalid request body", http.StatusBadRequest)
835 return
836 }
837 status, err := convertStatus(req.Status)
838 if err != nil {
839 http.Error(w, "Invalid status", http.StatusBadRequest)
840 return
841 }
842 switch status {
843 case Member:
844 err = s.store.RemoveMemberUser(nil, req.GroupId, req.UserId)
845 case Owner:
846 err = s.store.RemoveOwnerUser(nil, req.GroupId, req.UserId)
847 }
848 if err != nil {
849 http.Error(w, err.Error(), http.StatusInternalServerError)
850 return
851 }
852}
853
854type groupGroupStatus struct {
855 GroupId string `json:"groupId"`
856 OtherId string `json:"otherId"`
857 Status string `json:"status"`
858}
859
860func (s *Server) apiAddGroupToGroup(w http.ResponseWriter, r *http.Request) {
861 var req groupGroupStatus
862 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
863 http.Error(w, "Invalid request body", http.StatusBadRequest)
864 return
865 }
866 status, err := convertStatus(req.Status)
867 if err != nil {
868 http.Error(w, "Invalid status", http.StatusBadRequest)
869 return
870 }
871 switch status {
872 case Member:
873 err = s.store.AddMemberGroup(nil, req.GroupId, req.OtherId)
874 case Owner:
875 err = s.store.AddOwnerGroup(nil, req.GroupId, req.OtherId)
876 }
877 if err != nil {
878 http.Error(w, err.Error(), http.StatusInternalServerError)
879 return
880 }
881}
882
883func (s *Server) apiRemoveGroupFromGroup(w http.ResponseWriter, r *http.Request) {
884 var req groupGroupStatus
885 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
886 http.Error(w, "Invalid request body", http.StatusBadRequest)
887 return
888 }
889 status, err := convertStatus(req.Status)
890 if err != nil {
891 http.Error(w, "Invalid status", http.StatusBadRequest)
892 return
893 }
894 switch status {
895 case Member:
896 err = s.store.RemoveMemberGroup(nil, req.GroupId, req.OtherId)
897 case Owner:
898 err = s.store.RemoveOwnerGroup(nil, req.GroupId, req.OtherId)
899 }
900 if err != nil {
901 http.Error(w, err.Error(), http.StatusInternalServerError)
902 return
903 }
904}
905
906type getGroupsOwnedByRequest struct {
907 User string `json:"user"`
908}
909
910type getGroupsOwnedByResponse struct {
911 Groups []string `json:"groups"`
912}
913
914func (s *Server) apiGetUserOwnedGroups(w http.ResponseWriter, r *http.Request) {
915 var req getGroupsOwnedByRequest
916 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
917 http.Error(w, err.Error(), http.StatusBadRequest)
918 return
919 }
920 groups, err := s.store.GetGroupsUserOwns(nil, req.User)
921 if err != nil {
922 http.Error(w, err.Error(), http.StatusInternalServerError)
923 return
924 }
925 var resp getGroupsOwnedByResponse
926 for _, g := range groups {
927 resp.Groups = append(resp.Groups, g.Title)
928 }
929 if err := json.NewEncoder(w).Encode(resp); err != nil {
Davit Tabidze75d57c32024-07-19 19:17:55 +0400930 http.Error(w, err.Error(), http.StatusInternalServerError)
931 return
932 }
Davit Tabidze75d57c32024-07-19 19:17:55 +0400933}
934
gio7fbd4ad2024-08-27 10:06:39 +0400935type addUserKeyRequest struct {
936 User string `json:"user"`
937 PublicKey string `json:"publicKey"`
938}
939
940func (s *Server) apiAddUserKey(w http.ResponseWriter, r *http.Request) {
941 var req addUserKeyRequest
942 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
943 http.Error(w, "Invalid request body", http.StatusBadRequest)
944 return
945 }
946 if req.User == "" {
947 http.Error(w, "Username cannot be empty", http.StatusBadRequest)
948 return
949 }
950 if req.PublicKey == "" {
951 http.Error(w, "PublicKey cannot be empty", http.StatusBadRequest)
952 return
953 }
gio134be722025-07-20 19:01:17 +0400954 if err := s.store.AddUserPublicKey(nil, strings.ToLower(req.User), req.PublicKey); err != nil {
gio7fbd4ad2024-08-27 10:06:39 +0400955 http.Error(w, err.Error(), http.StatusInternalServerError)
956 return
957 }
958}
959
960// TODO(gio): enque sync event instead of directly reaching out to clients.
961// This will allow to deduplicate sync events and save resources.
Davit Tabidze75d57c32024-07-19 19:17:55 +0400962func (s *Server) pingAllSyncAddresses() {
963 s.mu.Lock()
964 defer s.mu.Unlock()
965 for address := range s.syncAddresses {
gio7fbd4ad2024-08-27 10:06:39 +0400966 go func(address string) {
967 log.Printf("Pinging %s", address)
968 resp, err := http.Get(address)
969 if err != nil {
970 // TODO(gio): remove sync address after N number of failures.
971 log.Printf("Failed to ping %s: %v", address, err)
972 return
973 }
974 defer resp.Body.Close()
975 if resp.StatusCode != http.StatusOK {
976 log.Printf("Ping to %s returned status %d", address, resp.StatusCode)
977 }
978 }(address)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400979 }
980}
981
982func (s *Server) addSyncAddress(address string) {
gio7fbd4ad2024-08-27 10:06:39 +0400983 if address == "" {
984 return
985 }
986 fmt.Printf("Adding sync address: %s\n", address)
Davit Tabidze75d57c32024-07-19 19:17:55 +0400987 s.mu.Lock()
988 defer s.mu.Unlock()
989 s.syncAddresses[address] = struct{}{}
990}
991
DTabidze908bb852024-03-25 20:07:57 +0400992func convertStatus(status string) (Status, error) {
gio134be722025-07-20 19:01:17 +0400993 switch strings.ToLower(status) {
994 case "owner":
DTabidze908bb852024-03-25 20:07:57 +0400995 return Owner, nil
gio134be722025-07-20 19:01:17 +0400996 case "member":
DTabidze908bb852024-03-25 20:07:57 +0400997 return Member, nil
998 default:
999 return Owner, fmt.Errorf("invalid status: %s", status)
1000 }
1001}
1002
gio134be722025-07-20 19:01:17 +04001003func isValidGroupId(group string) error {
DTabidze908bb852024-03-25 20:07:57 +04001004 if strings.TrimSpace(group) == "" {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001005 return fmt.Errorf("Group name can't be empty or contain only whitespaces")
DTabidze908bb852024-03-25 20:07:57 +04001006 }
1007 validGroupName := regexp.MustCompile(`^[a-z0-9\-_:.\/ ]+$`)
1008 if !validGroupName.MatchString(group) {
Davit Tabidze5f1a2c62024-07-17 17:57:27 +04001009 return fmt.Errorf("Group name should contain only lowercase letters, digits, -, _, :, ., /")
DTabidze908bb852024-03-25 20:07:57 +04001010 }
1011 return nil
1012}
1013
DTabidze0d802592024-03-19 17:42:45 +04001014func main() {
1015 flag.Parse()
DTabidzec0b4d8f2024-03-22 17:25:10 +04001016 db, err := sql.Open("sqlite3", *dbPath)
DTabidze0d802592024-03-19 17:42:45 +04001017 if err != nil {
1018 panic(err)
1019 }
DTabidzec0b4d8f2024-03-22 17:25:10 +04001020 store, err := NewSQLiteStore(db)
1021 if err != nil {
1022 panic(err)
1023 }
Davit Tabidze75d57c32024-07-19 19:17:55 +04001024 s := Server{
1025 store: store,
1026 syncAddresses: make(map[string]struct{}),
1027 mu: sync.Mutex{},
1028 }
Giorgi Lekveishvili329af572024-03-25 20:14:41 +04001029 log.Fatal(s.Start())
DTabidze0d802592024-03-19 17:42:45 +04001030}
gio134be722025-07-20 19:01:17 +04001031
1032func normalizeGroupId(groupId string) string {
1033 if strings.Contains(groupId, "/") && !strings.HasPrefix(groupId, "/") {
1034 return fmt.Sprintf("/%s", groupId)
1035 }
1036 return groupId
1037}