Add support for subscriptions
This commit is contained in:
parent
92eadbba99
commit
9f090f355a
@ -310,6 +310,20 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Subscription",
|
||||
"path": "/subscription",
|
||||
"entrypoint": "subscription",
|
||||
"apis": [
|
||||
{
|
||||
"name": "current",
|
||||
"method": "GET",
|
||||
"title": "Returns current subscription status",
|
||||
"path": "/",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Organisations",
|
||||
"description": "Organisations represent a top-level grouping entity. There may be many organisations defined in a single deployment.",
|
||||
|
||||
18
api/system/spec/subscription.json
Normal file
18
api/system/spec/subscription.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Title": "Subscription",
|
||||
"Interface": "Subscription",
|
||||
"Struct": null,
|
||||
"Parameters": null,
|
||||
"Protocol": "",
|
||||
"Authentication": null,
|
||||
"Path": "/subscription",
|
||||
"APIs": [
|
||||
{
|
||||
"Name": "current",
|
||||
"Method": "GET",
|
||||
"Title": "Returns current subscription status",
|
||||
"Path": "/",
|
||||
"Parameters": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1078,6 +1078,32 @@ An organisation may have many roles. Roles may have many channels available. Acc
|
||||
|
||||
|
||||
|
||||
# Subscription
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
| ------ | -------- | ------- |
|
||||
| `GET` | `/subscription/` | Returns current subscription status |
|
||||
|
||||
## Returns current subscription status
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/subscription/` | HTTP/S | GET |
|
||||
Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.crust/corteza-server/codegen/templates/README.tpl on line 32
|
||||
|
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
# Users
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
|
||||
@ -75,7 +75,14 @@ func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{
|
||||
}
|
||||
|
||||
func (ctrl *Auth) Settings(ctx context.Context, r *request.AuthSettings) (interface{}, error) {
|
||||
return ctrl.authSettings.Format(), nil
|
||||
f := ctrl.authSettings.Format()
|
||||
|
||||
if err := ctrl.authSvc.With(ctx).CanRegister(); err != nil {
|
||||
// f["internalSignUpEnabled"] = false
|
||||
f["signUpDisabled"] = err.Error()
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (ctrl *Auth) ExchangeAuthToken(ctx context.Context, r *request.AuthExchangeAuthToken) (interface{}, error) {
|
||||
|
||||
70
system/rest/handlers/subscription.go
Normal file
70
system/rest/handlers/subscription.go
Normal file
@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
/*
|
||||
Hello! This file is auto-generated from `docs/src/spec.json`.
|
||||
|
||||
For development:
|
||||
In order to update the generated files, edit this file under the location,
|
||||
add your struct fields, imports, API definitions and whatever you want, and:
|
||||
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `subscription.go`, `subscription.util.go` or `subscription_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `subscription.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/system/rest/request"
|
||||
)
|
||||
|
||||
// Internal API interface
|
||||
type SubscriptionAPI interface {
|
||||
Current(context.Context, *request.SubscriptionCurrent) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Subscription struct {
|
||||
Current func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewSubscription(h SubscriptionAPI) *Subscription {
|
||||
return &Subscription{
|
||||
Current: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewSubscriptionCurrent()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Subscription.Current", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Current(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Subscription.Current", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Subscription.Current", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h Subscription) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Get("/subscription/", h.Current)
|
||||
})
|
||||
}
|
||||
77
system/rest/request/subscription.go
Normal file
77
system/rest/request/subscription.go
Normal file
@ -0,0 +1,77 @@
|
||||
package request
|
||||
|
||||
/*
|
||||
Hello! This file is auto-generated from `docs/src/spec.json`.
|
||||
|
||||
For development:
|
||||
In order to update the generated files, edit this file under the location,
|
||||
add your struct fields, imports, API definitions and whatever you want, and:
|
||||
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `subscription.go`, `subscription.util.go` or `subscription_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `subscription.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Subscription current request parameters
|
||||
type SubscriptionCurrent struct {
|
||||
}
|
||||
|
||||
func NewSubscriptionCurrent() *SubscriptionCurrent {
|
||||
return &SubscriptionCurrent{}
|
||||
}
|
||||
|
||||
func (r SubscriptionCurrent) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *SubscriptionCurrent) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewSubscriptionCurrent()
|
||||
@ -27,6 +27,7 @@ func MountRoutes(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.MiddlewareValidOnly)
|
||||
|
||||
handlers.NewSubscription(Subscription{}.New()).MountRoutes(r)
|
||||
handlers.NewUser(User{}.New()).MountRoutes(r)
|
||||
handlers.NewRole(Role{}.New()).MountRoutes(r)
|
||||
handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r)
|
||||
|
||||
50
system/rest/subscription.go
Normal file
50
system/rest/subscription.go
Normal file
@ -0,0 +1,50 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/system/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/system/service"
|
||||
)
|
||||
|
||||
type Subscription struct{}
|
||||
|
||||
func (Subscription) New() *Subscription {
|
||||
return &Subscription{}
|
||||
}
|
||||
|
||||
func (ctrl *Subscription) Current(ctx context.Context, r *request.SubscriptionCurrent) (interface{}, error) {
|
||||
if service.CurrentSubscription == nil {
|
||||
// Nothing to do here
|
||||
return resputil.OK(), nil
|
||||
}
|
||||
|
||||
// Returning function that gets called with writter & request
|
||||
//
|
||||
// This is the only way to get to the request URL we need to do a domain check
|
||||
// for the permit
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
var (
|
||||
domain = req.Host
|
||||
pos = strings.IndexByte(domain, ':')
|
||||
|
||||
// Anyone that has access permissions is considered admin
|
||||
isAdmin = service.DefaultAccessControl.CanAccess(ctx)
|
||||
)
|
||||
|
||||
if pos > -1 {
|
||||
// Strip port
|
||||
domain = domain[:pos]
|
||||
}
|
||||
|
||||
if err := service.CurrentSubscription.Validate(domain, isAdmin); err != nil {
|
||||
resputil.JSON(w, err)
|
||||
} else {
|
||||
resputil.JSON(w, resputil.OK())
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
@ -26,6 +26,7 @@ type (
|
||||
ctx context.Context
|
||||
logger *zap.Logger
|
||||
|
||||
subscription authSubscriptionChecker
|
||||
credentials repository.CredentialsRepository
|
||||
users repository.UserRepository
|
||||
roles repository.RoleRepository
|
||||
@ -55,11 +56,17 @@ type (
|
||||
SendEmailAddressConfirmationToken(email string) (err error)
|
||||
SendPasswordResetToken(email string) (err error)
|
||||
|
||||
CanRegister() error
|
||||
|
||||
LoadRoleMemberships(*types.User) error
|
||||
|
||||
checkPasswordStrength(string) error
|
||||
changePassword(uint64, string) error
|
||||
}
|
||||
|
||||
authSubscriptionChecker interface {
|
||||
CanRegister(uint) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
@ -98,6 +105,7 @@ func (svc auth) With(ctx context.Context) AuthService {
|
||||
users: repository.User(ctx, db),
|
||||
roles: repository.Role(ctx, db),
|
||||
|
||||
subscription: CurrentSubscription,
|
||||
settings: DefaultAuthSettings,
|
||||
notifications: DefaultAuthNotification,
|
||||
|
||||
@ -205,6 +213,10 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
Handle: profile.NickName,
|
||||
}
|
||||
|
||||
if err = svc.CanRegister(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u, err = svc.users.Create(u); err != nil {
|
||||
return errors.Wrap(err, "could not create user after successful external authentication")
|
||||
}
|
||||
@ -316,6 +328,10 @@ func (svc auth) InternalSignUp(input *types.User, password string) (u *types.Use
|
||||
return nil, errors.Wrap(err, "could not check existing emails")
|
||||
}
|
||||
|
||||
if err = svc.CanRegister(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Whitelisted user data to copy
|
||||
u, err = svc.users.Create(&types.User{
|
||||
Email: input.Email,
|
||||
@ -699,6 +715,18 @@ func (svc auth) SendPasswordResetToken(email string) error {
|
||||
return svc.sendPasswordResetToken(u)
|
||||
}
|
||||
|
||||
func (svc auth) CanRegister() error {
|
||||
|
||||
if svc.subscription != nil {
|
||||
// When we have an active subscription, we need to check
|
||||
// if users can register or did this deployment hit
|
||||
// it's user-limit
|
||||
return svc.subscription.CanRegister(svc.users.Total())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc auth) sendPasswordResetToken(u *types.User) (err error) {
|
||||
log := svc.log(svc.ctx, zap.Uint64("userID", u.ID), zap.String("email", u.Email))
|
||||
|
||||
|
||||
@ -36,11 +36,28 @@ type (
|
||||
Corredor options.CorredorOpt
|
||||
GRPCClientSystem options.GRPCServerOpt
|
||||
}
|
||||
|
||||
permitChecker interface {
|
||||
Validate(string, bool) error
|
||||
CanCreateUser(uint) error
|
||||
CanRegister(uint) error
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultLogger *zap.Logger
|
||||
|
||||
// CurrentSubscription holds current subscription info,
|
||||
// and functions for domain validation, user limit checks and
|
||||
// warning texts
|
||||
//
|
||||
// By default, Corteza (community edition) has this set to nil
|
||||
// and with that all checks & validations are skipped
|
||||
//
|
||||
// Other flavours or distributions can set this to
|
||||
// something that suits their needs.
|
||||
CurrentSubscription permitChecker
|
||||
|
||||
// DefaultPermissions Retrieves & stores permissions
|
||||
DefaultPermissions permissionServicer
|
||||
|
||||
@ -80,7 +97,6 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) {
|
||||
DefaultLogger = log.Named("service")
|
||||
|
||||
DefaultIntSettings = internalSettings.NewService(internalSettings.NewRepository(repository.DB(ctx), "sys_settings"))
|
||||
|
||||
if DefaultPermissions == nil {
|
||||
DefaultPermissions = permissions.Service(ctx, DefaultLogger, repository.DB(ctx), "sys_permission_rules")
|
||||
}
|
||||
|
||||
@ -35,7 +35,8 @@ type (
|
||||
|
||||
settings *AuthSettings
|
||||
|
||||
auth userAuth
|
||||
auth userAuth
|
||||
subscription userSubscriptionChecker
|
||||
|
||||
ac userAccessController
|
||||
user repository.UserRepository
|
||||
@ -52,6 +53,10 @@ type (
|
||||
changePassword(uint64, string) error
|
||||
}
|
||||
|
||||
userSubscriptionChecker interface {
|
||||
CanCreateUser(uint) error
|
||||
}
|
||||
|
||||
userAccessController interface {
|
||||
CanAccess(context.Context) bool
|
||||
CanCreateUser(context.Context) bool
|
||||
@ -113,6 +118,8 @@ func (svc user) With(ctx context.Context) UserService {
|
||||
settings: DefaultAuthSettings,
|
||||
auth: DefaultAuth,
|
||||
|
||||
subscription: CurrentSubscription,
|
||||
|
||||
user: repository.User(ctx, db),
|
||||
credentials: repository.Credentials(ctx, db),
|
||||
|
||||
@ -199,6 +206,16 @@ func (svc user) Create(input *types.User) (out *types.User, err error) {
|
||||
return nil, ErrNoCreatePermissions.withStack()
|
||||
}
|
||||
|
||||
if svc.subscription != nil {
|
||||
// When we have an active subscription, we need to check
|
||||
// if users can be creare or did this deployment hit
|
||||
// it's user-limit
|
||||
err = svc.subscription.CanCreateUser(svc.user.Total())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return out, svc.db.Transaction(func() (err error) {
|
||||
if err = svc.UniqueCheck(input); err != nil {
|
||||
return
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user