• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vocdoni / saas-backend / 18185484210

02 Oct 2025 06:41AM UTC coverage: 57.778%. Remained the same
18185484210

Pull #268

github

altergui
apicommon: OrganizationInvite.Role is now a db.UserRole rather than a string
Pull Request #268: api: updatePendingUserInvitationHandler now checks invite belongs to org.Address

8 of 9 new or added lines in 1 file covered. (88.89%)

4 existing lines in 1 file now uncovered.

5835 of 10099 relevant lines covered (57.78%)

33.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.08
/api/organization_users.go
1
package api
2

3
import (
4
        "encoding/json"
5
        "net/http"
6
        "strconv"
7
        "time"
8

9
        "github.com/ethereum/go-ethereum/common"
10
        "github.com/go-chi/chi/v5"
11
        "github.com/vocdoni/saas-backend/api/apicommon"
12
        "github.com/vocdoni/saas-backend/db"
13
        "github.com/vocdoni/saas-backend/errors"
14
        "github.com/vocdoni/saas-backend/internal"
15
        "github.com/vocdoni/saas-backend/notifications/mailtemplates"
16
        "github.com/vocdoni/saas-backend/subscriptions"
17
        "go.vocdoni.io/dvote/log"
18
)
19

20
// organizationUsersHandler godoc
21
//
22
//        @Summary                Get organization users
23
//        @Description        Get the list of users with their roles in the organization
24
//        @Tags                        organizations
25
//        @Accept                        json
26
//        @Produce                json
27
//        @Security                BearerAuth
28
//        @Param                        address        path                string        true        "Organization address"
29
//        @Success                200                {object}        apicommon.OrganizationUsers
30
//        @Failure                400                {object}        errors.Error        "Invalid input data"
31
//        @Failure                401                {object}        errors.Error        "Unauthorized"
32
//        @Failure                404                {object}        errors.Error        "Organization not found"
33
//        @Failure                500                {object}        errors.Error        "Internal server error"
34
//        @Router                        /organizations/{address}/users [get]
35
func (a *API) organizationUsersHandler(w http.ResponseWriter, r *http.Request) {
5✔
36
        // get the user from the request context
5✔
37
        user, ok := apicommon.UserFromContext(r.Context())
5✔
38
        if !ok {
5✔
39
                errors.ErrUnauthorized.Write(w)
×
40
                return
×
41
        }
×
42
        // get the organization info from the request context
43
        org, _, ok := a.organizationFromRequest(r)
5✔
44
        if !ok {
5✔
45
                errors.ErrNoOrganizationProvided.Write(w)
×
46
                return
×
47
        }
×
48
        if !user.HasRoleFor(org.Address, db.AdminRole) {
5✔
49
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
50
                return
×
51
        }
×
52
        // send the organization back to the user
53
        users, err := a.db.OrganizationUsers(org.Address)
5✔
54
        if err != nil {
5✔
55
                errors.ErrGenericInternalServerError.Withf("could not get organization users: %v", err).Write(w)
×
56
                return
×
57
        }
×
58
        orgUsers := apicommon.OrganizationUsers{
5✔
59
                Users: make([]*apicommon.OrganizationUser, 0, len(users)),
5✔
60
        }
5✔
61
        for _, user := range users {
14✔
62
                var role string
9✔
63
                for _, userOrg := range user.Organizations {
22✔
64
                        if userOrg.Address == org.Address {
22✔
65
                                role = string(userOrg.Role)
9✔
66
                                break
9✔
67
                        }
68
                }
69
                if role == "" {
9✔
70
                        continue
×
71
                }
72
                orgUsers.Users = append(orgUsers.Users, &apicommon.OrganizationUser{
9✔
73
                        Info: &apicommon.UserInfo{
9✔
74
                                ID:        user.ID,
9✔
75
                                Email:     user.Email,
9✔
76
                                FirstName: user.FirstName,
9✔
77
                                LastName:  user.LastName,
9✔
78
                        },
9✔
79
                        Role: role,
9✔
80
                })
9✔
81
        }
82
        apicommon.HTTPWriteJSON(w, orgUsers)
5✔
83
}
84

85
// inviteOrganizationUserHandler godoc
86
//
87
//        @Summary                Invite a new user to an organization
88
//        @Description        Invite a new user to an organization. Only the admin of the organization can invite a new user.
89
//        @Description        It stores the invitation in the database and sends an email to the new user with the invitation code.
90
//        @Tags                        organizations
91
//        @Accept                        json
92
//        @Produce                json
93
//        @Security                BearerAuth
94
//        @Param                        address        path                string                                                        true        "Organization address"
95
//        @Param                        request        body                apicommon.OrganizationInvite        true        "Invitation information"
96
//        @Success                200                {string}        string                                                        "OK"
97
//        @Failure                400                {object}        errors.Error                                        "Invalid input data"
98
//        @Failure                401                {object}        errors.Error                                        "Unauthorized"
99
//        @Failure                409                {object}        errors.Error                                        "User already has a role in the organization"
100
//        @Failure                500                {object}        errors.Error                                        "Internal server error"
101
//        @Router                        /organizations/{address}/users [post]
102
func (a *API) inviteOrganizationUserHandler(w http.ResponseWriter, r *http.Request) {
22✔
103
        // get the user from the request context
22✔
104
        user, ok := apicommon.UserFromContext(r.Context())
22✔
105
        if !ok {
22✔
106
                errors.ErrUnauthorized.Write(w)
×
107
                return
×
108
        }
×
109
        // get the organization info from the request context
110
        org, _, ok := a.organizationFromRequest(r)
22✔
111
        if !ok {
22✔
112
                errors.ErrNoOrganizationProvided.Write(w)
×
113
                return
×
114
        }
×
115

116
        // check if the user/org has permission to invite new users
117
        hasPermission, err := a.subscriptions.HasDBPermission(user.Email, org.Address, subscriptions.InviteUser)
22✔
118
        if !hasPermission || err != nil {
22✔
119
                errors.ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
×
120
                return
×
121
        }
×
122
        // get new admin info from the request body
123
        invite := &apicommon.OrganizationInvite{}
22✔
124
        if err := json.NewDecoder(r.Body).Decode(invite); err != nil {
22✔
125
                errors.ErrMalformedBody.Write(w)
×
126
                return
×
127
        }
×
128
        // check the email is correct format
129
        if !internal.ValidEmail(invite.Email) {
22✔
130
                errors.ErrEmailMalformed.Write(w)
×
131
                return
×
132
        }
×
133
        // check the role is valid
134
        if valid := db.IsValidUserRole(invite.Role); !valid {
22✔
135
                errors.ErrInvalidUserData.Withf("invalid role").Write(w)
×
136
                return
×
137
        }
×
138
        // check if the new user already has a role in the organization
139
        if hasAnyRole, err := a.db.UserHasAnyRoleInOrg(invite.Email, org.Address); err != nil && err != db.ErrNotFound {
22✔
140
                errors.ErrInvalidUserData.WithErr(err).Write(w)
×
141
                return
×
142
        } else if hasAnyRole {
22✔
143
                errors.ErrDuplicateConflict.With("user already has a role in the organization").Write(w)
×
144
                return
×
145
        }
×
146

147
        // all pre-checks OK, now update the org users counter
148
        if err := a.db.IncrementOrganizationUsersCounter(org.Address); err != nil {
33✔
149
                errors.ErrGenericInternalServerError.Withf("increment users: %v", err).Write(w)
11✔
150
                return
11✔
151
        }
11✔
152
        // create new invitation
153
        orgInvite := &db.OrganizationInvite{
11✔
154
                OrganizationAddress: org.Address,
11✔
155
                NewUserEmail:        invite.Email,
11✔
156
                Role:                invite.Role,
11✔
157
                CurrentUserID:       user.ID,
11✔
158
        }
11✔
159
        // generate the verification code and the verification link
11✔
160
        code, link, err := a.generateVerificationCodeAndLink(orgInvite, db.CodeTypeOrgInvite)
11✔
161
        if err == db.ErrAlreadyExists {
11✔
162
                if err := a.db.DecrementOrganizationUsersCounter(org.Address); err != nil {
×
163
                        log.Errorf("decrement users: %v", err)
×
164
                }
×
165
                errors.ErrDuplicateConflict.With("user is already invited to the organization").Write(w)
×
166
                return
×
167
        }
168
        if err != nil {
11✔
169
                if err := a.db.DecrementOrganizationUsersCounter(org.Address); err != nil {
×
170
                        log.Errorf("decrement users: %v", err)
×
171
                }
×
172
                errors.ErrGenericInternalServerError.Withf("could not create the invite: %v", err).Write(w)
×
173
                return
×
174
        }
175
        // send the invitation mail to invited user email with the invite code and
176
        // the invite link
177
        if err := a.sendMail(r.Context(), invite.Email, mailtemplates.InviteNotification,
11✔
178
                struct {
11✔
179
                        Organization common.Address
11✔
180
                        Code         string
11✔
181
                        Link         string
11✔
182
                }{org.Address, code, link},
11✔
183
        ); err != nil {
11✔
184
                // in this case we don't DecrementOrganizationUsersCounter because the invite was actually created,
×
185
                // just the notification failed.
×
186
                log.Warnw("could not send verification code email", "error", err)
×
187
                errors.ErrGenericInternalServerError.Write(w)
×
188
                return
×
189
        }
×
190
        apicommon.HTTPWriteOK(w)
11✔
191
}
192

193
// acceptOrganizationUserInvitationHandler godoc
194
//
195
//        @Summary                Accept an invitation to an organization
196
//        @Description        Accept an invitation to an organization. It checks if the invitation is valid and not expired, and that the
197
//        @Description        user has no role in the organization yet. If the user does not exist, it creates a new user with the provided
198
//        @Description        information. If the user already exists and is verified, it assigns a role to the user in the organization.
199
//        @Tags                        organizations
200
//        @Accept                        json
201
//        @Produce                json
202
//        @Param                        address        path                string                                                                        true        "Organization address"
203
//        @Param                        request        body                apicommon.AcceptOrganizationInvitation        true        "Invitation acceptance information"
204
//        @Success                200                {string}        string                                                                        "OK"
205
//        @Failure                400                {object}        errors.Error                                                        "Invalid input data"
206
//        @Failure                401                {object}        errors.Error                                                        "Unauthorized or invalid invitation"
207
//        @Failure                409                {object}        errors.Error                                                        "User already has a role in the organization"
208
//        @Failure                410                {object}        errors.Error                                                        "Invitation expired"
209
//        @Failure                500                {object}        errors.Error                                                        "Internal server error"
210
//        @Router                        /organizations/{address}/users/accept [post]
211
func (a *API) acceptOrganizationUserInvitationHandler(w http.ResponseWriter, r *http.Request) {
2✔
212
        // get the organization info from the request context
2✔
213
        org, _, ok := a.organizationFromRequest(r)
2✔
214
        if !ok {
2✔
215
                errors.ErrNoOrganizationProvided.Write(w)
×
216
                return
×
217
        }
×
218
        // get new user info from the request body
219
        invitationReq := &apicommon.AcceptOrganizationInvitation{}
2✔
220
        if err := json.NewDecoder(r.Body).Decode(invitationReq); err != nil {
2✔
221
                errors.ErrMalformedBody.Write(w)
×
222
                return
×
223
        }
×
224
        // get the invitation from the database
225
        invitation, err := a.db.InvitationByCode(invitationReq.Code)
2✔
226
        if err != nil {
2✔
227
                errors.ErrUnauthorized.Withf("could not get invitation: %v", err).Write(w)
×
228
                return
×
229
        }
×
230
        // check if the organization is correct
231
        if invitation.OrganizationAddress != org.Address {
2✔
232
                errors.ErrUnauthorized.Withf("invitation is not for this organization").Write(w)
×
233
                return
×
234
        }
×
235
        // create a helper function to remove the invitation from the database in
236
        // case of error or expiration
237
        removeInvitation := func() {
4✔
238
                if err := a.db.DeleteInvitationByCode(invitationReq.Code); err != nil {
2✔
239
                        log.Warnf("could not delete invitation: %v", err)
×
240
                }
×
241
        }
242
        // check if the invitation is expired
243
        if invitation.Expiration.Before(time.Now()) {
2✔
244
                go removeInvitation()
×
245
                errors.ErrInvitationExpired.Write(w)
×
246
                return
×
247
        }
×
248
        // try to get the user from the database
249
        dbUser, err := a.db.UserByEmail(invitation.NewUserEmail)
2✔
250
        if err != nil {
2✔
251
                // if the error is different from not found, return the error, if not,
×
252
                // continue to try to create the user
×
253
                if err != db.ErrNotFound {
×
254
                        errors.ErrGenericInternalServerError.Withf("could not get user: %v", err).Write(w)
×
255
                        return
×
256
                }
×
257
                // check if the user info is provided, at least the first name, last
258
                // name and the password, the email is already checked in the invitation
259
                if invitationReq.User == nil || invitationReq.User.FirstName == "" ||
×
260
                        invitationReq.User.LastName == "" || invitationReq.User.Password == "" {
×
261
                        errors.ErrMalformedBody.With("user info not provided").Write(w)
×
262
                        return
×
263
                }
×
264
                // create the new user and move on to include the organization, the user
265
                // is verified because it is an invitation and the email is already
266
                // checked in the invitation so just hash the password and create the
267
                // user with the first name and last name provided
268
                hPassword := internal.HexHashPassword(passwordSalt, invitationReq.User.Password)
×
269
                dbUser = &db.User{
×
270
                        Email:     invitation.NewUserEmail,
×
271
                        Password:  hPassword,
×
272
                        FirstName: invitationReq.User.FirstName,
×
273
                        LastName:  invitationReq.User.LastName,
×
274
                        Verified:  true,
×
275
                }
×
276
        } else {
2✔
277
                // if it does, check if the user is already verified
2✔
278
                if !dbUser.Verified {
2✔
279
                        errors.ErrUserNoVerified.With("user already exists but is not verified").Write(w)
×
280
                        return
×
281
                }
×
282
                // check if the user already has the role in the organization
283
                if _, err := a.db.UserHasRoleInOrg(invitation.NewUserEmail, org.Address, invitation.Role); err == nil {
2✔
284
                        go removeInvitation()
×
285
                        errors.ErrDuplicateConflict.With("user already has the role in the organization").Write(w)
×
286
                        return
×
287
                }
×
288
        }
289
        // include the new organization in the user
290
        dbUser.Organizations = append(dbUser.Organizations, db.OrganizationUser{
2✔
291
                Address: org.Address,
2✔
292
                Role:    invitation.Role,
2✔
293
        })
2✔
294
        // set the user in the database
2✔
295
        if _, err := a.db.SetUser(dbUser); err != nil {
2✔
296
                errors.ErrGenericInternalServerError.Withf("could not set user: %v", err).Write(w)
×
297
                return
×
298
        }
×
299
        // delete the invitation
300
        go removeInvitation()
2✔
301
        apicommon.HTTPWriteOK(w)
2✔
302
}
303

304
// updatePendingUserInvitationHandler godoc
305
//
306
//        @Summary                Update a pending invitation to an organization
307
//        @Description        Update the code, link and expiration time of a pending invitation to an organization by email.
308
//        @Description        Resend the invitation email.
309
//        @Description        Only the admin of the organization can update an invitation.
310
//        @Tags                        organizations
311
//        @Accept                        json
312
//        @Produce                json
313
//        @Security                BearerAuth
314
//        @Param                        address                        path                string                        true        "Organization address"
315
//        @Param                        invitationID        path                string                        true        "Invitation ID"
316
//        @Success                200                                {string}        string                        "OK"
317
//        @Failure                400                                {object}        errors.Error        "Invalid input data"
318
//        @Failure                401                                {object}        errors.Error        "Unauthorized"
319
//        @Failure                400                                {object}        errors.Error        "Invalid data - invitation not found"
320
//        @Failure                500                                {object}        errors.Error        "Internal server error"
321
//        @Router                        /organizations/{address}/users/pending/{invitationID} [put]
322
func (a *API) updatePendingUserInvitationHandler(w http.ResponseWriter, r *http.Request) {
4✔
323
        // get the user from the request context
4✔
324
        user, ok := apicommon.UserFromContext(r.Context())
4✔
325
        if !ok {
4✔
326
                errors.ErrUnauthorized.Write(w)
×
327
                return
×
328
        }
×
329
        // get the organization info from the request context
330
        org, _, ok := a.organizationFromRequest(r)
4✔
331
        if !ok {
4✔
332
                errors.ErrNoOrganizationProvided.Write(w)
×
333
                return
×
334
        }
×
335
        if !user.HasRoleFor(org.Address, db.AdminRole) {
5✔
336
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
337
                return
1✔
338
        }
1✔
339

340
        invitationID := chi.URLParam(r, "invitationID")
3✔
341
        if invitationID == "" {
3✔
342
                errors.ErrMalformedBody.With("invitation ID not provided").Write(w)
×
343
                return
×
344
        }
×
345
        // get the invitation from the database
346
        invitation, err := a.db.Invitation(invitationID)
3✔
347
        if err != nil {
4✔
348
                if err == db.ErrNotFound {
2✔
349
                        errors.ErrInvalidData.With("invitation not found").Write(w)
1✔
350
                        return
1✔
351
                }
1✔
352
                errors.ErrGenericInternalServerError.Withf("could not get invitation: %v", err).Write(w)
×
353
                return
×
354
        }
355

356
        if invitation.OrganizationAddress != org.Address {
3✔
357
                errors.ErrInvalidData.Withf("invitation is not for this organization").Write(w)
1✔
358
        }
1✔
359

360
        // create the updated invitation
361
        orgInvite := &db.OrganizationInvite{
2✔
362
                ID:                  invitation.ID,
2✔
363
                OrganizationAddress: invitation.OrganizationAddress,
2✔
364
                NewUserEmail:        invitation.NewUserEmail,
2✔
365
                Role:                invitation.Role,
2✔
366
                CurrentUserID:       user.ID,
2✔
367
        }
2✔
368
        // generate the verification code and the verification link
2✔
369
        code, link, err := a.generateVerificationCodeAndLink(orgInvite, db.CodeTypeOrgInviteUpdate)
2✔
370
        if err != nil {
2✔
NEW
371
                errors.ErrGenericInternalServerError.Withf("could not regenerate code for the invite: %v", err).Write(w)
×
372
                return
×
UNCOV
373
        }
×
374

375
        orgInvite.InvitationCode = code
2✔
376
        orgInvite.Expiration = time.Now().Add(apicommon.InvitationExpiration)
2✔
377
        // store the updated invitation in the database
2✔
378
        if err := a.db.UpdateInvitation(orgInvite); err != nil {
2✔
UNCOV
379
                errors.ErrGenericInternalServerError.Withf("could not update invitation: %v", err).Write(w)
×
UNCOV
380
                return
×
UNCOV
381
        }
×
382

383
        // send the invitation mail to invited user email with the invite code and
384
        // the invite link
385
        if err := a.sendMail(r.Context(), orgInvite.NewUserEmail, mailtemplates.InviteNotification,
2✔
386
                struct {
2✔
387
                        Organization common.Address
2✔
388
                        Code         string
2✔
389
                        Link         string
2✔
390
                }{org.Address, code, link},
2✔
391
        ); err != nil {
2✔
392
                log.Warnw("could not send verification code email", "error", err)
×
393
                errors.ErrGenericInternalServerError.Write(w)
×
394
                return
×
395
        }
×
396
        apicommon.HTTPWriteOK(w)
2✔
397
}
398

399
// deletePendingUserInvitationHandler godoc
400
//
401
//        @Summary                Delete a pending invitation to an organization
402
//        @Description        Delete a pending invitation to an organization by email.
403
//        @Description        Only the admin of the organization can delete an invitation.
404
//        @Tags                        organizations
405
//        @Accept                        json
406
//        @Produce                json
407
//        @Security                BearerAuth
408
//        @Param                        address                        path                string                        true        "Organization address"
409
//        @Param                        invitationID        path                string                        true        "Invitation ID"
410
//        @Success                200                                {string}        string                        "OK"
411
//        @Failure                400                                {object}        errors.Error        "Invalid input data"
412
//        @Failure                401                                {object}        errors.Error        "Unauthorized"
413
//        @Failure                400                                {object}        errors.Error        "Invalid data - invitation not found"
414
//        @Failure                500                                {object}        errors.Error        "Internal server error"
415
//        @Router                        /organizations/{address}/users/pending/{invitationID} [delete]
416
func (a *API) deletePendingUserInvitationHandler(w http.ResponseWriter, r *http.Request) {
7✔
417
        // get the user from the request context
7✔
418
        user, ok := apicommon.UserFromContext(r.Context())
7✔
419
        if !ok {
7✔
420
                errors.ErrUnauthorized.Write(w)
×
421
                return
×
422
        }
×
423
        // get the organization info from the request context
424
        org, _, ok := a.organizationFromRequest(r)
7✔
425
        if !ok {
7✔
426
                errors.ErrNoOrganizationProvided.Write(w)
×
427
                return
×
428
        }
×
429
        if !user.HasRoleFor(org.Address, db.AdminRole) {
8✔
430
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
431
                return
1✔
432
        }
1✔
433

434
        invitationID := chi.URLParam(r, "invitationID")
6✔
435
        if invitationID == "" {
6✔
436
                errors.ErrMalformedBody.With("invitation ID not provided").Write(w)
×
437
                return
×
438
        }
×
439
        // get the invitation from the database
440
        invitation, err := a.db.Invitation(invitationID)
6✔
441
        if err != nil {
7✔
442
                if err == db.ErrNotFound {
2✔
443
                        errors.ErrInvalidData.With("invitation not found").Write(w)
1✔
444
                        return
1✔
445
                }
1✔
446
                errors.ErrGenericInternalServerError.Withf("could not get invitation: %v", err).Write(w)
×
447
                return
×
448
        }
449
        // check if the organization is correct
450
        if invitation.OrganizationAddress != org.Address {
6✔
451
                errors.ErrUnauthorized.Withf("invitation is not for this organization").Write(w)
1✔
452
                return
1✔
453
        }
1✔
454

455
        if err := a.db.DeleteInvitation(invitationID); err != nil {
4✔
456
                errors.ErrGenericInternalServerError.Withf("could not get invitation: %v", err).Write(w)
×
457
                return
×
458
        }
×
459

460
        // update the org users counter
461
        if err := a.db.DecrementOrganizationUsersCounter(org.Address); err != nil {
4✔
462
                log.Errorf("decrement users: %v", err)
×
463
                errors.ErrGenericInternalServerError.Withf("could not update organization users counter: %v", err).Write(w)
×
464
                return
×
465
        }
×
466
        apicommon.HTTPWriteOK(w)
4✔
467
}
468

469
// pendingOrganizationUsersHandler godoc
470
//
471
//        @Summary                Get pending organization users
472
//        @Description        Get the list of pending invitations for an organization
473
//        @Tags                        organizations
474
//        @Accept                        json
475
//        @Produce                json
476
//        @Security                BearerAuth
477
//        @Param                        address        path                string        true        "Organization address"
478
//        @Success                200                {object}        apicommon.OrganizationInviteList
479
//        @Failure                400                {object}        errors.Error        "Invalid input data"
480
//        @Failure                401                {object}        errors.Error        "Unauthorized"
481
//        @Failure                404                {object}        errors.Error        "Organization not found"
482
//        @Failure                500                {object}        errors.Error        "Internal server error"
483
//        @Router                        /organizations/{address}/users/pending [get]
484
func (a *API) pendingOrganizationUsersHandler(w http.ResponseWriter, r *http.Request) {
7✔
485
        // get the user from the request context
7✔
486
        user, ok := apicommon.UserFromContext(r.Context())
7✔
487
        if !ok {
7✔
488
                errors.ErrUnauthorized.Write(w)
×
489
                return
×
490
        }
×
491
        // get the organization info from the request context
492
        org, _, ok := a.organizationFromRequest(r)
7✔
493
        if !ok {
7✔
494
                errors.ErrNoOrganizationProvided.Write(w)
×
495
                return
×
496
        }
×
497
        if !user.HasRoleFor(org.Address, db.AdminRole) {
7✔
498
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
499
                return
×
500
        }
×
501
        // get the pending invitations
502
        invitations, err := a.db.PendingInvitations(org.Address)
7✔
503
        if err != nil {
7✔
504
                errors.ErrGenericInternalServerError.Withf("could not get pending invitations: %v", err).Write(w)
×
505
                return
×
506
        }
×
507
        invitationsList := make([]*apicommon.OrganizationInvite, 0, len(invitations))
7✔
508
        for _, invitation := range invitations {
17✔
509
                invitationsList = append(invitationsList, &apicommon.OrganizationInvite{
10✔
510
                        ID:         invitation.ID.Hex(),
10✔
511
                        Email:      invitation.NewUserEmail,
10✔
512
                        Role:       invitation.Role,
10✔
513
                        Expiration: invitation.Expiration,
10✔
514
                })
10✔
515
        }
10✔
516
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationInviteList{Invites: invitationsList})
7✔
517
}
518

519
// organizationRolesHandler godoc
520
//
521
//        @Summary                Get available organization user roles
522
//        @Description        Get the list of available roles that can be assigned to a user of an organization
523
//        @Tags                        organizations
524
//        @Accept                        json
525
//        @Produce                json
526
//        @Success                200        {object}        apicommon.OrganizationRoleList
527
//        @Router                        /organizations/roles [get]
528
func (*API) organizationRolesHandler(w http.ResponseWriter, _ *http.Request) {
1✔
529
        availableRoles := []*apicommon.OrganizationRole{}
1✔
530
        for role, name := range db.UserRolesNames {
4✔
531
                availableRoles = append(availableRoles, &apicommon.OrganizationRole{
3✔
532
                        Role:                        string(role),
3✔
533
                        Name:                        name,
3✔
534
                        OrganizationWritePermission: db.HasOrganizationWritePermission(role),
3✔
535
                        ProcessWritePermission:      db.HasProcessWritePermission(role),
3✔
536
                })
3✔
537
        }
3✔
538
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationRoleList{Roles: availableRoles})
1✔
539
}
540

541
// updateOrganizationUserHandler godoc
542
//
543
//        @Summary                Update organization user role
544
//        @Description        Update the role of a user in an organization. Only the admin of the organization can update the role.
545
//        @Tags                        organizations
546
//        @Accept                        json
547
//        @Produce                json
548
//        @Security                BearerAuth
549
//        @Param                        address        path                string                                                                                true        "Organization address"
550
//        @Param                        userid        path                string                                                                                true        "User ID"
551
//        @Param                        request        body                apicommon.UpdateOrganizationUserRoleRequest        true        "Update user role information"
552
//        @Success                200                {string}        string                                                                                "OK"
553
//        @Failure                400                {object}        errors.Error                                                                "Invalid input data"
554
//        @Failure                401                {object}        errors.Error                                                                "Unauthorized"
555
//        @Failure                404                {object}        errors.Error                                                                "Organization not found"
556
//
557
// Note: The implementation returns 200 OK even for non-existent users
558
//
559
//        @Failure                500                {object}        errors.Error                                                                "Internal server error"
560
//        @Router                        /organizations/{address}/users/{userid} [put]
561
func (a *API) updateOrganizationUserHandler(w http.ResponseWriter, r *http.Request) {
5✔
562
        // get the user from the request context
5✔
563
        user, ok := apicommon.UserFromContext(r.Context())
5✔
564
        if !ok {
5✔
565
                errors.ErrUnauthorized.Write(w)
×
566
                return
×
567
        }
×
568
        // get the organization info from the request context
569
        org, _, ok := a.organizationFromRequest(r)
5✔
570
        if !ok {
5✔
571
                errors.ErrNoOrganizationProvided.Write(w)
×
572
                return
×
573
        }
×
574
        if !user.HasRoleFor(org.Address, db.AdminRole) {
6✔
575
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
576
                return
1✔
577
        }
1✔
578
        // get the user ID from the request path
579
        userID := chi.URLParam(r, "userid")
4✔
580
        if userID == "" {
4✔
581
                errors.ErrMalformedBody.With("user ID not provided").Write(w)
×
582
                return
×
583
        }
×
584
        // convert the user ID to the correct type
585
        userIDInt, err := strconv.Atoi(userID)
4✔
586
        if err != nil {
4✔
587
                errors.ErrMalformedBody.Withf("invalid user ID: %v", err).Write(w)
×
588
                return
×
589
        }
×
590

591
        // get the new role from the request body
592
        update := &apicommon.UpdateOrganizationUserRoleRequest{}
4✔
593
        if err := json.NewDecoder(r.Body).Decode(update); err != nil {
4✔
594
                errors.ErrMalformedBody.Write(w)
×
595
                return
×
596
        }
×
597
        if update.Role == "" {
4✔
598
                errors.ErrMalformedBody.With("role not provided").Write(w)
×
599
                return
×
600
        }
×
601
        if valid := db.IsValidUserRole(db.UserRole(update.Role)); !valid {
5✔
602
                errors.ErrInvalidUserData.Withf("invalid role").Write(w)
1✔
603
                return
1✔
604
        }
1✔
605
        if err := a.db.UpdateOrganizationUserRole(org.Address, uint64(userIDInt), db.UserRole(update.Role)); err != nil {
3✔
606
                errors.ErrInvalidUserData.Withf("user not found: %v", err).Write(w)
×
607
                return
×
608
        }
×
609
        apicommon.HTTPWriteOK(w)
3✔
610
}
611

612
// removeOrganizationUserHandler godoc
613
//
614
//        @Summary                Remove a user from the organization
615
//        @Description        Remove a user from the organization. Only the admin of the organization can remove a user.
616
//        @Tags                        organizations
617
//        @Accept                        json
618
//        @Produce                json
619
//        @Security                BearerAuth
620
//        @Param                        address        path                string                        true        "Organization address"
621
//        @Param                        userid        path                string                        true        "User ID"
622
//        @Success                200                {string}        string                        "OK"
623
//        @Failure                400                {object}        errors.Error        "Invalid input data"
624
//        @Failure                401                {object}        errors.Error        "Unauthorized"
625
//        @Failure                404                {object}        errors.Error        "Organization not found"
626
//
627
// Note: The implementation returns 200 OK even for non-existent users
628
//
629
//        @Failure                400                {object}        errors.Error        "Invalid input data - User cannot remove itself"
630
//        @Failure                500                {object}        errors.Error        "Internal server error"
631
//        @Router                        /organizations/{address}/users/{userid} [delete]
632
func (a *API) removeOrganizationUserHandler(w http.ResponseWriter, r *http.Request) {
4✔
633
        // get the user from the request context
4✔
634
        user, ok := apicommon.UserFromContext(r.Context())
4✔
635
        if !ok {
4✔
636
                errors.ErrUnauthorized.Write(w)
×
637
                return
×
638
        }
×
639
        // get the organization info from the request context
640
        org, _, ok := a.organizationFromRequest(r)
4✔
641
        if !ok {
4✔
642
                errors.ErrNoOrganizationProvided.Write(w)
×
643
                return
×
644
        }
×
645
        if !user.HasRoleFor(org.Address, db.AdminRole) {
5✔
646
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
647
                return
1✔
648
        }
1✔
649
        // get the user ID from the request path
650
        userID := chi.URLParam(r, "userid")
3✔
651
        if userID == "" {
3✔
652
                errors.ErrMalformedBody.With("user ID not provided").Write(w)
×
653
                return
×
654
        }
×
655
        // convert the user ID to the correct type
656
        userIDInt, err := strconv.Atoi(userID)
3✔
657
        if err != nil {
3✔
658
                errors.ErrMalformedBody.Withf("invalid user ID: %v", err).Write(w)
×
659
                return
×
660
        }
×
661
        if uint64(userIDInt) == user.ID {
4✔
662
                errors.ErrInvalidUserData.With("user cannot remove itself from the organization").Write(w)
1✔
663
                return
1✔
664
        }
1✔
665
        if err := a.db.RemoveOrganizationUser(org.Address, uint64(userIDInt)); err != nil {
2✔
666
                errors.ErrInvalidUserData.Withf("user not found: %v", err).Write(w)
×
667
                return
×
668
        }
×
669

670
        // update the org users counter
671
        if err := a.db.DecrementOrganizationUsersCounter(org.Address); err != nil {
2✔
672
                log.Errorf("decrement users: %v", err)
×
673
                errors.ErrGenericInternalServerError.Withf("could not update organization users counter: %v", err).Write(w)
×
674
                return
×
675
        }
×
676
        apicommon.HTTPWriteOK(w)
2✔
677
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc