1package backend
2
3import (
4 "context"
5 "errors"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/soft-serve/pkg/db"
10 "github.com/charmbracelet/soft-serve/pkg/db/models"
11 "github.com/charmbracelet/soft-serve/pkg/proto"
12 "github.com/charmbracelet/soft-serve/pkg/sshutils"
13 "github.com/charmbracelet/soft-serve/pkg/utils"
14 "golang.org/x/crypto/ssh"
15)
16
17// User finds a user by username.
18//
19// It implements backend.Backend.
20func (d *Backend) User(ctx context.Context, username string) (proto.User, error) {
21 username = strings.ToLower(username)
22 if err := utils.ValidateHandle(username); err != nil {
23 return nil, err
24 }
25
26 var m models.User
27 var pks []ssh.PublicKey
28 var hl models.Handle
29 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
30 var err error
31 m, err = d.store.FindUserByUsername(ctx, tx, username)
32 if err != nil {
33 return err
34 }
35
36 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
37 if err != nil {
38 return err
39 }
40
41 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
42 return err
43 }); err != nil {
44 err = db.WrapError(err)
45 if errors.Is(err, db.ErrRecordNotFound) {
46 return nil, proto.ErrUserNotFound
47 }
48 d.logger.Error("error finding user", "username", username, "error", err)
49 return nil, err
50 }
51
52 return &user{
53 user: m,
54 publicKeys: pks,
55 handle: hl,
56 }, nil
57}
58
59// UserByID finds a user by ID.
60func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
61 var m models.User
62 var pks []ssh.PublicKey
63 var hl models.Handle
64 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
65 var err error
66 m, err = d.store.GetUserByID(ctx, tx, id)
67 if err != nil {
68 return err
69 }
70
71 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
72 if err != nil {
73 return err
74 }
75
76 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
77 return err
78 }); err != nil {
79 err = db.WrapError(err)
80 if errors.Is(err, db.ErrRecordNotFound) {
81 return nil, proto.ErrUserNotFound
82 }
83 d.logger.Error("error finding user", "id", id, "error", err)
84 return nil, err
85 }
86
87 return &user{
88 user: m,
89 publicKeys: pks,
90 handle: hl,
91 }, nil
92}
93
94// UserByPublicKey finds a user by public key.
95//
96// It implements backend.Backend.
97func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.User, error) {
98 var m models.User
99 var pks []ssh.PublicKey
100 var hl models.Handle
101 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
102 var err error
103 m, err = d.store.FindUserByPublicKey(ctx, tx, pk)
104 if err != nil {
105 return db.WrapError(err)
106 }
107
108 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
109 if err != nil {
110 return err
111 }
112
113 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
114 return err
115 }); err != nil {
116 err = db.WrapError(err)
117 if errors.Is(err, db.ErrRecordNotFound) {
118 return nil, proto.ErrUserNotFound
119 }
120 d.logger.Error("error finding user", "pk", sshutils.MarshalAuthorizedKey(pk), "error", err)
121 return nil, err
122 }
123
124 return &user{
125 user: m,
126 publicKeys: pks,
127 handle: hl,
128 }, nil
129}
130
131// UserByAccessToken finds a user by access token.
132// This also validates the token for expiration and returns proto.ErrTokenExpired.
133func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.User, error) {
134 var m models.User
135 var pks []ssh.PublicKey
136 var hl models.Handle
137 token = HashToken(token)
138
139 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
140 t, err := d.store.GetAccessTokenByToken(ctx, tx, token)
141 if err != nil {
142 return db.WrapError(err)
143 }
144
145 if t.ExpiresAt.Valid && t.ExpiresAt.Time.Before(time.Now()) {
146 return proto.ErrTokenExpired
147 }
148
149 m, err = d.store.FindUserByAccessToken(ctx, tx, token)
150 if err != nil {
151 return db.WrapError(err)
152 }
153
154 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
155 if err != nil {
156 return err
157 }
158
159 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
160 return err
161 }); err != nil {
162 err = db.WrapError(err)
163 if errors.Is(err, db.ErrRecordNotFound) {
164 return nil, proto.ErrUserNotFound
165 }
166 d.logger.Error("failed to find user by access token", "err", err, "token", token)
167 return nil, err
168 }
169
170 return &user{
171 user: m,
172 publicKeys: pks,
173 handle: hl,
174 }, nil
175}
176
177// Users returns all users.
178//
179// It implements backend.Backend.
180func (d *Backend) Users(ctx context.Context) ([]string, error) {
181 var users []string
182 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
183 ms, err := d.store.GetAllUsers(ctx, tx)
184 if err != nil {
185 return err
186 }
187
188 ids := make([]int64, len(ms))
189 for i, m := range ms {
190 ids[i] = m.ID
191 }
192
193 handles, err := d.store.ListHandlesForIDs(ctx, tx, ids)
194 if err != nil {
195 return err
196 }
197
198 for _, h := range handles {
199 users = append(users, h.Handle)
200 }
201
202 return nil
203 }); err != nil {
204 return nil, db.WrapError(err)
205 }
206
207 return users, nil
208}
209
210// AddPublicKey adds a public key to a user.
211//
212// It implements backend.Backend.
213func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
214 username = strings.ToLower(username)
215 if err := utils.ValidateHandle(username); err != nil {
216 return err
217 }
218
219 return db.WrapError(
220 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
221 return d.store.AddPublicKeyByUsername(ctx, tx, username, pk)
222 }),
223 )
224}
225
226// CreateUser creates a new user.
227//
228// It implements backend.Backend.
229func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {
230 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
231 return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys)
232 }); err != nil {
233 return nil, db.WrapError(err)
234 }
235
236 return d.User(ctx, username)
237}
238
239// DeleteUser deletes a user.
240//
241// It implements backend.Backend.
242func (d *Backend) DeleteUser(ctx context.Context, username string) error {
243 username = strings.ToLower(username)
244 if err := utils.ValidateHandle(username); err != nil {
245 return err
246 }
247
248 return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
249 if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {
250 return db.WrapError(err)
251 }
252
253 return d.DeleteUserRepositories(ctx, username)
254 })
255}
256
257// RemovePublicKey removes a public key from a user.
258//
259// It implements backend.Backend.
260func (d *Backend) RemovePublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
261 return db.WrapError(
262 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
263 return d.store.RemovePublicKeyByUsername(ctx, tx, username, pk)
264 }),
265 )
266}
267
268// ListPublicKeys lists the public keys of a user.
269func (d *Backend) ListPublicKeys(ctx context.Context, username string) ([]ssh.PublicKey, error) {
270 username = strings.ToLower(username)
271 if err := utils.ValidateHandle(username); err != nil {
272 return nil, err
273 }
274
275 var keys []ssh.PublicKey
276 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
277 var err error
278 keys, err = d.store.ListPublicKeysByUsername(ctx, tx, username)
279 return err
280 }); err != nil {
281 return nil, db.WrapError(err)
282 }
283
284 return keys, nil
285}
286
287// SetUsername sets the username of a user.
288//
289// It implements backend.Backend.
290func (d *Backend) SetUsername(ctx context.Context, username string, newUsername string) error {
291 username = strings.ToLower(username)
292 if err := utils.ValidateHandle(username); err != nil {
293 return err
294 }
295
296 return db.WrapError(
297 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
298 return d.store.SetUsernameByUsername(ctx, tx, username, newUsername)
299 }),
300 )
301}
302
303// SetAdmin sets the admin flag of a user.
304//
305// It implements backend.Backend.
306func (d *Backend) SetAdmin(ctx context.Context, username string, admin bool) error {
307 username = strings.ToLower(username)
308 if err := utils.ValidateHandle(username); err != nil {
309 return err
310 }
311
312 return db.WrapError(
313 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
314 return d.store.SetAdminByUsername(ctx, tx, username, admin)
315 }),
316 )
317}
318
319// SetPassword sets the password of a user.
320func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword string) error {
321 username = strings.ToLower(username)
322 if err := utils.ValidateHandle(username); err != nil {
323 return err
324 }
325
326 password, err := HashPassword(rawPassword)
327 if err != nil {
328 return err
329 }
330
331 return db.WrapError(
332 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
333 return d.store.SetUserPasswordByUsername(ctx, tx, username, password)
334 }),
335 )
336}
337
338type user struct {
339 user models.User
340 publicKeys []ssh.PublicKey
341 handle models.Handle
342}
343
344var _ proto.User = (*user)(nil)
345
346// IsAdmin implements proto.User
347func (u *user) IsAdmin() bool {
348 return u.user.Admin
349}
350
351// PublicKeys implements proto.User
352func (u *user) PublicKeys() []ssh.PublicKey {
353 return u.publicKeys
354}
355
356// Username implements proto.User
357func (u *user) Username() string {
358 return u.handle.Handle
359}
360
361// ID implements proto.User.
362func (u *user) ID() int64 {
363 return u.user.ID
364}
365
366// Password implements proto.User.
367func (u *user) Password() string {
368 if u.user.Password.Valid {
369 return u.user.Password.String
370 }
371
372 return ""
373}