blob: 3bb09485bd1d7126012a186113b9bee15187252f [file] [log] [blame]
DTabidze0d802592024-03-19 17:42:45 +04001package main
2
3import (
4 "database/sql"
5 "embed"
6 "flag"
7 "fmt"
8 "html/template"
9 "log"
10 "net/http"
11 "strings"
12
13 "github.com/ncruces/go-sqlite3"
14 _ "github.com/ncruces/go-sqlite3/driver"
15 _ "github.com/ncruces/go-sqlite3/embed"
16)
17
18var port = flag.Int("port", 8080, "ort to listen on")
19var dbPath = flag.String("db-path", "memberships.db", "Path to SQLite file")
20
21//go:embed index.html
22var indexHTML string
23
24//go:embed group.html
25var groupHTML string
26
27//go:embed static
28var staticResources embed.FS
29
30type Store interface {
31 CreateGroup(owner string, group Group) error
32 AddChildGroup(parent, child string) error
33 GetGroupsOwnedBy(user string) ([]Group, error)
34 GetMembershipGroups(user string) ([]Group, error)
35 IsGroupOwner(user, group string) (bool, error)
36 AddGroupMember(user, group string) error
37 AddGroupOwner(user, group string) error
38 GetGroupOwners(group string) ([]string, error)
39 GetGroupMembers(group string) ([]string, error)
40 GetGroupDescription(group string) (string, error)
41 GetAvailableGroupsAsChild(group string) ([]string, error)
42}
43
44type Server struct {
45 store Store
46}
47
48type Group struct {
49 Name string
50 Description string
51}
52
53type SQLiteStore struct {
54 db *sql.DB
55}
56
57func NewSQLiteStore(path string) (*SQLiteStore, error) {
58 db, err := sql.Open("sqlite3", path)
59 if err != nil {
60 return nil, err
61 }
62 _, err = db.Exec(`
63 CREATE TABLE IF NOT EXISTS groups (
64 name TEXT PRIMARY KEY,
65 description TEXT
66 );
67
68 CREATE TABLE IF NOT EXISTS owners (
69 username TEXT,
70 group_name TEXT,
71 FOREIGN KEY(group_name) REFERENCES groups(name)
72 );
73
74 CREATE TABLE IF NOT EXISTS group_to_group (
75 parent_group TEXT,
76 child_group TEXT,
77 FOREIGN KEY(parent_group) REFERENCES groups(name),
78 FOREIGN KEY(child_group) REFERENCES groups(name)
79 );
80
81 CREATE TABLE IF NOT EXISTS user_to_group (
82 username TEXT,
83 group_name TEXT,
84 FOREIGN KEY(group_name) REFERENCES groups(name)
85 );`)
86 if err != nil {
87 return nil, err
88 }
89 return &SQLiteStore{db: db}, nil
90}
91
92func (s *SQLiteStore) queryGroups(query string, args ...interface{}) ([]Group, error) {
93 groups := make([]Group, 0)
94 rows, err := s.db.Query(query, args...)
95 if err != nil {
96 return nil, err
97 }
98 defer rows.Close()
99 for rows.Next() {
100 var group Group
101 if err := rows.Scan(&group.Name, &group.Description); err != nil {
102 return nil, err
103 }
104 groups = append(groups, group)
105 }
106 if err := rows.Err(); err != nil {
107 return nil, err
108 }
109 return groups, nil
110}
111
112func (s *SQLiteStore) GetGroupsOwnedBy(user string) ([]Group, error) {
113 query := `
114 SELECT groups.name, groups.description
115 FROM groups
116 JOIN owners ON groups.name = owners.group_name
117 WHERE owners.username = ?`
118 return s.queryGroups(query, user)
119}
120
121func (s *SQLiteStore) GetMembershipGroups(user string) ([]Group, error) {
122 query := `
123 SELECT groups.name, groups.description
124 FROM groups
125 JOIN user_to_group ON groups.name = user_to_group.group_name
126 WHERE user_to_group.username = ?`
127 return s.queryGroups(query, user)
128}
129
130func (s *SQLiteStore) CreateGroup(owner string, group Group) error {
131 tx, err := s.db.Begin()
132 if err != nil {
133 return err
134 }
135 defer tx.Rollback()
136 query := `INSERT INTO groups (name, description) VALUES (?, ?)`
137 if _, err := tx.Exec(query, group.Name, group.Description); err != nil {
138 sqliteErr, ok := err.(*sqlite3.Error)
139 if ok && sqliteErr.ExtendedCode() == 1555 {
140 return fmt.Errorf("Group with the name %s already exists", group.Name)
141 }
142 return err
143 }
144 query = `INSERT INTO owners (username, group_name) VALUES (?, ?)`
145 if _, err := tx.Exec(query, owner, group.Name); err != nil {
146 return err
147 }
148 if err := tx.Commit(); err != nil {
149 return err
150 }
151 return nil
152}
153
154func (s *SQLiteStore) IsGroupOwner(user, group string) (bool, error) {
155 query := `
156 SELECT EXISTS (
157 SELECT 1
158 FROM owners
159 WHERE username = ? AND group_name = ?
160 )`
161 var exists bool
162 if err := s.db.QueryRow(query, user, group).Scan(&exists); err != nil {
163 return false, err
164 }
165 return exists, nil
166}
167
168func (s *SQLiteStore) userGroupPairExists(tx *sql.Tx, table, user, group string) (bool, error) {
169 query := fmt.Sprintf("SELECT EXISTS (SELECT 1 FROM %s WHERE username = ? AND group_name = ?)", table)
170 var exists bool
171 if err := tx.QueryRow(query, user, group).Scan(&exists); err != nil {
172 return false, err
173 }
174 return exists, nil
175}
176
177func (s *SQLiteStore) AddGroupMember(user, group string) error {
178 tx, err := s.db.Begin()
179 if err != nil {
180 return err
181 }
182 defer tx.Rollback()
183 existsInUserToGroup, err := s.userGroupPairExists(tx, "user_to_group", user, group)
184 if err != nil {
185 return err
186 }
187 if existsInUserToGroup {
188 return fmt.Errorf("%s is already a member of group %s", user, group)
189 }
190 if _, err := tx.Exec(`INSERT INTO user_to_group (username, group_name) VALUES (?, ?)`, user, group); err != nil {
191 return err
192 }
193 if err := tx.Commit(); err != nil {
194 return err
195 }
196 return nil
197}
198
199func (s *SQLiteStore) AddGroupOwner(user, group string) error {
200 tx, err := s.db.Begin()
201 if err != nil {
202 return err
203 }
204 defer tx.Rollback()
205 existsInOwners, err := s.userGroupPairExists(tx, "owners", user, group)
206 if err != nil {
207 return err
208 }
209 if existsInOwners {
210 return fmt.Errorf("%s is already an owner of group %s", user, group)
211 }
212 if _, err = tx.Exec(`INSERT INTO owners (username, group_name) VALUES (?, ?)`, user, group); err != nil {
213 return err
214 }
215 if err := tx.Commit(); err != nil {
216 return err
217 }
218 return nil
219}
220
221func (s *SQLiteStore) getUsersByGroup(table, group string) ([]string, error) {
222 query := fmt.Sprintf("SELECT username FROM %s WHERE group_name = ?", table)
223 rows, err := s.db.Query(query, group)
224 if err != nil {
225 return nil, err
226 }
227 defer rows.Close()
228 var users []string
229 for rows.Next() {
230 var username string
231 if err := rows.Scan(&username); err != nil {
232 return nil, err
233 }
234 users = append(users, username)
235 }
236 if err := rows.Err(); err != nil {
237 return nil, err
238 }
239 return users, nil
240}
241
242func (s *SQLiteStore) GetGroupOwners(group string) ([]string, error) {
243 return s.getUsersByGroup("owners", group)
244}
245
246func (s *SQLiteStore) GetGroupMembers(group string) ([]string, error) {
247 return s.getUsersByGroup("user_to_group", group)
248}
249
250func (s *SQLiteStore) GetGroupDescription(group string) (string, error) {
251 var description string
252 query := `SELECT description FROM groups WHERE name = ?`
253 if err := s.db.QueryRow(query, group).Scan(&description); err != nil {
254 return "", err
255 }
256 return description, nil
257}
258
259func (s *SQLiteStore) parentChildGroupPairExists(tx *sql.Tx, parent, child string) (bool, error) {
260 query := `SELECT EXISTS (SELECT 1 FROM group_to_group WHERE parent_group = ? AND child_group = ?)`
261 var exists bool
262 if err := tx.QueryRow(query, parent, child).Scan(&exists); err != nil {
263 return false, err
264 }
265 return exists, nil
266}
267
268func (s *SQLiteStore) AddChildGroup(parent, child string) error {
269 tx, err := s.db.Begin()
270 if err != nil {
271 return err
272 }
273 defer tx.Rollback()
274 existsInGroupToGroup, err := s.parentChildGroupPairExists(tx, parent, child)
275 if err != nil {
276 return err
277 }
278 if existsInGroupToGroup {
279 return fmt.Errorf("child group name %s already exists in group %s", child, parent)
280 }
281 if _, err := tx.Exec(`INSERT INTO group_to_group (parent_group, child_group) VALUES (?, ?)`, parent, child); err != nil {
282 return err
283 }
284 if err := tx.Commit(); err != nil {
285 return err
286 }
287 return nil
288}
289
290func (s *SQLiteStore) GetAvailableGroupsAsChild(group string) ([]string, error) {
291 // TODO(dtabidze): Might have to add further logic to filter available groups as children.
292 query := `
293 SELECT name FROM groups
294 WHERE name != ? AND name NOT IN (
295 SELECT child_group FROM group_to_group WHERE parent_group = ?
296 )
297 `
298 rows, err := s.db.Query(query, group, group)
299 if err != nil {
300 return nil, err
301 }
302 defer rows.Close()
303 var availableGroups []string
304 for rows.Next() {
305 var groupName string
306 if err := rows.Scan(&groupName); err != nil {
307 return nil, err
308 }
309 availableGroups = append(availableGroups, groupName)
310 }
311 return availableGroups, nil
312}
313
314func getLoggedInUser(r *http.Request) (string, error) {
315 // TODO(dtabidze): should make a request to get loggedin user
316 return "tabo", nil
317}
318
319type Status int
320
321const (
322 Owner Status = iota
323 Member
324)
325
326func convertStatus(status string) (Status, error) {
327 switch status {
328 case "Owner":
329 return Owner, nil
330 case "Member":
331 return Member, nil
332 default:
333 return Owner, fmt.Errorf("invalid status: %s", status)
334 }
335}
336
337func (s *Server) Start() {
338 http.Handle("/static/", http.FileServer(http.FS(staticResources)))
339 http.HandleFunc("/", s.homePageHandler)
340 http.HandleFunc("/group/", s.groupHandler)
341 http.HandleFunc("/create-group", s.createGroupHandler)
342 http.HandleFunc("/add-user", s.addUserHandler)
343 http.HandleFunc("/add-child-group", s.addChildGroupHandler)
344 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
345}
346
347type GroupData struct {
348 Group Group
349 Membership string
350}
351
352func (s *Server) checkIsOwner(w http.ResponseWriter, user, group string) (bool, error) {
353 isOwner, err := s.store.IsGroupOwner(user, group)
354 if err != nil {
355 http.Error(w, err.Error(), http.StatusInternalServerError)
356 return false, err
357 }
358 if !isOwner {
359 http.Error(w, fmt.Sprintf("You are not the owner of the group %s", group), http.StatusUnauthorized)
360 return false, nil
361 }
362 return true, nil
363}
364
365func (s *Server) homePageHandler(w http.ResponseWriter, r *http.Request) {
366 loggedInUser, err := getLoggedInUser(r)
367 if err != nil {
368 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
369 return
370 }
371 ownerGroups, err := s.store.GetGroupsOwnedBy(loggedInUser)
372 if err != nil {
373 http.Error(w, err.Error(), http.StatusInternalServerError)
374 return
375 }
376 membershipGroups, err := s.store.GetMembershipGroups(loggedInUser)
377 if err != nil {
378 http.Error(w, err.Error(), http.StatusInternalServerError)
379 return
380 }
381 tmpl, err := template.New("index").Parse(indexHTML)
382 if err != nil {
383 http.Error(w, err.Error(), http.StatusInternalServerError)
384 return
385 }
386 data := struct {
387 OwnerGroups []Group
388 MembershipGroups []Group
389 }{
390 OwnerGroups: ownerGroups,
391 MembershipGroups: membershipGroups,
392 }
393 w.Header().Set("Content-Type", "text/html")
394 if err := tmpl.Execute(w, data); err != nil {
395 http.Error(w, err.Error(), http.StatusInternalServerError)
396 return
397 }
398}
399
400func (s *Server) createGroupHandler(w http.ResponseWriter, r *http.Request) {
401 loggedInUser, err := getLoggedInUser(r)
402 if err != nil {
403 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
404 return
405 }
406 if r.Method != http.MethodPost {
407 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
408 return
409 }
410 if err := r.ParseForm(); err != nil {
411 http.Error(w, err.Error(), http.StatusInternalServerError)
412 return
413 }
414 var group Group
415 group.Name = r.PostFormValue("group-name")
416 group.Description = r.PostFormValue("description")
417 if err := s.store.CreateGroup(loggedInUser, group); err != nil {
418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
421 http.Redirect(w, r, "/", http.StatusSeeOther)
422}
423
424func (s *Server) groupHandler(w http.ResponseWriter, r *http.Request) {
425 groupName := strings.TrimPrefix(r.URL.Path, "/group/")
426 tmpl, err := template.New("group").Parse(groupHTML)
427 if err != nil {
428 http.Error(w, err.Error(), http.StatusInternalServerError)
429 return
430 }
431 owners, err := s.store.GetGroupOwners(groupName)
432 if err != nil {
433 http.Error(w, err.Error(), http.StatusInternalServerError)
434 return
435 }
436 members, err := s.store.GetGroupMembers(groupName)
437 if err != nil {
438 http.Error(w, err.Error(), http.StatusInternalServerError)
439 return
440 }
441 description, err := s.store.GetGroupDescription(groupName)
442 if err != nil {
443 http.Error(w, err.Error(), http.StatusInternalServerError)
444 return
445 }
446 availableGroups, err := s.store.GetAvailableGroupsAsChild(groupName)
447 if err != nil {
448 http.Error(w, err.Error(), http.StatusInternalServerError)
449 return
450 }
451 data := struct {
452 GroupName string
453 Description string
454 Owners []string
455 Members []string
456 AvailableGroups []string
457 }{
458 GroupName: groupName,
459 Description: description,
460 Owners: owners,
461 Members: members,
462 AvailableGroups: availableGroups,
463 }
464 if err := tmpl.Execute(w, data); err != nil {
465 http.Error(w, err.Error(), http.StatusInternalServerError)
466 return
467 }
468}
469
470func (s *Server) addUserHandler(w http.ResponseWriter, r *http.Request) {
471 if r.Method != http.MethodPost {
472 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
473 return
474 }
475 loggedInUser, err := getLoggedInUser(r)
476 if err != nil {
477 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
478 return
479 }
480 groupName := r.FormValue("group")
481 username := r.FormValue("username")
482 status, err := convertStatus(r.FormValue("status"))
483 if err != nil {
484 http.Error(w, err.Error(), http.StatusBadRequest)
485 return
486 }
487 if _, err := s.checkIsOwner(w, loggedInUser, groupName); err != nil {
488 return
489 }
490 switch status {
491 case Owner:
492 err = s.store.AddGroupOwner(username, groupName)
493 case Member:
494 err = s.store.AddGroupMember(username, groupName)
495 default:
496 http.Error(w, "Invalid status", http.StatusBadRequest)
497 return
498 }
499 if err != nil {
500 http.Error(w, err.Error(), http.StatusInternalServerError)
501 return
502 }
503 http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
504}
505
506func (s *Server) addChildGroupHandler(w http.ResponseWriter, r *http.Request) {
507 // TODO(dtabidze): In future we might need to make one group OWNER of another and not just a member.
508 if r.Method != http.MethodPost {
509 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
510 return
511 }
512 loggedInUser, err := getLoggedInUser(r)
513 if err != nil {
514 http.Error(w, "User Not Logged In", http.StatusUnauthorized)
515 return
516 }
517 parentGroup := r.FormValue("parent-group")
518 childGroup := r.FormValue("child-group")
519 if _, err := s.checkIsOwner(w, loggedInUser, parentGroup); err != nil {
520 return
521 }
522 if err := s.store.AddChildGroup(parentGroup, childGroup); err != nil {
523 http.Error(w, err.Error(), http.StatusInternalServerError)
524 return
525 }
526 http.Redirect(w, r, "/group/"+parentGroup, http.StatusSeeOther)
527}
528
529func main() {
530 flag.Parse()
531 db, err := NewSQLiteStore(*dbPath)
532 if err != nil {
533 panic(err)
534 }
535 s := Server{store: db}
536 s.Start()
537}