1package sqlite
2
3import (
4 "context"
5 "strings"
6
7 "github.com/charmbracelet/soft-serve/server/access"
8 "github.com/charmbracelet/soft-serve/server/auth"
9 "github.com/charmbracelet/soft-serve/server/db"
10 "github.com/charmbracelet/soft-serve/server/db/sqlite"
11 "github.com/charmbracelet/soft-serve/server/settings"
12 "github.com/charmbracelet/soft-serve/server/store"
13 "github.com/charmbracelet/soft-serve/server/utils"
14 "github.com/jmoiron/sqlx"
15)
16
17// SqliteAccess is an access backend implementation that uses SQLite.
18type SqliteAccess struct {
19 ctx context.Context
20 db db.Database
21}
22
23var _ access.Access = (*SqliteAccess)(nil)
24
25func init() {
26 access.Register("sqlite", newSqliteAccess)
27}
28
29func newSqliteAccess(ctx context.Context) (access.Access, error) {
30 sdb := db.FromContext(ctx)
31 if sdb == nil {
32 return nil, db.ErrNoDatabase
33 }
34
35 return &SqliteAccess{
36 ctx: ctx,
37 db: sdb,
38 }, nil
39}
40
41// AccessLevel implements access.Access.
42func (d *SqliteAccess) AccessLevel(ctx context.Context, repo string, user auth.User) (access.AccessLevel, error) {
43 settings := settings.FromContext(d.ctx)
44 store := store.FromContext(d.ctx)
45
46 anon := settings.AnonAccess(ctx)
47
48 // TODO: add admin access to user repositories
49
50 // If the user is an admin, they have admin access.
51 if user != nil && user.IsAdmin() {
52 return access.AdminAccess, nil
53 }
54
55 // If the repository exists, check if the user is a collaborator.
56 r, _ := store.Repository(ctx, repo)
57 if r != nil {
58 // If the user is a collaborator, they have read/write access.
59 if user != nil {
60 isCollab, _ := d.IsCollaborator(ctx, repo, user.Username())
61 if isCollab {
62 if anon > access.ReadWriteAccess {
63 return anon, nil
64 }
65 return access.ReadWriteAccess, nil
66 }
67 }
68
69 // If the repository is private, the user has no access.
70 if r.IsPrivate() {
71 return access.NoAccess, nil
72 }
73
74 // Otherwise, the user has read-only access.
75 return access.ReadOnlyAccess, nil
76 }
77
78 if user != nil {
79 // If the repository doesn't exist, the user has read/write access.
80 if anon > access.ReadWriteAccess {
81 return anon, nil
82 }
83
84 return access.ReadWriteAccess, nil
85 }
86
87 // If the user doesn't exist, give them the anonymous access level.
88 return anon, nil
89}
90
91// AddCollaborator adds a collaborator to a repository.
92//
93// It implements backend.Backend.
94func (d *SqliteAccess) AddCollaborator(ctx context.Context, repo string, username string) error {
95 username = strings.ToLower(username)
96 if err := utils.ValidateUsername(username); err != nil {
97 return err
98 }
99
100 repo = utils.SanitizeRepo(repo)
101 return sqlite.WrapDbErr(sqlite.WrapTx(d.db.DBx(), ctx, func(tx *sqlx.Tx) error {
102 _, err := tx.Exec(`INSERT INTO collab (user_id, repo_id, updated_at)
103 VALUES (
104 (SELECT id FROM user WHERE username = ?),
105 (SELECT id FROM repo WHERE name = ?),
106 CURRENT_TIMESTAMP
107 );`, username, repo)
108 return err
109 }),
110 )
111}
112
113// Collaborators returns a list of collaborators for a repository.
114//
115// It implements backend.Backend.
116func (d *SqliteAccess) Collaborators(ctx context.Context, repo string) ([]string, error) {
117 repo = utils.SanitizeRepo(repo)
118 var users []string
119 if err := sqlite.WrapTx(d.db.DBx(), d.ctx, func(tx *sqlx.Tx) error {
120 return tx.Select(&users, `SELECT user.username FROM user
121 INNER JOIN collab ON user.id = collab.user_id
122 INNER JOIN repo ON repo.id = collab.repo_id
123 WHERE repo.name = ?`, repo)
124 }); err != nil {
125 return nil, sqlite.WrapDbErr(err)
126 }
127
128 return users, nil
129}
130
131// IsCollaborator returns true if the user is a collaborator of the repository.
132//
133// It implements backend.Backend.
134func (d *SqliteAccess) IsCollaborator(ctx context.Context, repo string, username string) (bool, error) {
135 repo = utils.SanitizeRepo(repo)
136 var count int
137 if err := sqlite.WrapTx(d.db.DBx(), ctx, func(tx *sqlx.Tx) error {
138 return tx.Get(&count, `SELECT COUNT(*) FROM user
139 INNER JOIN collab ON user.id = collab.user_id
140 INNER JOIN repo ON repo.id = collab.repo_id
141 WHERE repo.name = ? AND user.username = ?`, repo, username)
142 }); err != nil {
143 return false, sqlite.WrapDbErr(err)
144 }
145
146 return count > 0, nil
147}
148
149// RemoveCollaborator removes a collaborator from a repository.
150//
151// It implements backend.Backend.
152func (d *SqliteAccess) RemoveCollaborator(ctx context.Context, repo string, username string) error {
153 repo = utils.SanitizeRepo(repo)
154 return sqlite.WrapDbErr(
155 sqlite.WrapTx(d.db.DBx(), ctx, func(tx *sqlx.Tx) error {
156 _, err := tx.Exec(`DELETE FROM collab
157 WHERE user_id = (SELECT id FROM user WHERE username = ?)
158 AND repo_id = (SELECT id FROM repo WHERE name = ?)`, username, repo)
159 return err
160 }),
161 )
162}