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

vocdoni / saas-backend / 15048859546

15 May 2025 03:21PM UTC coverage: 51.892% (-0.3%) from 52.143%
15048859546

Pull #112

github

emmdim
WIP
Pull Request #112: WIP

10 of 51 new or added lines in 3 files covered. (19.61%)

14 existing lines in 2 files now uncovered.

3839 of 7398 relevant lines covered (51.89%)

21.45 hits per line

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

9.78
/api/organizations.go
1
package api
2

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

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

18
// createOrganizationHandler godoc
19
//
20
//        @Summary                Create a new organization
21
//        @Description        Create a new organization. If the organization is a suborganization, the parent organization must be
22
//        @Description        specified in the request body, and the user must be an admin of the parent. If the parent organization
23
//        @Description        is already a suborganization, an error is returned.
24
//        @Tags                        organizations
25
//        @Accept                        json
26
//        @Produce                json
27
//        @Security                BearerAuth
28
//        @Param                        request        body                apicommon.OrganizationInfo        true        "Organization information"
29
//        @Success                200                {object}        apicommon.OrganizationInfo
30
//        @Failure                400                {object}        errors.Error        "Invalid input data"
31
//        @Failure                401                {object}        errors.Error        "Unauthorized"
32
//        @Failure                404                {object}        errors.Error        "Parent organization not found"
33
//        @Failure                500                {object}        errors.Error        "Internal server error"
34
//        @Router                        /organizations [post]
35
func (a *API) createOrganizationHandler(w http.ResponseWriter, r *http.Request) {
6✔
36
        // get the user from the request context
6✔
37
        user, ok := apicommon.UserFromContext(r.Context())
6✔
38
        if !ok {
6✔
39
                errors.ErrUnauthorized.Write(w)
×
40
                return
×
41
        }
×
42
        // get the organization info from the request body
43
        orgInfo := &apicommon.OrganizationInfo{}
6✔
44
        if err := json.NewDecoder(r.Body).Decode(orgInfo); err != nil {
6✔
45
                errors.ErrMalformedBody.Write(w)
×
46
                return
×
47
        }
×
48
        // create the organization signer to store the address and the nonce
49
        signer, nonce, err := account.NewSigner(a.secret, user.Email) // TODO: replace email with something else such as user ID
6✔
50
        if err != nil {
6✔
51
                errors.ErrGenericInternalServerError.Withf("could not create organization signer: %v", err).Write(w)
×
52
                return
×
53
        }
×
54
        // check if the organization type is valid
55
        if !db.IsOrganizationTypeValid(orgInfo.Type) {
6✔
56
                errors.ErrMalformedBody.Withf("invalid organization type").Write(w)
×
57
                return
×
58
        }
×
59
        parentOrg := ""
6✔
60
        var dbParentOrg *db.Organization
6✔
61
        if orgInfo.Parent != nil {
6✔
62
                // check if the org has permission to create suborganizations
×
63
                hasPermission, err := a.subscriptions.HasDBPersmission(user.Email, orgInfo.Parent.Address, subscriptions.CreateSubOrg)
×
64
                if !hasPermission || err != nil {
×
65
                        errors.ErrUnauthorized.Withf("user does not have permission to create suborganizations: %v", err).Write(w)
×
66
                        return
×
67
                }
×
68

69
                dbParentOrg, err = a.db.Organization(orgInfo.Parent.Address)
×
70
                if err != nil {
×
71
                        if err == db.ErrNotFound {
×
72
                                errors.ErrOrganizationNotFound.Withf("parent organization not found").Write(w)
×
73
                                return
×
74
                        }
×
75
                        errors.ErrGenericInternalServerError.Withf("could not get parent organization: %v", err).Write(w)
×
76
                        return
×
77
                }
78
                if dbParentOrg.Parent != "" {
×
79
                        errors.ErrMalformedBody.Withf("parent organization is already a suborganization").Write(w)
×
80
                        return
×
81
                }
×
82
                isAdmin, err := a.db.IsMemberOf(user.Email, dbParentOrg.Address, db.AdminRole)
×
83
                if err != nil {
×
84
                        errors.ErrGenericInternalServerError.Withf("could not check if user is admin of parent organization: %v", err).Write(w)
×
85
                        return
×
86
                }
×
87
                if !isAdmin {
×
88
                        errors.ErrUnauthorized.Withf("user is not admin of parent organization").Write(w)
×
89
                        return
×
90
                }
×
91
                parentOrg = orgInfo.Parent.Address
×
92
        }
93
        // find default plan
94
        defaultPlan, err := a.db.DefaultPlan()
6✔
95
        if err != nil || defaultPlan == nil {
6✔
96
                errors.ErrNoDefaultPlan.WithErr((err)).Write(w)
×
97
                return
×
98
        }
×
99
        subscription := &db.OrganizationSubscription{
6✔
100
                PlanID:        defaultPlan.ID,
6✔
101
                StartDate:     time.Now(),
6✔
102
                Active:        true,
6✔
103
                MaxCensusSize: defaultPlan.Organization.MaxCensus,
6✔
104
        }
6✔
105
        // create the organization
6✔
106
        dbOrg := &db.Organization{
6✔
107
                Address:         signer.AddressString(),
6✔
108
                Website:         orgInfo.Website,
6✔
109
                Creator:         user.Email,
6✔
110
                CreatedAt:       time.Now(),
6✔
111
                Nonce:           nonce,
6✔
112
                Type:            db.OrganizationType(orgInfo.Type),
6✔
113
                Size:            orgInfo.Size,
6✔
114
                Color:           orgInfo.Color,
6✔
115
                Country:         orgInfo.Country,
6✔
116
                Subdomain:       orgInfo.Subdomain,
6✔
117
                Timezone:        orgInfo.Timezone,
6✔
118
                Active:          true,
6✔
119
                Communications:  orgInfo.Communications,
6✔
120
                TokensPurchased: 0,
6✔
121
                TokensRemaining: 0,
6✔
122
                Parent:          parentOrg,
6✔
123
                Subscription:    *subscription,
6✔
124
        }
6✔
125
        if err := a.db.SetOrganization(dbOrg); err != nil {
6✔
126
                if err == db.ErrAlreadyExists {
×
127
                        errors.ErrInvalidOrganizationData.WithErr(err).Write(w)
×
128
                        return
×
129
                }
×
130
                errors.ErrGenericInternalServerError.Write(w)
×
131
                return
×
132
        }
133

134
        // update the parent organization counter
135
        if orgInfo.Parent != nil {
6✔
136
                dbParentOrg.Counters.SubOrgs++
×
137
                if err := a.db.SetOrganization(dbParentOrg); err != nil {
×
138
                        errors.ErrGenericInternalServerError.Withf("could not update parent organization: %v", err).Write(w)
×
139
                        return
×
140
                }
×
141
        }
142
        // send the organization back to the user
143
        apicommon.HTTPWriteJSON(w, apicommon.OrganizationFromDB(dbOrg, dbParentOrg))
6✔
144
}
145

146
// organizationInfoHandler godoc
147
//
148
//        @Summary                Get organization information
149
//        @Description        Get information about an organization
150
//        @Tags                        organizations
151
//        @Accept                        json
152
//        @Produce                json
153
//        @Param                        address        path                string        true        "Organization address"
154
//        @Success                200                {object}        apicommon.OrganizationInfo
155
//        @Failure                400                {object}        errors.Error        "Invalid input data"
156
//        @Failure                404                {object}        errors.Error        "Organization not found"
157
//        @Failure                500                {object}        errors.Error        "Internal server error"
158
//        @Router                        /organizations/{address} [get]
159
func (a *API) organizationInfoHandler(w http.ResponseWriter, r *http.Request) {
5✔
160
        // get the organization info from the request context
5✔
161
        org, parent, ok := a.organizationFromRequest(r)
5✔
162
        if !ok {
5✔
163
                errors.ErrNoOrganizationProvided.Write(w)
×
164
                return
×
165
        }
×
166
        // send the organization back to the user
167
        apicommon.HTTPWriteJSON(w, apicommon.OrganizationFromDB(org, parent))
5✔
168
}
169

170
// organizationMembersHandler godoc
171
//
172
//        @Summary                Get organization members
173
//        @Description        Get the list of members with their roles in the organization
174
//        @Tags                        organizations
175
//        @Accept                        json
176
//        @Produce                json
177
//        @Security                BearerAuth
178
//        @Param                        address        path                string        true        "Organization address"
179
//        @Success                200                {object}        apicommon.OrganizationMembers
180
//        @Failure                400                {object}        errors.Error        "Invalid input data"
181
//        @Failure                401                {object}        errors.Error        "Unauthorized"
182
//        @Failure                404                {object}        errors.Error        "Organization not found"
183
//        @Failure                500                {object}        errors.Error        "Internal server error"
184
//        @Router                        /organizations/{address}/members [get]
185
func (a *API) organizationMembersHandler(w http.ResponseWriter, r *http.Request) {
×
186
        // get the user from the request context
×
187
        user, ok := apicommon.UserFromContext(r.Context())
×
188
        if !ok {
×
189
                errors.ErrUnauthorized.Write(w)
×
190
                return
×
191
        }
×
192
        // get the organization info from the request context
193
        org, _, ok := a.organizationFromRequest(r)
×
194
        if !ok {
×
195
                errors.ErrNoOrganizationProvided.Write(w)
×
196
                return
×
197
        }
×
198
        if !user.HasRoleFor(org.Address, db.AdminRole) {
×
199
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
200
                return
×
201
        }
×
202
        // send the organization back to the user
203
        members, err := a.db.OrganizationsMembers(org.Address)
×
204
        if err != nil {
×
205
                errors.ErrGenericInternalServerError.Withf("could not get organization members: %v", err).Write(w)
×
206
                return
×
207
        }
×
208
        orgMembers := apicommon.OrganizationMembers{
×
209
                Members: make([]*apicommon.OrganizationMember, 0, len(members)),
×
210
        }
×
211
        for _, member := range members {
×
212
                var role string
×
213
                for _, userOrg := range member.Organizations {
×
214
                        if userOrg.Address == org.Address {
×
215
                                role = string(userOrg.Role)
×
216
                                break
×
217
                        }
218
                }
219
                if role == "" {
×
220
                        continue
×
221
                }
222
                orgMembers.Members = append(orgMembers.Members, &apicommon.OrganizationMember{
×
223
                        Info: &apicommon.UserInfo{
×
224
                                Email:     member.Email,
×
225
                                FirstName: member.FirstName,
×
226
                                LastName:  member.LastName,
×
227
                        },
×
228
                        Role: role,
×
229
                })
×
230
        }
231
        apicommon.HTTPWriteJSON(w, orgMembers)
×
232
}
233

234
// updateOrganizationHandler godoc
235
//
236
//        @Summary                Update organization information
237
//        @Description        Update the information of an organization. Only the admin of the organization can update the information.
238
//        @Description        Only certain fields can be updated, and they will be updated only if they are not empty.
239
//        @Tags                        organizations
240
//        @Accept                        json
241
//        @Produce                json
242
//        @Security                BearerAuth
243
//        @Param                        address        path                string                                                true        "Organization address"
244
//        @Param                        request        body                apicommon.OrganizationInfo        true        "Organization information to update"
245
//        @Success                200                {string}        string                                                "OK"
246
//        @Failure                400                {object}        errors.Error                                "Invalid input data"
247
//        @Failure                401                {object}        errors.Error                                "Unauthorized"
248
//        @Failure                404                {object}        errors.Error                                "Organization not found"
249
//        @Failure                500                {object}        errors.Error                                "Internal server error"
250
//        @Router                        /organizations/{address} [put]
251
func (a *API) updateOrganizationHandler(w http.ResponseWriter, r *http.Request) {
×
252
        // get the user from the request context
×
253
        user, ok := apicommon.UserFromContext(r.Context())
×
254
        if !ok {
×
255
                errors.ErrUnauthorized.Write(w)
×
256
                return
×
257
        }
×
258
        // get the organization info from the request context
259
        org, _, ok := a.organizationFromRequest(r)
×
260
        if !ok {
×
261
                errors.ErrNoOrganizationProvided.Write(w)
×
262
                return
×
263
        }
×
264
        if !user.HasRoleFor(org.Address, db.AdminRole) {
×
265
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
266
                return
×
267
        }
×
268
        // get the organization info from the request body
269
        newOrgInfo := &apicommon.OrganizationInfo{}
×
270
        if err := json.NewDecoder(r.Body).Decode(newOrgInfo); err != nil {
×
271
                errors.ErrMalformedBody.Write(w)
×
272
                return
×
273
        }
×
274
        // update just the fields that can be updated and are not empty
275
        updateOrg := false
×
276
        if newOrgInfo.Website != "" {
×
277
                org.Website = newOrgInfo.Website
×
278
                updateOrg = true
×
279
        }
×
280
        if newOrgInfo.Size != "" {
×
281
                org.Size = newOrgInfo.Size
×
282
                updateOrg = true
×
283
        }
×
284
        if newOrgInfo.Color != "" {
×
285
                org.Color = newOrgInfo.Color
×
286
                updateOrg = true
×
287
        }
×
288
        if newOrgInfo.Subdomain != "" {
×
289
                org.Subdomain = newOrgInfo.Subdomain
×
290
                updateOrg = true
×
291
        }
×
292
        if newOrgInfo.Country != "" {
×
293
                org.Country = newOrgInfo.Country
×
294
                updateOrg = true
×
295
        }
×
296
        if newOrgInfo.Timezone != "" {
×
297
                org.Timezone = newOrgInfo.Timezone
×
298
                updateOrg = true
×
299
        }
×
300
        if newOrgInfo.Active != org.Active {
×
301
                org.Active = newOrgInfo.Active
×
302
                updateOrg = true
×
303
        }
×
304
        // update the organization if any field was changed
305
        if updateOrg {
×
306
                if err := a.db.SetOrganization(org); err != nil {
×
307
                        errors.ErrGenericInternalServerError.Withf("could not update organization: %v", err).Write(w)
×
308
                        return
×
309
                }
×
310
        }
311
        apicommon.HTTPWriteOK(w)
×
312
}
313

314
// inviteOrganizationMemberHandler godoc
315
//
316
//        @Summary                Invite a new member to an organization
317
//        @Description        Invite a new member to an organization. Only the admin of the organization can invite a new member.
318
//        @Description        It stores the invitation in the database and sends an email to the new member with the invitation code.
319
//        @Tags                        organizations
320
//        @Accept                        json
321
//        @Produce                json
322
//        @Security                BearerAuth
323
//        @Param                        address        path                string                                                        true        "Organization address"
324
//        @Param                        request        body                apicommon.OrganizationInvite        true        "Invitation information"
325
//        @Success                200                {string}        string                                                        "OK"
326
//        @Failure                400                {object}        errors.Error                                        "Invalid input data"
327
//        @Failure                401                {object}        errors.Error                                        "Unauthorized"
328
//        @Failure                409                {object}        errors.Error                                        "User is already a member of the organization"
329
//        @Failure                500                {object}        errors.Error                                        "Internal server error"
330
//        @Router                        /organizations/{address}/members [post]
331
func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Request) {
×
332
        // get the user from the request context
×
333
        user, ok := apicommon.UserFromContext(r.Context())
×
334
        if !ok {
×
335
                errors.ErrUnauthorized.Write(w)
×
336
                return
×
337
        }
×
338
        // get the organization info from the request context
339
        org, _, ok := a.organizationFromRequest(r)
×
340
        if !ok {
×
341
                errors.ErrNoOrganizationProvided.Write(w)
×
342
                return
×
343
        }
×
344

345
        // check if the user/org has permission to invite members
346
        hasPermission, err := a.subscriptions.HasDBPersmission(user.Email, org.Address, subscriptions.InviteMember)
×
347
        if !hasPermission || err != nil {
×
348
                errors.ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
×
349
                return
×
350
        }
×
351
        // get new admin info from the request body
352
        invite := &apicommon.OrganizationInvite{}
×
353
        if err := json.NewDecoder(r.Body).Decode(invite); err != nil {
×
354
                errors.ErrMalformedBody.Write(w)
×
355
                return
×
356
        }
×
357
        // check the email is correct format
358
        if !internal.ValidEmail(invite.Email) {
×
359
                errors.ErrEmailMalformed.Write(w)
×
360
                return
×
361
        }
×
362
        // check the role is valid
363
        if valid := db.IsValidUserRole(db.UserRole(invite.Role)); !valid {
×
364
                errors.ErrInvalidUserData.Withf("invalid role").Write(w)
×
365
                return
×
366
        }
×
367
        // check if the new user is already a member of the organization
368
        if _, err := a.db.IsMemberOf(invite.Email, org.Address, db.AdminRole); err == nil {
×
369
                errors.ErrDuplicateConflict.With("user is already admin of organization").Write(w)
×
370
                return
×
371
        }
×
372
        // create new invitation
373
        orgInvite := &db.OrganizationInvite{
×
374
                OrganizationAddress: org.Address,
×
375
                NewUserEmail:        invite.Email,
×
376
                Role:                db.UserRole(invite.Role),
×
377
                CurrentUserID:       user.ID,
×
378
        }
×
379
        // generate the verification code and the verification link
×
380
        code, link, err := a.generateVerificationCodeAndLink(orgInvite, db.CodeTypeOrgInvite)
×
381
        if err != nil {
×
382
                if err == db.ErrAlreadyExists {
×
383
                        errors.ErrDuplicateConflict.With("user is already invited to the organization").Write(w)
×
384
                        return
×
385
                }
×
386
                errors.ErrGenericInternalServerError.Withf("could not create the invite: %v", err).Write(w)
×
387
                return
×
388
        }
389
        // send the invitation mail to invited user email with the invite code and
390
        // the invite link
391
        if err := a.sendMail(r.Context(), invite.Email, mailtemplates.InviteNotification,
×
392
                struct {
×
393
                        Organization string
×
394
                        Code         string
×
395
                        Link         string
×
396
                }{org.Address, code, link},
×
397
        ); err != nil {
×
398
                log.Warnw("could not send verification code email", "error", err)
×
399
                errors.ErrGenericInternalServerError.Write(w)
×
400
                return
×
401
        }
×
402

403
        // update the org members counter
404
        org.Counters.Members++
×
405
        if err := a.db.SetOrganization(org); err != nil {
×
406
                errors.ErrGenericInternalServerError.Withf("could not update organization: %v", err).Write(w)
×
407
                return
×
408
        }
×
409
        apicommon.HTTPWriteOK(w)
×
410
}
411

412
// acceptOrganizationMemberInvitationHandler godoc
413
//
414
//        @Summary                Accept an invitation to an organization
415
//        @Description        Accept an invitation to an organization. It checks if the invitation is valid and not expired, and if the
416
//        @Description        user is not already a member of the organization. If the user does not exist, it creates a new user with
417
//        @Description        the provided information. If the user already exists and is verified, it adds the organization to the user.
418
//        @Tags                        organizations
419
//        @Accept                        json
420
//        @Produce                json
421
//        @Param                        address        path                string                                                                        true        "Organization address"
422
//        @Param                        request        body                apicommon.AcceptOrganizationInvitation        true        "Invitation acceptance information"
423
//        @Success                200                {string}        string                                                                        "OK"
424
//        @Failure                400                {object}        errors.Error                                                        "Invalid input data"
425
//        @Failure                401                {object}        errors.Error                                                        "Unauthorized or invalid invitation"
426
//        @Failure                409                {object}        errors.Error                                                        "User is already a member of the organization"
427
//        @Failure                410                {object}        errors.Error                                                        "Invitation expired"
428
//        @Failure                500                {object}        errors.Error                                                        "Internal server error"
429
//        @Router                        /organizations/{address}/members/accept [post]
430
func (a *API) acceptOrganizationMemberInvitationHandler(w http.ResponseWriter, r *http.Request) {
×
431
        // get the organization info from the request context
×
432
        org, _, ok := a.organizationFromRequest(r)
×
433
        if !ok {
×
434
                errors.ErrNoOrganizationProvided.Write(w)
×
435
                return
×
436
        }
×
437
        // get new member info from the request body
438
        invitationReq := &apicommon.AcceptOrganizationInvitation{}
×
439
        if err := json.NewDecoder(r.Body).Decode(invitationReq); err != nil {
×
440
                errors.ErrMalformedBody.Write(w)
×
441
                return
×
442
        }
×
443
        // get the invitation from the database
444
        invitation, err := a.db.Invitation(invitationReq.Code)
×
445
        if err != nil {
×
446
                errors.ErrUnauthorized.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 {
×
451
                errors.ErrUnauthorized.Withf("invitation is not for this organization").Write(w)
×
452
                return
×
453
        }
×
454
        // create a helper function to remove the invitation from the database in
455
        // case of error or expiration
456
        removeInvitation := func() {
×
457
                if err := a.db.DeleteInvitation(invitationReq.Code); err != nil {
×
458
                        log.Warnf("could not delete invitation: %v", err)
×
459
                }
×
460
        }
461
        // check if the invitation is expired
462
        if invitation.Expiration.Before(time.Now()) {
×
463
                go removeInvitation()
×
464
                errors.ErrInvitationExpired.Write(w)
×
465
                return
×
466
        }
×
467
        // try to get the user from the database
468
        dbUser, err := a.db.UserByEmail(invitation.NewUserEmail)
×
469
        if err != nil {
×
470
                // if the error is different from not found, return the error, if not,
×
471
                // continue to try to create the user
×
472
                if err != db.ErrNotFound {
×
473
                        errors.ErrGenericInternalServerError.Withf("could not get user: %v", err).Write(w)
×
474
                        return
×
475
                }
×
476
                // check if the user info is provided, at least the first name, last
477
                // name and the password, the email is already checked in the invitation
478
                if invitationReq.User == nil || invitationReq.User.FirstName == "" ||
×
479
                        invitationReq.User.LastName == "" || invitationReq.User.Password == "" {
×
480
                        errors.ErrMalformedBody.With("user info not provided").Write(w)
×
481
                        return
×
482
                }
×
483
                // create the new user and move on to include the organization, the user
484
                // is verified because it is an invitation and the email is already
485
                // checked in the invitation so just hash the password and create the
486
                // user with the first name and last name provided
487
                hPassword := internal.HexHashPassword(passwordSalt, invitationReq.User.Password)
×
488
                dbUser = &db.User{
×
489
                        Email:     invitation.NewUserEmail,
×
490
                        Password:  hPassword,
×
491
                        FirstName: invitationReq.User.FirstName,
×
492
                        LastName:  invitationReq.User.LastName,
×
493
                        Verified:  true,
×
494
                }
×
495
        } else {
×
496
                // if it does, check if the user is already verified
×
497
                if !dbUser.Verified {
×
498
                        errors.ErrUserNoVerified.With("user already exists but is not verified").Write(w)
×
499
                        return
×
500
                }
×
501
                // check if the user is already a member of the organization
502
                if _, err := a.db.IsMemberOf(invitation.NewUserEmail, org.Address, invitation.Role); err == nil {
×
503
                        go removeInvitation()
×
504
                        errors.ErrDuplicateConflict.With("user is already admin of organization").Write(w)
×
505
                        return
×
506
                }
×
507
        }
508
        // include the new organization in the user
509
        dbUser.Organizations = append(dbUser.Organizations, db.OrganizationMember{
×
510
                Address: org.Address,
×
511
                Role:    invitation.Role,
×
512
        })
×
513
        // set the user in the database
×
514
        if _, err := a.db.SetUser(dbUser); err != nil {
×
515
                errors.ErrGenericInternalServerError.Withf("could not set user: %v", err).Write(w)
×
516
                return
×
517
        }
×
518
        // delete the invitation
519
        go removeInvitation()
×
520
        apicommon.HTTPWriteOK(w)
×
521
}
522

523
// pendingOrganizationMembersHandler godoc
524
//
525
//        @Summary                Get pending organization members
526
//        @Description        Get the list of pending invitations for an organization
527
//        @Tags                        organizations
528
//        @Accept                        json
529
//        @Produce                json
530
//        @Security                BearerAuth
531
//        @Param                        address        path                string        true        "Organization address"
532
//        @Success                200                {object}        apicommon.OrganizationInviteList
533
//        @Failure                400                {object}        errors.Error        "Invalid input data"
534
//        @Failure                401                {object}        errors.Error        "Unauthorized"
535
//        @Failure                404                {object}        errors.Error        "Organization not found"
536
//        @Failure                500                {object}        errors.Error        "Internal server error"
537
//        @Router                        /organizations/{address}/members/pending [get]
538
func (a *API) pendingOrganizationMembersHandler(w http.ResponseWriter, r *http.Request) {
×
539
        // get the user from the request context
×
540
        user, ok := apicommon.UserFromContext(r.Context())
×
541
        if !ok {
×
542
                errors.ErrUnauthorized.Write(w)
×
543
                return
×
544
        }
×
545
        // get the organization info from the request context
546
        org, _, ok := a.organizationFromRequest(r)
×
547
        if !ok {
×
548
                errors.ErrNoOrganizationProvided.Write(w)
×
549
                return
×
550
        }
×
551
        if !user.HasRoleFor(org.Address, db.AdminRole) {
×
552
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
553
                return
×
554
        }
×
555
        // get the pending invitations
556
        invitations, err := a.db.PendingInvitations(org.Address)
×
557
        if err != nil {
×
558
                errors.ErrGenericInternalServerError.Withf("could not get pending invitations: %v", err).Write(w)
×
559
                return
×
560
        }
×
561
        invitationsList := make([]*apicommon.OrganizationInvite, 0, len(invitations))
×
562
        for _, invitation := range invitations {
×
563
                invitationsList = append(invitationsList, &apicommon.OrganizationInvite{
×
564
                        Email:      invitation.NewUserEmail,
×
565
                        Role:       string(invitation.Role),
×
566
                        Expiration: invitation.Expiration,
×
567
                })
×
568
        }
×
569
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationInviteList{Invites: invitationsList})
×
570
}
571

572
// organizationsMembersRolesHandler godoc
573
//
574
//        @Summary                Get available organization member roles
575
//        @Description        Get the list of available roles that can be assigned to a member of an organization
576
//        @Tags                        organizations
577
//        @Accept                        json
578
//        @Produce                json
579
//        @Success                200        {object}        apicommon.OrganizationRoleList
580
//        @Router                        /organizations/roles [get]
581
func (*API) organizationsMembersRolesHandler(w http.ResponseWriter, _ *http.Request) {
×
582
        availableRoles := []*apicommon.OrganizationRole{}
×
583
        for role, name := range db.UserRolesNames {
×
584
                availableRoles = append(availableRoles, &apicommon.OrganizationRole{
×
585
                        Role:            string(role),
×
586
                        Name:            name,
×
587
                        WritePermission: db.HasWriteAccess(role),
×
588
                })
×
589
        }
×
590
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationRoleList{Roles: availableRoles})
×
591
}
592

593
// organizationsTypesHandler godoc
594
//
595
//        @Summary                Get available organization types
596
//        @Description        Get the list of available organization types that can be assigned to an organization
597
//        @Tags                        organizations
598
//        @Accept                        json
599
//        @Produce                json
600
//        @Success                200        {object}        apicommon.OrganizationTypeList
601
//        @Router                        /organizations/types [get]
602
func (*API) organizationsTypesHandler(w http.ResponseWriter, _ *http.Request) {
×
603
        organizationTypes := []*apicommon.OrganizationType{}
×
604
        for orgType, name := range db.OrganizationTypesNames {
×
605
                organizationTypes = append(organizationTypes, &apicommon.OrganizationType{
×
606
                        Type: string(orgType),
×
607
                        Name: name,
×
608
                })
×
609
        }
×
610
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationTypeList{Types: organizationTypes})
×
611
}
612

613
// organizationSubscriptionHandler godoc
614
//
615
//        @Summary                Get organization subscription
616
//        @Description        Get the subscription information for an organization
617
//        @Tags                        organizations
618
//        @Accept                        json
619
//        @Produce                json
620
//        @Security                BearerAuth
621
//        @Param                        address        path                string        true        "Organization address"
622
//        @Success                200                {object}        apicommon.OrganizationSubscriptionInfo
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 or no subscription"
626
//        @Failure                500                {object}        errors.Error        "Internal server error"
627
//        @Router                        /organizations/{address}/subscription [get]
628
func (a *API) organizationSubscriptionHandler(w http.ResponseWriter, r *http.Request) {
×
629
        // get the user from the request context
×
630
        user, ok := apicommon.UserFromContext(r.Context())
×
631
        if !ok {
×
632
                errors.ErrUnauthorized.Write(w)
×
633
                return
×
634
        }
×
635
        // get the organization info from the request context
636
        org, _, ok := a.organizationFromRequest(r)
×
637
        if !ok {
×
638
                errors.ErrNoOrganizationProvided.Write(w)
×
639
                return
×
640
        }
×
641
        if !user.HasRoleFor(org.Address, db.AdminRole) {
×
642
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
643
                return
×
644
        }
×
645
        if org.Subscription == (db.OrganizationSubscription{}) {
×
646
                errors.ErrNoOrganizationSubscription.Write(w)
×
647
                return
×
648
        }
×
649
        // get the subscription from the database
650
        plan, err := a.db.Plan(org.Subscription.PlanID)
×
651
        if err != nil {
×
652
                errors.ErrGenericInternalServerError.Withf("could not get subscription: %v", err).Write(w)
×
653
                return
×
654
        }
×
655
        info := &apicommon.OrganizationSubscriptionInfo{
×
656
                SubcriptionDetails: apicommon.SubscriptionDetailsFromDB(&org.Subscription),
×
657
                Usage:              apicommon.SubscriptionUsageFromDB(&org.Counters),
×
658
                Plan:               apicommon.SubscriptionPlanFromDB(plan),
×
659
        }
×
660
        apicommon.HTTPWriteJSON(w, info)
×
661
}
662

663
// organizationCensusesHandler godoc
664
//
665
//        @Summary                Get organization censuses
666
//        @Description        Get the list of censuses for an organization
667
//        @Tags                        organizations
668
//        @Accept                        json
669
//        @Produce                json
670
//        @Security                BearerAuth
671
//        @Param                        address        path                string        true        "Organization address"
672
//        @Success                200                {object}        apicommon.OrganizationCensuses
673
//        @Failure                400                {object}        errors.Error        "Invalid input data"
674
//        @Failure                401                {object}        errors.Error        "Unauthorized"
675
//        @Failure                404                {object}        errors.Error        "Organization not found"
676
//        @Failure                500                {object}        errors.Error        "Internal server error"
677
//        @Router                        /organizations/{address}/censuses [get]
678
func (a *API) organizationCensusesHandler(w http.ResponseWriter, r *http.Request) {
×
679
        // get the user from the request context
×
680
        user, ok := apicommon.UserFromContext(r.Context())
×
681
        if !ok {
×
682
                errors.ErrUnauthorized.Write(w)
×
683
                return
×
684
        }
×
685
        // get the organization info from the request context
686
        org, _, ok := a.organizationFromRequest(r)
×
687
        if !ok {
×
688
                errors.ErrNoOrganizationProvided.Write(w)
×
689
                return
×
690
        }
×
691
        if !user.HasRoleFor(org.Address, db.AdminRole) {
×
692
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
693
                return
×
694
        }
×
695
        // get the censuses from the database
696
        censuses, err := a.db.CensusesByOrg(org.Address)
×
697
        if err != nil {
×
698
                if err == db.ErrNotFound {
×
699
                        errors.ErrOrganizationNotFound.Write(w)
×
700
                        return
×
701
                }
×
702
                errors.ErrGenericInternalServerError.Withf("could not get censuses: %v", err).Write(w)
×
703
                return
×
704
        }
705
        // decode the censuses from the database
706
        result := apicommon.OrganizationCensuses{
×
707
                Censuses: []apicommon.OrganizationCensus{},
×
708
        }
×
709
        for _, census := range censuses {
×
710
                result.Censuses = append(result.Censuses, apicommon.OrganizationCensusFromDB(census))
×
711
        }
×
712
        apicommon.HTTPWriteJSON(w, result)
×
713
}
714

715
// organizationCreateTicket godoc
716
//
717
//        @Summary                Create a new ticket for an organization
718
//        @Description        Create a new ticket for an organization. The user must be a member of the organization with any role.
719
//        @Tags                        organizations
720
//        @Accept                        json
721
//        @Produce                json
722
//        @Security                BearerAuth
723
//        @Param                        address        path                string                                                                                true        "Organization address"
724
//        @Param                        request        body                apicommon.CreateOrganizationTicketRequest        true        "Ticket request information"
725
//        @Success                200                {string}        string                                                                                "OK"
726
//        @Failure                400                {object}        errors.Error                                                                "Invalid input data"
727
//        @Failure                401                {object}        errors.Error                                                                "Unauthorized"
728
//        @Failure                404                {object}        errors.Error                                                                "Organization not found"
729
//        @Failure                500                {object}        errors.Error                                                                "Internal server error"
730
//        @Router                        /organizations/{address}/tickets [post]
NEW
731
func (a *API) organizationCreateTicket(w http.ResponseWriter, r *http.Request) {
×
NEW
732
        // get the user from the request context
×
NEW
733
        user, ok := apicommon.UserFromContext(r.Context())
×
NEW
734
        if !ok {
×
NEW
735
                errors.ErrUnauthorized.Write(w)
×
NEW
736
                return
×
NEW
737
        }
×
738
        // get the organization info from the request context
NEW
739
        org, _, ok := a.organizationFromRequest(r)
×
NEW
740
        if !ok {
×
NEW
741
                errors.ErrNoOrganizationProvided.Write(w)
×
NEW
742
                return
×
NEW
743
        }
×
NEW
744
        if !user.HasRoleFor(org.Address, db.AnyRole) {
×
NEW
745
                errors.ErrUnauthorized.Withf("user is not a member of organization").Write(w)
×
NEW
746
                return
×
NEW
747
        }
×
748
        // get the ticket request from the request body
NEW
749
        ticketReq := &apicommon.CreateOrganizationTicketRequest{}
×
NEW
750
        if err := json.NewDecoder(r.Body).Decode(ticketReq); err != nil {
×
NEW
751
                errors.ErrMalformedBody.Write(w)
×
NEW
752
                return
×
NEW
753
        }
×
754
        // validate the ticket request
NEW
755
        if ticketReq.Title == "" || ticketReq.Description == "" {
×
NEW
756
                errors.ErrMalformedBody.With("title and description are required").Write(w)
×
NEW
757
                return
×
NEW
758
        }
×
759
        // send an email to the support destination
NEW
760
        if err := a.sendMail(r.Context(), apicommon.SupportEmail, mailtemplates.SupportNotification,
×
NEW
761
                struct {
×
NEW
762
                        Type         string
×
NEW
763
                        Organization string
×
NEW
764
                        Title        string
×
NEW
765
                        Description  string
×
NEW
766
                        Email        string
×
NEW
767
                }{ticketReq.TicketType, org.Address, ticketReq.Title, ticketReq.Description, user.Email},
×
NEW
768
        ); err != nil {
×
NEW
769
                log.Warnw("could not send ticket notification email", "error", err)
×
NEW
770
                errors.ErrGenericInternalServerError.Write(w)
×
NEW
771
                return
×
NEW
772
        }
×
NEW
773
        apicommon.HTTPWriteOK(w)
×
774
}
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