3
0

Add support for subscriptions

This commit is contained in:
Denis Arh 2019-10-28 19:03:23 +01:00
parent 92eadbba99
commit 9f090f355a
11 changed files with 327 additions and 3 deletions

View File

@ -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.",

View 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": {}
}
]
}

View File

@ -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 |

View File

@ -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) {

View 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)
})
}

View 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()

View File

@ -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)

View 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
}

View File

@ -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))

View File

@ -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")
}

View File

@ -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