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

vocdoni / saas-backend / 17557469823

08 Sep 2025 04:25PM UTC coverage: 58.777% (-0.06%) from 58.841%
17557469823

Pull #213

github

altergui
fix
Pull Request #213: api: standardize parameters ProcessID, CensusID, GroupID, JobID, UserID, BundleID

254 of 345 new or added lines in 22 files covered. (73.62%)

19 existing lines in 7 files now uncovered.

5652 of 9616 relevant lines covered (58.78%)

32.01 hits per line

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

55.45
/api/organization_users.go
1
package api
2

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

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

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

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

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

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

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

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

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

355
        // create the updated invitation
356
        orgInvite := &db.OrganizationInvite{
2✔
357
                ID:                  invitation.ID,
2✔
358
                OrganizationAddress: org.Address,
2✔
359
                NewUserEmail:        invitation.NewUserEmail,
2✔
360
                Role:                db.UserRole(invitation.Role),
2✔
361
                CurrentUserID:       user.ID,
2✔
362
        }
2✔
363
        // generate the verification code and the verification link
2✔
364
        code, link, err := a.generateVerificationCodeAndLink(orgInvite, db.CodeTypeOrgInviteUpdate)
2✔
365
        if err != nil {
2✔
366
                if err == db.ErrAlreadyExists {
×
367
                        errors.ErrDuplicateConflict.With("user is already invited to the organization").Write(w)
×
368
                        return
×
369
                }
×
370
                errors.ErrGenericInternalServerError.Withf("could not create the invite: %v", err).Write(w)
×
371
                return
×
372
        }
373

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

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

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

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

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

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

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

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

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

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

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

657
        // update the org users counter
658
        if err := a.db.DecrementOrganizationUsersCounter(org.Address); err != nil {
2✔
659
                log.Errorf("decrement users: %v", err)
×
660
                errors.ErrGenericInternalServerError.Withf("could not update organization users counter: %v", err).Write(w)
×
661
                return
×
662
        }
×
663
        apicommon.HTTPWriteOK(w)
2✔
664
}
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