Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements subscription limitations #40

Merged
merged 3 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions api/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/internal"
"github.com/vocdoni/saas-backend/notifications/mailtemplates"
"github.com/vocdoni/saas-backend/subscriptions"
"go.vocdoni.io/dvote/log"
)

Expand Down Expand Up @@ -43,6 +44,13 @@ func (a *API) createOrganizationHandler(w http.ResponseWriter, r *http.Request)
parentOrg := ""
var dbParentOrg *db.Organization
if orgInfo.Parent != nil {
// check if the org has permission to create suborganizations
hasPermission, err := a.subscriptions.HasDBPersmission(user.Email, orgInfo.Parent.Address, subscriptions.CreateSubOrg)
if !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to create suborganizations: %v", err).Write(w)
return
}

dbParentOrg, _, err = a.db.Organization(orgInfo.Parent.Address, false)
if err != nil {
if err == db.ErrNotFound {
Expand Down Expand Up @@ -107,6 +115,15 @@ func (a *API) createOrganizationHandler(w http.ResponseWriter, r *http.Request)
ErrGenericInternalServerError.Write(w)
return
}

// update the parent organization counter
if orgInfo.Parent != nil {
dbParentOrg.Counters.SubOrgs++
if err := a.db.SetOrganization(dbParentOrg); err != nil {
ErrGenericInternalServerError.Withf("could not update parent organization: %v", err).Write(w)
return
}
}
// send the organization back to the user
httpWriteJSON(w, organizationFromDB(dbOrg, dbParentOrg))
}
Expand Down Expand Up @@ -260,8 +277,11 @@ func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Req
ErrNoOrganizationProvided.Write(w)
return
}
if !user.HasRoleFor(org.Address, db.AdminRole) {
ErrUnauthorized.Withf("user is not admin of organization").Write(w)

// check if the user/org has permission to invite members
hasPermission, err := a.subscriptions.HasDBPersmission(user.Email, org.Address, subscriptions.InviteMember)
if !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
// get new admin info from the request body
Expand Down Expand Up @@ -315,6 +335,13 @@ func (a *API) inviteOrganizationMemberHandler(w http.ResponseWriter, r *http.Req
ErrGenericInternalServerError.Write(w)
return
}

// update the org members counter
org.Counters.Members++
if err := a.db.SetOrganization(org); err != nil {
ErrGenericInternalServerError.Withf("could not update organization: %v", err).Write(w)
return
}
httpWriteOK(w)
}

Expand Down
52 changes: 46 additions & 6 deletions api/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrMalformedBody.Withf("could not decode request body: %v", err).Write(w)
return
}
// check if the user has the admin role for the organization
if !user.HasRoleFor(signReq.Address, db.AdminRole) {
ErrUnauthorized.With("user does not have admin role").Write(w)
// check if the user is a member of the organization
if !user.HasRoleFor(signReq.Address, db.AnyRole) {
ErrUnauthorized.With("user is not an organization member").Write(w)
return
}
// get the organization info from the database with the address provided in
Expand Down Expand Up @@ -67,6 +67,9 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrInvalidTxFormat.Write(w)
return
}
// flag to know if the TX is New Process
isNewProcess := false

// check if the api is not in transparent mode
if !a.transparentMode {
// get subscription plan
Expand All @@ -83,6 +86,10 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrUnauthorized.With("invalid account").Write(w)
return
}
if hasPermission, err := a.subscriptions.HasTxPermission(tx, txSetAccount.Txtype, org, user); !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
// check the tx subtype
switch txSetAccount.Txtype {
case models.TxType_CREATE_ACCOUNT:
Expand All @@ -107,6 +114,29 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
},
}
}
case models.TxType_SET_ACCOUNT_INFO_URI:
// generate a new faucet package if it's not present and include it in the tx
if txSetAccount.FaucetPackage == nil {
// get the tx cost for the tx type
amount, ok := a.account.TxCosts[models.TxType_SET_ACCOUNT_INFO_URI]
if !ok {
panic("invalid tx type")
}
// generate the faucet package with the calculated amount
faucetPkg, err := a.account.FaucetPackage(organizationSigner.AddressString(), amount)
if err != nil {
ErrCouldNotCreateFaucetPackage.WithErr(err).Write(w)
return
}
// include the faucet package in the tx
txSetAccount.FaucetPackage = faucetPkg
tx = &models.Tx{
Payload: &models.Tx_SetAccount{
SetAccount: txSetAccount,
},
}
}

}
case *models.Tx_NewProcess:
txNewProcess := tx.GetNewProcess()
Expand All @@ -116,14 +146,14 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrInvalidTxFormat.With("missing fields").Write(w)
return
}
if hasPermission, err := a.subscriptions.HasPermission(tx, txNewProcess.Txtype, org); !hasPermission || err != nil {
if hasPermission, err := a.subscriptions.HasTxPermission(tx, txNewProcess.Txtype, org, user); !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
// check the tx subtype
switch txNewProcess.Txtype {
case models.TxType_NEW_PROCESS:

isNewProcess = true
// generate a new faucet package if it's not present and include it in the tx
if txNewProcess.FaucetPackage == nil {
// get the tx cost for the tx type
Expand Down Expand Up @@ -169,7 +199,7 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrInvalidTxFormat.With("invalid tx type").Write(w)
return
}
if hasPermission, err := a.subscriptions.HasPermission(tx, txSetProcess.Txtype, org); !hasPermission || err != nil {
if hasPermission, err := a.subscriptions.HasTxPermission(tx, txSetProcess.Txtype, org, user); !hasPermission || err != nil {
ErrUnauthorized.Withf("user does not have permission to sign transactions: %v", err).Write(w)
return
}
Expand Down Expand Up @@ -315,6 +345,16 @@ func (a *API) signTxHandler(w http.ResponseWriter, r *http.Request) {
ErrCouldNotSignTransaction.WithErr(err).Write(w)
return
}

// If isNewProcess and everything went well so far update the organization process counter
if isNewProcess {
org.Counters.Processes++
if err := a.db.SetOrganization(org); err != nil {
ErrGenericInternalServerError.Withf("could not update organization process counter: %v", err).Write(w)
return
}
}

// return the signed tx payload
httpWriteJSON(w, &TransactionData{
TxPayload: base64.StdEncoding.EncodeToString(stx),
Expand Down
4 changes: 4 additions & 0 deletions db/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const (
AdminRole UserRole = "admin"
ManagerRole UserRole = "manager"
ViewerRole UserRole = "viewer"
AnyRole UserRole = "any"
// organization types
AssemblyType OrganizationType = "assembly"
AssociationType OrganizationType = "association"
Expand All @@ -31,13 +32,15 @@ var writableRoles = map[UserRole]bool{
AdminRole: true,
ManagerRole: true,
ViewerRole: false,
AnyRole: false,
}

// UserRoleNames is a map that contains the user role names by role
var UserRolesNames = map[UserRole]string{
AdminRole: "Admin",
ManagerRole: "Manager",
ViewerRole: "Viewer",
AnyRole: "Any",
}

// HasWriteAccess function checks if the user role has write access
Expand Down Expand Up @@ -81,6 +84,7 @@ var validRoles = map[UserRole]bool{
AdminRole: true,
ManagerRole: true,
ViewerRole: true,
AnyRole: false,
}

// IsValidUserRole function checks if the user role is valid
Expand Down
21 changes: 12 additions & 9 deletions db/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ type UserVerification struct {

func (u *User) HasRoleFor(address string, role UserRole) bool {
for _, org := range u.Organizations {
if org.Address == address && string(org.Role) == string(role) {
if org.Address == address &&
// Check if the role is "any: or if the role matches the organization role
(string(role) == string(AnyRole) || string(org.Role) == string(role)) {
return true
}
}
Expand Down Expand Up @@ -63,14 +65,14 @@ type Organization struct {
}

type PlanLimits struct {
Members int `json:"members" bson:"members"`
SubOrgs int `json:"subOrgs" bson:"subOrgs"`
CensusSize int `json:"censusSize" bson:"censusSize"`
MaxProcesses int `json:"maxProcesses" bson:"maxProcesses"`
MaxCensus int `json:"maxCensus" bson:"maxCensus"`
MaxDuration string `json:"maxDuration" bson:"maxDuration"`
CustomURL bool `json:"customURL" bson:"customURL"`
Drafts int `json:"drafts" bson:"drafts"`
Members int `json:"members" bson:"members"`
SubOrgs int `json:"subOrgs" bson:"subOrgs"`
MaxProcesses int `json:"maxProcesses" bson:"maxProcesses"`
MaxCensus int `json:"maxCensus" bson:"maxCensus"`
// Max process duration in days
MaxDuration string `json:"maxDuration" bson:"maxDuration"`
CustomURL bool `json:"customURL" bson:"customURL"`
Drafts int `json:"drafts" bson:"drafts"`
}

type VotingTypes struct {
Expand Down Expand Up @@ -126,6 +128,7 @@ type OrganizationCounters struct {
SentEmails int `json:"sentEmails" bson:"sentEmails"`
SubOrgs int `json:"subOrgs" bson:"subOrgs"`
Members int `json:"members" bson:"members"`
Processes int `json:"processes" bson:"processes"`
}

type OrganizationInvite struct {
Expand Down
Loading
Loading