Skip to content

Commit

Permalink
Merge branch 'main' into f/email_templates
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmenendez committed Oct 1, 2024
2 parents 414cf10 + bf16dd3 commit 361bb38
Show file tree
Hide file tree
Showing 31 changed files with 1,007 additions and 299 deletions.
19 changes: 17 additions & 2 deletions account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import (
"go.vocdoni.io/dvote/crypto/ethereum"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/dvote/vochain"
"go.vocdoni.io/dvote/vochain/state/electionprice"
"go.vocdoni.io/proto/build/go/models"
)

// Account handles the account operations that include signing transactions, creating faucet packages, etc.
type Account struct {
client *apiclient.HTTPclient
signer *ethereum.SignKeys

TxCosts map[models.TxType]uint64
ElectionPriceCalc *electionprice.Calculator
}

// New creates a new account with the given private key and API endpoint.
Expand Down Expand Up @@ -50,9 +54,20 @@ func New(privateKey string, apiEndpoint string) (*Account, error) {
"address", account.Address,
"balance", account.Balance,
)
// initialize the election price calculator
electionPriceCalc, err := InitElectionPriceCalculator(apiEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to initialize election price calculator: %w", err)
}
txCosts, err := vochainTxCosts(apiEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to get transaction costs: %w", err)
}
return &Account{
client: apiClient,
signer: &signer,
client: apiClient,
signer: &signer,
TxCosts: txCosts,
ElectionPriceCalc: electionPriceCalc,
}, nil
}

Expand Down
102 changes: 102 additions & 0 deletions account/price.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package account

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"go.vocdoni.io/dvote/api"
"go.vocdoni.io/dvote/vochain/genesis"
"go.vocdoni.io/dvote/vochain/state/electionprice"
"go.vocdoni.io/proto/build/go/models"
)

const (
// electionPriceEndpoint is the endpoint to get the election price factors
// from the Vochain.
electionPriceEndpoint = "/chain/info/electionPriceFactors"
// txCostsEndpoint is the endpoint to get the transaction costs from the
// Vochain.
txCostsEndpoint = "/chain/transactions/cost"
)

// InitElectionPriceCalculator initializes the election price calculator with
// the factors from the Vochain. It returns the election price calculator or an
// error if it fails to get the factors.
func InitElectionPriceCalculator(vochainURI string) (*electionprice.Calculator, error) {
basePrice, capacity, factors, err := electionPriceFactors(vochainURI)
if err != nil {
return nil, fmt.Errorf("failed to get election price factors: %w", err)
}
electionPriceCalc := electionprice.NewElectionPriceCalculator(factors)
electionPriceCalc.SetBasePrice(basePrice)
electionPriceCalc.SetCapacity(capacity)
return electionPriceCalc, nil
}

// ElectionPriceFactors returns the election price factors from the Vochain. It
// returns the base price, capacity, and factors. If there is an error, it
// returns the error.
func electionPriceFactors(vochainURI string) (uint64, uint64, electionprice.Factors, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// create the request to get the election price factors
url := vochainURI + electionPriceEndpoint
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return 0, 0, electionprice.Factors{}, fmt.Errorf("failed to create request: %w", err)
}
// send the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, 0, electionprice.Factors{}, fmt.Errorf("failed to send request: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
// parse the response
if resp.StatusCode != http.StatusOK {
return 0, 0, electionprice.Factors{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var data electionprice.Calculator
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return 0, 0, electionprice.Factors{}, fmt.Errorf("failed to decode response: %w", err)
}
return data.BasePrice, data.Capacity, data.Factors, nil
}

// vochainTxCosts returns the transaction costs from the Vochain. It returns the
// transaction costs or an error if it fails to get them.
func vochainTxCosts(vochainURI string) (map[models.TxType]uint64, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// create the request to get the transactions costs
url := vochainURI + txCostsEndpoint
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// send the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
// parse the response
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var strTxCosts api.Transaction
if err := json.NewDecoder(resp.Body).Decode(&strTxCosts); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
txCosts := make(map[models.TxType]uint64)
for strType, cost := range strTxCosts.Costs {
txCosts[genesis.TxCostNameToTxType(strType)] = cost
}
return txCosts, nil
}
6 changes: 6 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ func (a *API) initRouter() http.Handler {
// verify user
log.Infow("new route", "method", "POST", "path", verifyUserEndpoint)
r.Post(verifyUserEndpoint, a.verifyUserAccountHandler)
// get user verification code information
log.Infow("new route", "method", "GET", "path", verifyUserCodeEndpoint)
r.Get(verifyUserCodeEndpoint, a.userVerificationCodeInfoHandler)
// resend user verification code
log.Infow("new route", "method", "POST", "path", verifyUserCodeEndpoint)
r.Post(verifyUserCodeEndpoint, a.resendUserVerificationCodeHandler)
// request user password recovery
log.Infow("new route", "method", "POST", "path", usersRecoveryPasswordEndpoint)
r.Post(usersRecoveryPasswordEndpoint, a.recoverUserPasswordHandler)
Expand Down
14 changes: 7 additions & 7 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

"github.com/vocdoni/saas-backend/account"
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/notifications/testmail"
"github.com/vocdoni/saas-backend/notifications/smtp"
"github.com/vocdoni/saas-backend/test"
"go.vocdoni.io/dvote/apiclient"
)
Expand Down Expand Up @@ -44,7 +44,7 @@ var testDB *db.MongoStorage

// testMailService is the test mail service for the tests. Make it global so it
// can be accessed by the tests directly.
var testMailService *testmail.TestMail
var testMailService *smtp.SMTPEmail

// testURL helper function returns the full URL for the given path using the
// test host and port.
Expand Down Expand Up @@ -161,14 +161,14 @@ func TestMain(m *testing.M) {
panic(err)
}
// create test mail service
testMailService = new(testmail.TestMail)
if err := testMailService.Init(&testmail.TestMailConfig{
testMailService = new(smtp.SMTPEmail)
if err := testMailService.New(&smtp.SMTPConfig{
FromAddress: adminEmail,
SMTPUser: adminUser,
SMTPUsername: adminUser,
SMTPPassword: adminPass,
Host: mailHost,
SMTPServer: mailHost,
SMTPPort: smtpPort.Int(),
APIPort: apiPort.Int(),
TestAPIPort: apiPort.Int(),
}); err != nil {
panic(err)
}
Expand Down
3 changes: 2 additions & 1 deletion api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ func (a *API) authLoginHandler(w http.ResponseWriter, r *http.Request) {
}
// check if the user is verified
if !user.Verified {
ErrUnauthorized.Withf("user not verified").Write(w)
ErrUserNoVerified.Write(w)
return
}
// generate a new token with the user name as the subject
res, err := a.buildLoginResponse(loginInfo.Email)
Expand Down
10 changes: 9 additions & 1 deletion api/const.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package api

import "github.com/vocdoni/saas-backend/notifications"
import (
"time"

"github.com/vocdoni/saas-backend/notifications"
)

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

const (
// VerificationCodeLength is the length of the verification code in bytes
Expand Down
74 changes: 66 additions & 8 deletions api/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@
- [👥 Users](#-users)
- [🙋 Register](#-register)
- [✅ Verify user](#-verify-user)
- [🪪 User verification code info](#-user-verification-code-info)
- [📤 Resend user verification code](#-resend-user-verification-code)
- [🧑‍💻 Get current user info](#-get-current-user-info)
- [💇 Update current user info](#-update-current-user-info)
- [🔏 Update current user password](#-update-current-user-password)
- [⛓️‍💥 Request a password recovery](#-request-a-password-recovery)
- [⛓️‍💥 Request a password recovery](#%EF%B8%8F-request-a-password-recovery)
- [🔗 Reset user password](#-reset-user-password)
- [🏤 Organizations](#-organizations)
- [🆕 Create organization](#-create-organization)
- [⚙️ Update organization](#-update-organization)
- [🔍 Organization info](#-organization-info)
- [🧑‍🤝‍🧑 Organization members](#-organization-members)

</details>

Expand Down Expand Up @@ -56,6 +59,7 @@
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40004` | `malformed JSON body` |
| `401` | `40014` | `user account not verified` |
| `500` | `50002` | `internal server error` |

### 🥤 Refresh token
Expand Down Expand Up @@ -200,7 +204,7 @@ This endpoint only returns the addresses of the organizations where the current

### ✅ Verify user

* **Path** `/auth/verify`
* **Path** `/users/verify`
* **Method** `POST`
* **Request Body**
```json
Expand All @@ -224,6 +228,57 @@ This endpoint only returns the addresses of the organizations where the current
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40004` | `malformed JSON body` |
| `400` | `40005` | `invalid user data` |
| `400` | `40015` | `user account already verified` |
| `401` | `40016` | `verification code expired` |
| `500` | `50002` | `internal server error` |

### 🪪 User verification code info

* **Path** `/users/verify/code`
* **Method** `GET`
* **Query params**
* `email`

* **Response**
```json
{
"email": "[email protected]",
"expiration": "2024-09-20T09:02:26.849Z",
"valid": true
}
```

* **Errors**

| HTTP Status | Error code | Message |
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40005` | `invalid user data` |
| `400` | `40015` | `user account already verified` |
| `404` | `40018` | `user not found` |
| `500` | `50002` | `internal server error` |

### 📤 Resend user verification code

* **Path** `/users/verify/code`
* **Method** `POST`
* **Request Body**
```json
{
"email": "[email protected]",
}
```

* **Errors**

| HTTP Status | Error code | Message |
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40004` | `malformed JSON body` |
| `400` | `40005` | `invalid user data` |
| `400` | `40015` | `user account already verified` |
| `400` | `40017` | `last verification code still valid` |
| `500` | `50002` | `internal server error` |

### 🧑‍💻 Get current user info
Expand Down Expand Up @@ -340,6 +395,7 @@ This method invalidates any previous JWT token for the user, so it returns a new
|:---:|:---:|:---|
| `401` | `40001` | `user not authorized` |
| `400` | `40004` | `malformed JSON body` |
| `401` | `40014` | `user account not verified` |
| `500` | `50002` | `internal server error` |

### 🔗 Reset user password
Expand Down Expand Up @@ -490,12 +546,14 @@ Only the following parameters can be changed. Every parameter is optional.
* **Method** `GET`
* **Response**
```json
[
{
"info": { /* user info response */ },
"role": "admin"
}
]
{
"members": [
{
"info": { /* user info response */ },
"role": "admin"
}
]
}
```

* **Errors**
Expand Down
2 changes: 2 additions & 0 deletions api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func (e Error) Write(w http.ResponseWriter) {
if log.Level() == log.LogLevelDebug {
log.Debugw("API error response", "error", e.Error(), "code", e.Code, "httpStatus", e.HTTPstatus)
}
// set the content type to JSON
w.Header().Set("Content-Type", "application/json")
http.Error(w, string(msg), e.HTTPstatus)
}

Expand Down
6 changes: 6 additions & 0 deletions api/errors_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ var (
ErrNoOrganizationProvided = Error{Code: 40011, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("no organization provided")}
ErrNoOrganizations = Error{Code: 40012, HTTPstatus: http.StatusNotFound, Err: fmt.Errorf("this user has not been assigned to any organization")}
ErrInvalidOrganizationData = Error{Code: 40013, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("invalid organization data")}
ErrUserNoVerified = Error{Code: 40014, HTTPstatus: http.StatusUnauthorized, Err: fmt.Errorf("user account not verified")}
ErrUserAlreadyVerified = Error{Code: 40015, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("user account already verified")}
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")}

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")}
ErrCouldNotCreateFaucetPackage = Error{Code: 50003, HTTPstatus: http.StatusInternalServerError, Err: fmt.Errorf("could not create faucet package")}
ErrVochainRequestFailed = Error{Code: 50004, HTTPstatus: http.StatusInternalServerError, Err: fmt.Errorf("vochain request failed")}
)
6 changes: 4 additions & 2 deletions api/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ func (a *API) organizationMembersHandler(w http.ResponseWriter, r *http.Request)
ErrGenericInternalServerError.Withf("could not get organization members: %v", err).Write(w)
return
}
orgMembers := []OrganizationMember{}
orgMembers := OrganizationMembers{
Members: make([]*OrganizationMember, 0, len(members)),
}
for _, member := range members {
var role string
for _, userOrg := range member.Organizations {
Expand All @@ -134,7 +136,7 @@ func (a *API) organizationMembersHandler(w http.ResponseWriter, r *http.Request)
if role == "" {
continue
}
orgMembers = append(orgMembers, OrganizationMember{
orgMembers.Members = append(orgMembers.Members, &OrganizationMember{
Info: &UserInfo{
Email: member.Email,
FirstName: member.FirstName,
Expand Down
3 changes: 3 additions & 0 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const (
usersEndpoint = "/users"
// POST /users/verify to verify the user
verifyUserEndpoint = "/users/verify"
// GET /users/verify/code to get the user verification code information
// POST /users/verify/code to try to resend the user verification code
verifyUserCodeEndpoint = "/users/verify/code"
// GET /users/me to get the current user information
// PUT /users/me to update the current user information
usersMeEndpoint = "/users/me"
Expand Down
Loading

0 comments on commit 361bb38

Please sign in to comment.