Skip to content

Commit

Permalink
Merge branch 'main' into f/subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
emmdim committed Nov 7, 2024
2 parents eb0fc73 + e875b70 commit 94df5d6
Show file tree
Hide file tree
Showing 25 changed files with 1,156 additions and 281 deletions.
18 changes: 15 additions & 3 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ type APIConfig struct {
Client *apiclient.HTTPclient
Account *account.Account
MailService notifications.NotificationService
SMSService notifications.NotificationService
// FullTransparentMode if true allows signing all transactions and does not
// modify any of them.
FullTransparentMode bool
Expand All @@ -52,7 +51,6 @@ type API struct {
client *apiclient.HTTPclient
account *account.Account
mail notifications.NotificationService
sms notifications.NotificationService
secret string
transparentMode bool
stripe *stripe.StripeClient
Expand All @@ -72,7 +70,6 @@ func New(conf *APIConfig) *API {
client: conf.Client,
account: conf.Account,
mail: conf.MailService,
sms: conf.SMSService,
secret: conf.Secret,
transparentMode: conf.FullTransparentMode,
stripe: conf.StripeClient,
Expand Down Expand Up @@ -144,6 +141,12 @@ func (a *API) initRouter() http.Handler {
// get organization subscription
log.Infow("new route", "method", "GET", "path", organizationSubscriptionEndpoint)
r.Get(organizationSubscriptionEndpoint, a.getOrganizationSubscriptionHandler)
// invite a new admin member to the organization
log.Infow("new route", "method", "POST", "path", organizationAddMemberEndpoint)
r.Post(organizationAddMemberEndpoint, a.inviteOrganizationMemberHandler)
// pending organization invitations
log.Infow("new route", "method", "GET", "path", organizationPendingMembersEndpoint)
r.Get(organizationPendingMembersEndpoint, a.pendingOrganizationMembersHandler)
})

// Public routes
Expand Down Expand Up @@ -185,6 +188,15 @@ func (a *API) initRouter() http.Handler {
r.Get(subscriptionsEndpoint, a.getSubscriptionsHandler)
log.Infow("new route", "method", "POST", "path", subscriptionsWebhook)
r.Post(subscriptionsWebhook, a.handleWebhook)
// accept organization invitation
log.Infow("new route", "method", "POST", "path", organizationAcceptMemberEndpoint)
r.Post(organizationAcceptMemberEndpoint, a.acceptOrganizationMemberInvitationHandler)
// get organization roles
log.Infow("new route", "method", "GET", "path", organizationRolesEndpoint)
r.Get(organizationRolesEndpoint, a.organizationsMembersRolesHandler)
// get organization types
log.Infow("new route", "method", "GET", "path", organizationTypesEndpoint)
r.Get(organizationTypesEndpoint, a.organizationsTypesHandler)
})
a.router = r
return r
Expand Down
8 changes: 7 additions & 1 deletion api/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "time"

// VerificationCodeExpiration is the duration of the verification code
// before it is invalidated
var VerificationCodeExpiration = 2 * time.Minute
var VerificationCodeExpiration = 3 * time.Minute

const (
// VerificationCodeLength is the length of the verification code in bytes
Expand All @@ -13,4 +13,10 @@ const (
VerificationCodeEmailSubject = "Vocdoni verification code"
// VerificationCodeTextBody is the body of the verification code email
VerificationCodeTextBody = "Your Vocdoni verification code is: "
// InvitationEmailSubject is the subject of the invitation email
InvitationEmailSubject = "Vocdoni organization invitation"
// InvitationTextBody is the body of the invitation email
InvitationTextBody = "You code to join to '%s' organization is: %s"
// InvitationExpiration is the duration of the invitation code before it is invalidated
InvitationExpiration = 5 * 24 * time.Hour // 5 days
)
197 changes: 193 additions & 4 deletions api/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@
- [⚙️ Update organization](#-update-organization)
- [🔍 Organization info](#-organization-info)
- [🧑‍🤝‍🧑 Organization members](#-organization-members)
- [Subscriptions](#subscriptions)
- [Get Available Subscritptions](#get-subscriptions)
- [🧑‍💼 Invite organization member](#-invite-organization-member)
- [⏳ List pending invitations](#-list-pending-invitations)
- [🤝 Accept organization invitation](#-accept-organization-invitation)
- [🤠 Available organization members roles](#-available-organization-members-roles)
- [🏛️ Available organization types](#-available-organization-types)
- [💳 Subscriptions](#-subscriptions)
- [Get Available Subscritptions](#-get-subscriptions)

</details>

Expand Down Expand Up @@ -202,6 +207,7 @@ This endpoint only returns the addresses of the organizations where the current
| `400` | `40002` | `email malformed` |
| `400` | `40003` | `password too short` |
| `400` | `40004` | `malformed JSON body` |
| `409` | `40901` | `duplicate conflict` |
| `500` | `50002` | `internal server error` |

### ✅ Verify user
Expand Down Expand Up @@ -567,8 +573,191 @@ Only the following parameters can be changed. Every parameter is optional.
| `400` | `4012` | `no organization provided` |
| `500` | `50002` | `internal server error` |

### 🧑‍💼 Invite organization member

## Subscriptions
* **Path** `/organizations/{address}/members`
* **Method** `POST`
* **Headers**
* `Authentication: Bearer <user_token>`
* **Request**
```json
{
"role": "admin",
"email": "[email protected]"
}
```

* **Errors**

| HTTP Status | Error code | Message |
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40002` | `email malformed` |
| `400` | `40004` | `malformed JSON body` |
| `400` | `40005` | `invalid user data` |
| `400` | `40009` | `organization not found` |
| `400` | `40011` | `no organization provided` |
| `401` | `40014` | `user account not verified` |
| `400` | `40019` | `inviation code expired` |
| `409` | `40901` | `duplicate conflict` |
| `500` | `50002` | `internal server error` |

### ⏳ List pending invitations

* **Path** `/organizations/{address}/members/pending`
* **Method** `GET`
* **Headers**
* `Authentication: Bearer <user_token>`
* **Response**
```json
{
"pending": [
{
"email": "[email protected]",
"role": "admin",
"expiration": "2024-12-12T12:00:00.000Z"
}
]
}
```

* **Errors**

| HTTP Status | Error code | Message |
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40009` | `organization not found` |
| `400` | `40011` | `no organization provided` |
| `401` | `40014` | `user account not verified` |
| `500` | `50002` | `internal server error` |

### 🤝 Accept organization invitation

* **Path** `/organizations/{address}/members/accept`
* **Method** `POST`
* **Request**
```json
{
"code": "a3f3b5",
"user": { // only if the invited user is not already registered
"email": "[email protected]",
"firstName": "Steve",
"lastName": "Urkel",
"password": "secretpass1234"
}
}
```
`user` object is only required if invited user is not registered yet.

* **Errors**

| HTTP Status | Error code | Message |
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40002` | `email malformed` |
| `400` | `40004` | `malformed JSON body` |
| `400` | `40005` | `invalid user data` |
| `400` | `40009` | `organization not found` |
| `400` | `40011` | `no organization provided` |
| `401` | `40014` | `user account not verified` |
| `400` | `40019` | `inviation code expired` |
| `409` | `40901` | `duplicate conflict` |
| `500` | `50002` | `internal server error` |

### 🤠 Available organization members roles
* **Path** `/organizations/roles`
* **Method** `GET`
* **Response**
```json
{
"roles": [
{
"role": "manager",
"name": "Manager",
"writePermission": true
},
{
"role": "viewer",
"name": "Viewer",
"writePermission": false
},
{
"role": "admin",
"name": "Admin",
"writePermission": true
}
]
}
```

### 🏛️ Available organization types
* **Path** `/organizations/types`
* **Method** `GET`
* **Response**
```json
{
"types": [
{
"type": "cooperative",
"name": "Cooperative"
},
{
"type": "educational",
"name": "University / Educational Institution"
},
{
"type": "others",
"name": "Others"
},
{
"type": "assembly",
"name": "Assembly"
},
{
"type": "religious",
"name": "Church / Religious Organization"
},
{
"type": "company",
"name": "Company / Corporation"
},
{
"type": "political_party",
"name": "Political Party"
},
{
"type": "chamber",
"name": "Chamber"
},
{
"type": "nonprofit",
"name": "Nonprofit / NGO"
},
{
"type": "community",
"name": "Community Group"
},
{
"type": "professional_college",
"name": "Professional College"
},
{
"type": "association",
"name": "Association"
},
{
"type": "city",
"name": "City / Municipality"
},
{
"type": "union",
"name": "Union"
}
]
}
```

## 💳 Subscriptions


### Get Subscriptions
Expand All @@ -587,4 +776,4 @@ Only the following parameters can be changed. Every parameter is optional.

| HTTP Status | Error code | Message |
|:---:|:---:|:---|
| `500` | `50002` | `internal server error` |
| `500` | `50002` | `internal server error` |
7 changes: 4 additions & 3 deletions api/errors_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ var (
ErrVerificationCodeExpired = Error{Code: 40016, HTTPstatus: http.StatusUnauthorized, Err: fmt.Errorf("verification code expired")}
ErrVerificationCodeValid = Error{Code: 40017, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("last verification code still valid")}
ErrUserNotFound = Error{Code: 40018, HTTPstatus: http.StatusNotFound, Err: fmt.Errorf("user not found")}
ErrNoOrganizationSubscription = Error{Code: 40019, HTTPstatus: http.StatusNotFound, Err: fmt.Errorf("organization subscription not found")}
ErrOganizationSubscriptionIncative = Error{Code: 40020, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("organization subscription not active")}
ErrNoDefaultPLan = Error{Code: 40021, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("did not found default plan for organization")}
ErrInvitationExpired = Error{Code: 40019, HTTPstatus: http.StatusUnauthorized, Err: fmt.Errorf("inviation code expired")}
ErrNoOrganizationSubscription = Error{Code: 40020, HTTPstatus: http.StatusNotFound, Err: fmt.Errorf("organization subscription not found")}
ErrOganizationSubscriptionIncative = Error{Code: 40021, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("organization subscription not active")}
ErrNoDefaultPLan = Error{Code: 40022, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("did not found default plan for organization")}

ErrMarshalingServerJSONFailed = Error{Code: 50001, HTTPstatus: http.StatusInternalServerError, Err: fmt.Errorf("marshaling (server-side) JSON failed")}
ErrGenericInternalServerError = Error{Code: 50002, HTTPstatus: http.StatusInternalServerError, Err: fmt.Errorf("internal server error")}
Expand Down
5 changes: 5 additions & 0 deletions api/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func (a *API) authenticator(next http.Handler) http.Handler {
ErrGenericInternalServerError.Withf("could not retrieve user from database: %v", err).Write(w)
return
}
// check if the user is already verified
if !user.Verified {
ErrUserNoVerified.With("user account not verified").Write(w)
return
}
// add the user to the context
ctx := context.WithValue(r.Context(), userMetadataKey, *user)
// token is authenticated, pass it through with the new context with the
Expand Down
Loading

0 comments on commit 94df5d6

Please sign in to comment.