rin
Публичный Open-source lightweight git hosting.
collab.go · 152 строки · 3.9 KB
Raw
1 package repo
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "strings"
9 "time"
10
11 "polar/internal/user"
12 )
13
14 const (
15 RoleRead = "read"
16 RoleWrite = "write"
17 )
18
19 var (
20 ErrCollabUserNotFound = errors.New("user not found")
21 ErrCannotAddOwner = errors.New("repository owner cannot be added as a collaborator")
22 )
23
24 type Collaborator struct {
25 UserID int64
26 Username string
27 FullName string
28 AvatarURL string
29 Role string
30 CreatedAt time.Time
31 }
32
33 func normalizeRole(role string) string {
34 if strings.TrimSpace(role) == RoleRead {
35 return RoleRead
36 }
37 return RoleWrite
38 }
39
40 func (s *Service) AddCollaborator(ctx context.Context, r *Repository, username, role string) error {
41 username = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(username), "@"))
42 if username == "" {
43 return ErrCollabUserNotFound
44 }
45
46 cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
47 defer cancel()
48
49 var userID int64
50 err := s.db.QueryRowContext(cctx, `SELECT id FROM users WHERE LOWER(username) = LOWER(?)`, username).Scan(&userID)
51 if err != nil {
52 if errors.Is(err, sql.ErrNoRows) {
53 return ErrCollabUserNotFound
54 }
55 return fmt.Errorf("lookup collaborator user: %w", err)
56 }
57 if userID == r.OwnerID {
58 return ErrCannotAddOwner
59 }
60
61 now := time.Now().UTC()
62 _, err = s.db.ExecContext(cctx, `
63 INSERT INTO collaborators (repo_id, user_id, role, created_at)
64 VALUES (?, ?, ?, ?)
65 ON CONFLICT(repo_id, user_id) DO UPDATE SET role = excluded.role
66 `, r.ID, userID, normalizeRole(role), now)
67 if err != nil {
68 return fmt.Errorf("upsert collaborator: %w", err)
69 }
70 return nil
71 }
72
73 func (s *Service) RemoveCollaborator(ctx context.Context, repoID, userID int64) error {
74 cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
75 defer cancel()
76
77 _, err := s.db.ExecContext(cctx, `DELETE FROM collaborators WHERE repo_id = ? AND user_id = ?`, repoID, userID)
78 if err != nil {
79 return fmt.Errorf("remove collaborator: %w", err)
80 }
81 return nil
82 }
83
84 func (s *Service) ListCollaborators(ctx context.Context, repoID int64) ([]Collaborator, error) {
85 cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
86 defer cancel()
87
88 rows, err := s.db.QueryContext(cctx, `
89 SELECT c.user_id, u.username, u.full_name, u.avatar_url, c.role, c.created_at
90 FROM collaborators c
91 JOIN users u ON u.id = c.user_id
92 WHERE c.repo_id = ?
93 ORDER BY c.created_at ASC
94 `, repoID)
95 if err != nil {
96 return nil, fmt.Errorf("list collaborators: %w", err)
97 }
98 defer rows.Close()
99
100 var list []Collaborator
101 for rows.Next() {
102 var c Collaborator
103 if err := rows.Scan(&c.UserID, &c.Username, &c.FullName, &c.AvatarURL, &c.Role, &c.CreatedAt); err != nil {
104 return nil, fmt.Errorf("scan collaborator: %w", err)
105 }
106 list = append(list, c)
107 }
108 return list, rows.Err()
109 }
110
111 func (s *Service) GetCollaboratorRole(ctx context.Context, repoID, userID int64) string {
112 cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
113 defer cancel()
114
115 var role string
116 err := s.db.QueryRowContext(cctx, `SELECT role FROM collaborators WHERE repo_id = ? AND user_id = ?`, repoID, userID).Scan(&role)
117 if err != nil {
118 return ""
119 }
120 return role
121 }
122
123 // CanRead reports whether the user may read the repository (owner, admin,
124 // collaborator of any role, or anyone for public repositories).
125 func (s *Service) CanRead(ctx context.Context, r *Repository, u *user.User) bool {
126 if r == nil {
127 return false
128 }
129 if !r.IsPrivate {
130 return true
131 }
132 if u == nil {
133 return false
134 }
135 if u.ID == r.OwnerID || u.IsAdmin {
136 return true
137 }
138 role := s.GetCollaboratorRole(ctx, r.ID, u.ID)
139 return role == RoleRead || role == RoleWrite
140 }
141
142 // CanWrite reports whether the user may push to the repository (owner, admin
143 // or a collaborator with the write role).
144 func (s *Service) CanWrite(ctx context.Context, r *Repository, u *user.User) bool {
145 if r == nil || u == nil {
146 return false
147 }
148 if u.ID == r.OwnerID || u.IsAdmin {
149 return true
150 }
151 return s.GetCollaboratorRole(ctx, r.ID, u.ID) == RoleWrite
152 }
153