1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-18 20:59:43 +02:00

PRovide basic in-app purchase/renewal flow

This commit is contained in:
Harvey Kandola 2018-11-05 19:48:50 +00:00
parent e116d3b000
commit d1b803b246
39 changed files with 1211 additions and 1154 deletions

View file

@ -58,9 +58,9 @@ Space view.
## Latest version
[Community edition: v1.72.1](https://github.com/documize/community/releases)
[Community edition: v1.73.0](https://github.com/documize/community/releases)
[Enterprise edition: v1.74.1](https://documize.com/downloads)
[Enterprise edition: v1.75.0](https://documize.com/downloads)
## OS support
@ -100,7 +100,7 @@ Documize supports the following (evergreen) browsers:
Documize is built with the following technologies:
- EmberJS (v3.1.2)
- Go (v1.11.1)
- Go (v1.11.2)
## Authentication options

View file

@ -32,15 +32,6 @@ func InstallUpgrade(runtime *env.Runtime, existingDB bool) (err error) {
return
}
// Filter out database specific scripts.
dbTypeScripts := SpecificScripts(runtime, scripts)
if len(dbTypeScripts) == 0 {
runtime.Log.Info(fmt.Sprintf("Database: unable to load scripts for database type %s", runtime.StoreProvider.Type()))
return
}
runtime.Log.Info(fmt.Sprintf("Database: loaded %d SQL scripts for provider %s", len(dbTypeScripts), runtime.StoreProvider.Type()))
// Get current database version.
currentVersion := 0
if existingDB {
@ -53,6 +44,15 @@ func InstallUpgrade(runtime *env.Runtime, existingDB bool) (err error) {
runtime.Log.Info(fmt.Sprintf("Database: current version number is %d", currentVersion))
}
// Filter out database specific scripts.
dbTypeScripts := SpecificScripts(runtime, scripts)
if len(dbTypeScripts) == 0 {
runtime.Log.Info(fmt.Sprintf("Database: unable to load scripts for database type %s", runtime.StoreProvider.Type()))
return
}
runtime.Log.Info(fmt.Sprintf("Database: loaded %d SQL scripts for provider %s", len(dbTypeScripts), runtime.StoreProvider.Type()))
// Make a list of scripts to execute based upon current database state.
toProcess := []Script{}
for _, s := range dbTypeScripts {
@ -90,52 +90,6 @@ func InstallUpgrade(runtime *env.Runtime, existingDB bool) (err error) {
tx.Commit()
return nil
// New style schema
// if existingDB {
// amLeader, err = Lock(runtime, len(toProcess))
// if err != nil {
// runtime.Log.Error("Database: failed to lock existing database for processing", err)
// }
// } else {
// // New installation hopes that you are only spinning up one instance of Documize.
// // Assumption: nobody will perform the intial setup in a clustered environment.
// amLeader = true
// }
// tx, err := runtime.Db.Beginx()
// if err != nil {
// return Unlock(runtime, tx, err, amLeader)
// }
// // If currently running process is database leader then we perform upgrade.
// if amLeader {
// runtime.Log.Info(fmt.Sprintf("Database: %d SQL scripts to process", len(toProcess)))
// err = runScripts(runtime, tx, toProcess)
// if err != nil {
// runtime.Log.Error("Database: error processing SQL script", err)
// }
// return Unlock(runtime, tx, err, amLeader)
// }
// // If currently running process is a slave instance then we wait for migration to complete.
// targetVersion := toProcess[len(toProcess)-1].Version
// for targetVersion != currentVersion {
// time.Sleep(time.Second)
// runtime.Log.Info("Database: slave instance polling for upgrade process completion")
// tx.Rollback()
// // Get database version and check again.
// currentVersion, err = CurrentVersion(runtime)
// if err != nil {
// return Unlock(runtime, tx, err, amLeader)
// }
// }
// return Unlock(runtime, tx, nil, amLeader)
}
// Run SQL scripts to instal or upgrade this database.

View file

@ -0,0 +1,6 @@
/* community edition */
-- add subscription
ALTER TABLE dmz_org ADD COLUMN `c_sub` JSON NULL AFTER `c_authconfig`;
-- deprecations

View file

@ -0,0 +1,6 @@
/* community edition */
-- add subscription
ALTER TABLE dmz_org ADD COLUMN c_sub JSON NULL;
-- deprecations

27
core/env/flags.go vendored
View file

@ -25,12 +25,13 @@ import (
type Flags struct {
DBConn string // database connection string
Salt string // the salt string used to encode JWT tokens
DBType string // (optional) database type
DBType string // database type
SSLCertFile string // (optional) name of SSL certificate PEM file
SSLKeyFile string // (optional) name of SSL key PEM file
HTTPPort string // (optional) HTTP or HTTPS port
ForceHTTPPort2SSL string // (optional) HTTP that should be redirected to HTTPS
SiteMode string // (optional) if 1 then serve offline web page
Location string // reserved
}
// SSLEnabled returns true if both cert and key were provided at runtime.
@ -71,8 +72,9 @@ var flagList progFlags
var loadMutex sync.Mutex
// ParseFlags loads command line and OS environment variables required by the program to function.
func ParseFlags() (f Flags) {
var dbConn, dbType, jwtKey, siteMode, port, certFile, keyFile, forcePort2SSL string
func ParseFlags() (f Flags, ok bool) {
ok = true
var dbConn, dbType, jwtKey, siteMode, port, certFile, keyFile, forcePort2SSL, location string
register(&jwtKey, "salt", false, "the salt string used to encode JWT tokens, if not set a random value will be generated")
register(&certFile, "cert", false, "the cert.pem file used for https")
@ -82,8 +84,11 @@ func ParseFlags() (f Flags) {
register(&siteMode, "offline", false, "set to '1' for OFFLINE mode")
register(&dbType, "dbtype", true, "specify the database provider: mysql|percona|mariadb|postgresql")
register(&dbConn, "db", true, `'database specific connection string for example "user:password@tcp(localhost:3306)/dbname"`)
register(&location, "location", false, `reserved`)
parse("db")
if !parse("db") {
ok = false
}
f.DBConn = dbConn
f.ForceHTTPPort2SSL = forcePort2SSL
@ -94,7 +99,13 @@ func ParseFlags() (f Flags) {
f.SSLKeyFile = keyFile
f.DBType = strings.ToLower(dbType)
return f
// reserved
if len(location) == 0 {
location = "selfhost"
}
f.Location = strings.ToLower(location)
return f, ok
}
// register prepares flag for subsequent parsing
@ -116,7 +127,7 @@ func register(target *string, name string, required bool, usage string) {
}
// parse loads flags from OS environment and command line switches
func parse(doFirst string) {
func parse(doFirst string) (ok bool) {
loadMutex.Lock()
defer loadMutex.Unlock()
@ -141,10 +152,12 @@ func parse(doFirst string) {
}
fmt.Fprintln(os.Stderr)
flag.Usage()
return
return false
}
}
}
}
}
return true
}

165
core/env/product.go vendored
View file

@ -1,165 +0,0 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package env
import (
"fmt"
"time"
)
// Edition is either Community or Enterprise.
type Edition string
// Package controls feature-set within edition.
type Package string
// Plan tells us if instance if self-hosted or Documize SaaS/Cloud.
type Plan string
// Seats represents number of users.
type Seats int
const (
// CommunityEdition is AGPL licensed open core of product.
CommunityEdition Edition = "Community"
// EnterpriseEdition is proprietary closed-source product.
EnterpriseEdition Edition = "Enterprise"
// PackageEssentials provides core capabilities.
PackageEssentials Package = "Essentials"
// PackageAdvanced provides analytics, reporting,
// content lifecycle, content verisoning, and audit logs.
PackageAdvanced Package = "Advanced"
// PackagePremium provides actions, feedback capture,
// approvals workflow, secure external sharing.
PackagePremium Package = "Premium"
// PackageDataCenter provides multi-tenanting
// and a bunch of professional services.
PackageDataCenter Package = "Data Center"
// PlanCloud represents *.documize.com hosting.
PlanCloud Plan = "Cloud"
// PlanSelfHost represents privately hosted Documize instance.
PlanSelfHost Plan = "Self-host"
// Seats0 is 0 users.
Seats0 Seats = 0
// Seats1 is 10 users.
Seats1 Seats = 10
// Seats2 is 25 users.
Seats2 Seats = 25
//Seats3 is 50 users.
Seats3 Seats = 50
// Seats4 is 100 users.
Seats4 Seats = 100
//Seats5 is 250 users.
Seats5 Seats = 250
// Seats6 is unlimited.
Seats6 Seats = 9999
)
// Product provides meta information about product and licensing.
type Product struct {
Edition Edition
Title string
Version string
Major string
Minor string
Patch string
Revision int
License License
}
// License provides details of product license.
type License struct {
Name string `json:"name"`
Email string `json:"email"`
Edition Edition `json:"edition"`
Package Package `json:"package"`
Plan Plan `json:"plan"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Seats Seats `json:"seats"`
Trial bool `json:"trial"`
// UserCount is number of users within Documize instance by tenant.
// Provided at runtime.
UserCount map[string]int
}
// IsEmpty determines if we have a license.
func (l *License) IsEmpty() bool {
return l.Seats == Seats0 &&
len(l.Name) == 0 && len(l.Email) == 0 && l.Start.Year() == 1 && l.End.Year() == 1
}
// Status returns formatted message stating if license is empty/populated and invalid/valid.
func (l *License) Status(orgID string) string {
lp := "populated"
if l.IsEmpty() {
lp = "empty"
}
lv := "invalid"
if l.IsValid(orgID) {
lv = "valid"
}
return fmt.Sprintf("License is %s and %s", lp, lv)
}
// IsValid returns if license is valid for specified tenant.
func (l *License) IsValid(orgID string) bool {
valid := false
// Community edition is always valid.
if l.Edition == CommunityEdition {
valid = true
}
// Enterprise edition is valid if subcription date is
// greater than now and we have enough users/seats.
if l.Edition == EnterpriseEdition {
if time.Now().UTC().Before(l.End) && l.UserCount[orgID] <= int(l.Seats) {
valid = true
}
}
// Empty means we cannot be valid
if l.IsEmpty() || len(l.UserCount) == 0 {
valid = false
}
return valid
}
// LicenseData holds encrypted data and is unpacked into License.
type LicenseData struct {
Key string `json:"key"`
Signature string `json:"signature"`
}
// LicenseUserAcount states number of active users by tenant.
type LicenseUserAcount struct {
OrgID string `json:"orgId"`
Users int `json:"users"`
}

3
core/env/runtime.go vendored
View file

@ -13,6 +13,7 @@
package env
import (
"github.com/documize/community/domain"
"github.com/jmoiron/sqlx"
)
@ -23,7 +24,7 @@ type Runtime struct {
Db *sqlx.DB
StoreProvider StoreProvider
Log Logger
Product Product
Product domain.Product
}
const (

View file

@ -14,7 +14,6 @@ package uniqueid
import (
"github.com/documize/community/core/uniqueid/xid"
"github.com/documize/community/core/uniqueid/xid16"
)
// Generate creates a randomly generated string suitable for use as part of an URI.
@ -22,16 +21,3 @@ import (
func Generate() string {
return xid.New().String()
}
// Generate16 creates a randomly generated 16 character length string suitable for use as part of an URI.
// It returns a string that is always 16 characters long.
func Generate16() string {
return xid16.New().String()
}
// beqassjmvbajrivsc0eg
// beqat1bmvbajrivsc0f0
// beqat1bmvbajrivsc1ag
// beqat1bmvbajrivsc1g0
// beqat1bmvbajrivsc1ug

View file

@ -206,7 +206,7 @@ func (b backerHandler) produce(id string) (files []backupItem, err error) {
func (b backerHandler) manifest(id string) (string, error) {
m := m.Manifest{
ID: id,
Edition: b.Runtime.Product.License.Edition,
Edition: b.Runtime.Product.Edition,
Version: b.Runtime.Product.Version,
Major: b.Runtime.Product.Major,
Minor: b.Runtime.Product.Minor,

View file

@ -449,6 +449,11 @@ func (r *restoreHandler) dmzConfig() (err error) {
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
for i := range c {
// We skip database schema version setting as this varies
// between database providers (e.g. MySQL v26, PostgreSQL v2).
if strings.ToUpper(c[i].ConfigKey) == "META" {
continue
}
err = r.Store.Setting.Set(c[i].ConfigKey, c[i].ConfigValue)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, c[i].ConfigKey))

View file

@ -39,7 +39,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "block.add"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}

View file

@ -20,8 +20,7 @@ import (
"github.com/jmoiron/sqlx"
)
// RequestContext provides per request scoped values required
// by HTTP handlers.
// RequestContext provides per request scoped values required for HTTP handlers.
type RequestContext struct {
AllowAnonymousAccess bool
Authenticated bool
@ -36,14 +35,13 @@ type RequestContext struct {
Expires time.Time
Fullname string
Transaction *sqlx.Tx
AppVersion string
Administrator bool
Analytics bool
Active bool
Editor bool
GlobalAdmin bool
ViewUsers bool
Subscription Subscription
}
//GetAppURL returns full HTTP url for the app

View file

@ -174,7 +174,7 @@ func processDocument(ctx domain.RequestContext, r *env.Runtime, store *store.Sto
documentID := uniqueid.Generate()
document.RefID = documentID
if r.Product.Edition == env.CommunityEdition {
if r.Product.Edition == domain.CommunityEdition {
document.Lifecycle = workflow.LifecycleLive
} else {
document.Lifecycle = sp.Lifecycle

View file

@ -83,7 +83,6 @@ func (store *LocalStorageProvider) Convert(params api.ConversionJobRequest) (fil
defer func() { os.RemoveAll(inputFolder) }()
for _, v := range list {
if v.Size() > 0 && !strings.HasPrefix(v.Name(), ".") && v.Mode().IsRegular() {
filename = inputFolder + v.Name()
fileData, err := ioutil.ReadFile(filename)
@ -100,8 +99,6 @@ func (store *LocalStorageProvider) Convert(params api.ConversionJobRequest) (fil
fileRequest.LicenseKey = params.LicenseKey
fileRequest.LicenseSignature = params.LicenseSignature
fileRequest.ServiceEndpoint = params.ServiceEndpoint
//fileRequest.Job = params.OrgID + string(os.PathSeparator) + params.Job
//fileRequest.OrgID = params.OrgID
bits := strings.Split(filename, ".")
xtn := strings.ToLower(bits[len(bits)-1])

View file

@ -60,10 +60,9 @@ func (h *Handler) Meta(w http.ResponseWriter, r *http.Request) {
data.Version = h.Runtime.Product.Version
data.Revision = h.Runtime.Product.Revision
data.Edition = h.Runtime.Product.Edition
data.Valid = h.Runtime.Product.License.IsValid(org.RefID)
data.ConversionEndpoint = org.ConversionEndpoint
data.License = h.Runtime.Product.License
data.Storage = h.Runtime.StoreProvider.Type()
data.Location = h.Runtime.Flags.Location // reserved
// Strip secrets
data.AuthConfig = auth.StripAuthSecrets(h.Runtime, org.AuthProvider, org.AuthConfig)

View file

@ -31,10 +31,10 @@ type Store struct {
}
// AddOrganization inserts the passed organization record into the organization table.
func (s Store) AddOrganization(ctx domain.RequestContext, org org.Organization) (err error) {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_org (c_refid, c_company, c_title, c_message, c_domain, c_email, c_anonaccess, c_serial, c_maxtags, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.Domain),
strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.MaxTags, org.Created, org.Revised)
func (s Store) AddOrganization(ctx domain.RequestContext, o org.Organization) (err error) {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_org (c_refid, c_company, c_title, c_message, c_domain, c_email, c_anonaccess, c_serial, c_maxtags, c_sub, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
o.RefID, o.Company, o.Title, o.Message, strings.ToLower(o.Domain),
strings.ToLower(o.Email), o.AllowAnonymousAccess, o.Serial, o.MaxTags, o.Subscription, o.Created, o.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert for org")
@ -49,8 +49,8 @@ func (s Store) GetOrganization(ctx domain.RequestContext, id string) (org org.Or
c_title AS title, c_message AS message, c_domain AS domain,
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, c_maxtags AS maxtags,
c_created AS created, c_revised AS revised
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org
WHERE c_refid=?`),
id)
@ -80,8 +80,8 @@ func (s Store) GetOrganizationByDomain(subdomain string) (o org.Organization, er
c_title AS title, c_message AS message, c_domain AS domain,
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, c_maxtags AS maxtags,
c_created AS created, c_revised AS revised
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org
WHERE c_domain=? AND c_active=true`),
subdomain)
@ -95,8 +95,8 @@ func (s Store) GetOrganizationByDomain(subdomain string) (o org.Organization, er
c_title AS title, c_message AS message, c_domain AS domain,
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, c_maxtags AS maxtags,
c_created AS created, c_revised AS revised
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org
WHERE c_domain='' AND c_active=true`))
@ -113,7 +113,7 @@ func (s Store) UpdateOrganization(ctx domain.RequestContext, org org.Organizatio
_, err = ctx.Transaction.NamedExec(`UPDATE dmz_org SET
c_title=:title, c_message=:message, c_service=:conversionendpoint, c_email=:email,
c_anonaccess=:allowanonymousaccess, c_maxtags=:maxtags, c_revised=:revised
c_anonaccess=:allowanonymousaccess, c_sub=:subscription, c_maxtags=:maxtags, c_revised=:revised
WHERE c_refid=:refid`,
&org)

View file

@ -52,7 +52,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "page.add"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -322,7 +322,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
method := "page.update"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -510,7 +510,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "page.delete"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -608,7 +608,7 @@ func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
method := "page.delete.pages"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -721,7 +721,7 @@ func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
method := "page.sequence"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -791,7 +791,7 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
method := "page.level"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -987,7 +987,7 @@ func (h *Handler) GetDocumentRevisions(w http.ResponseWriter, r *http.Request) {
method := "page.document.revisions"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -1018,7 +1018,7 @@ func (h *Handler) GetRevisions(w http.ResponseWriter, r *http.Request) {
method := "page.revisions"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -1053,7 +1053,7 @@ func (h *Handler) GetDiff(w http.ResponseWriter, r *http.Request) {
method := "page.diff"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}

View file

@ -45,7 +45,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return
}
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -154,7 +154,7 @@ func (h *Handler) DeleteUserPin(w http.ResponseWriter, r *http.Request) {
return
}
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -198,7 +198,7 @@ func (h *Handler) UpdatePinSequence(w http.ResponseWriter, r *http.Request) {
return
}
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}

313
domain/product.go Normal file
View file

@ -0,0 +1,313 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package domain
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"encoding/pem"
"encoding/xml"
"time"
)
// Edition is either Community or Enterprise.
type Edition string
// Package controls feature-set within edition.
type Package string
// Plan tells us if instance if self-hosted or Documize SaaS/Cloud.
type Plan string
// Seats represents number of users.
type Seats int
const (
// CommunityEdition is AGPL licensed open core of product.
CommunityEdition Edition = "Community"
// EnterpriseEdition is proprietary closed-source product.
EnterpriseEdition Edition = "Enterprise"
// PackageEssentials provides core capabilities.
PackageEssentials Package = "Essentials"
// PackageAdvanced provides analytics, reporting,
// content lifecycle, content verisoning, and audit logs.
PackageAdvanced Package = "Advanced"
// PackagePremium provides actions, feedback capture,
// approvals workflow, secure external sharing.
PackagePremium Package = "Premium"
// PackageDataCenter provides multi-tenanting
// and a bunch of professional services.
PackageDataCenter Package = "Data Center"
// PlanCloud represents *.documize.com hosting.
PlanCloud Plan = "Cloud"
// PlanSelfHost represents privately hosted Documize instance.
PlanSelfHost Plan = "Self-host"
// Seats0 is 0 users.
Seats0 Seats = 0
// Seats1 is 10 users.
Seats1 Seats = 10
// Seats2 is 25 users.
Seats2 Seats = 25
//Seats3 is 50 users.
Seats3 Seats = 50
// Seats4 is 100 users.
Seats4 Seats = 100
//Seats5 is 250 users.
Seats5 Seats = 250
// Seats6 is unlimited.
Seats6 Seats = 9999
)
// Product provides product meta information and handles
// subscription validation for Enterprise edition.
type Product struct {
Edition Edition
Title string
Version string
Major string
Minor string
Patch string
Revision int
// UserCount is number of users within Documize instance by tenant.
UserCount map[string]int
}
// IsValid returns if subscription is valid using RequestContext.
func (p *Product) IsValid(ctx RequestContext) bool {
// Community edition is always valid.
if p.Edition == CommunityEdition {
return true
}
// Empty means we cannot be valid.
if ctx.Subscription.IsEmpty() {
return false
}
// Enterprise edition is valid if system has loaded up user count by tenant.
if uc, ok := p.UserCount[ctx.OrgID]; ok {
// Enterprise edition is valid if subcription date is greater than now and we have enough users/seats.
if time.Now().UTC().Before(ctx.Subscription.End) && uc <= int(ctx.Subscription.Seats) {
return true
}
}
return false
}
// SubscriptionData holds encrypted data and is unpacked into Subscription.
type SubscriptionData struct {
Key string `json:"key"`
Signature string `json:"signature"`
}
// SubscriptionXML represents subscription data as XML document.
type SubscriptionXML struct {
XMLName xml.Name `xml:"Documize"`
Key string
Signature string
}
// Subscription data for customer.
type Subscription struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Edition Edition `json:"edition"`
Plan Plan `json:"plan"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Seats Seats `json:"seats"`
Trial bool `json:"trial"`
Price uint64 `json:"price"`
// Derived fields
ActiveUsers int `json:"activeUsers"`
Status int `json:"status"`
}
// IsEmpty determines if we have a license.
func (s *Subscription) IsEmpty() bool {
return s.Seats == Seats0 &&
len(s.Name) == 0 && len(s.Email) == 0 && s.Start.Year() == 1 && s.End.Year() == 1
}
// SubscriptionUserAccount states number of active users by tenant.
type SubscriptionUserAccount struct {
OrgID string `json:"orgId"`
Users int `json:"users"`
}
// SubscriptionAsXML returns subscription data as XML document:
//
// <DocumizeLicense>
// <Key>some key</Key>
// <Signature>some signature</Signature>
// </DocumizeLicense>
//
// XML document is empty in case of error.
func SubscriptionAsXML(j SubscriptionData) (b []byte, err error) {
x := &SubscriptionXML{Key: j.Key, Signature: j.Signature}
b, err = xml.Marshal(x)
return
}
// DecodeSubscription returns Documize issued product licensing information.
func DecodeSubscription(sd SubscriptionData) (sub Subscription, err error) {
// Empty check.
if len(sd.Key) == 0 || len(sd.Signature) == 0 {
return
}
var ciphertext, signature []byte
ciphertext, _ = hex.DecodeString(sd.Key)
signature, _ = hex.DecodeString(sd.Signature)
// Load up keys.
serverBlock, _ := pem.Decode([]byte(serverPublicKeyPEM4096))
serverPublicKey, _ := x509.ParsePKIXPublicKey(serverBlock.Bytes)
clientBlock, _ := pem.Decode([]byte(clientPrivateKeyPEM4096))
clientPrivateKey, _ := x509.ParsePKCS1PrivateKey(clientBlock.Bytes)
label := []byte("dmzsub")
hash := sha256.New()
plainText, err := rsa.DecryptOAEP(hash, rand.Reader, clientPrivateKey, ciphertext, label)
if err != nil {
return
}
// check signature
var opts rsa.PSSOptions
opts.SaltLength = rsa.PSSSaltLengthAuto
PSSmessage := plainText
newhash := crypto.SHA256
pssh := newhash.New()
pssh.Write(PSSmessage)
hashed := pssh.Sum(nil)
err = rsa.VerifyPSS(serverPublicKey.(*rsa.PublicKey), newhash, hashed, signature, &opts)
if err != nil {
return
}
err = json.Unmarshal(plainText, &sub)
return
}
var serverPublicKeyPEM4096 = `
-----BEGIN PUBLIC KEY-----
MIICITANBgkqhkiG9w0BAQEFAAOCAg4AMIICCQKCAgB1/J5crBk0rK+zkPn6p4nf
qitsftN1/wrGq3xrXLhBax/+zyr3wm4Cd8bYANZjfzKw8jSoTqhoqwGF2J1A8Mjg
Orfn04UGsM/Em+5g2b6d/Uc3tyoR7DJYwr0coc0rPZaypneAhaf6ob266CU8QEdE
xkRkPMc/1TAOPmUkeuM2LI9Q/LDA5zPnN3WgYLGd7O1bSVOQYjw4KVp7Xr987Cec
CHWzrrjwQ7vRYUqxpz1kQ8ZAmhnFAkAQzScE87kPKM9V2Txo0NQ9aL2idP2FoVi0
obgGfJShI25YAeQncJBsyHV/uWxd3l/egaTHyQlcMgxBv61qsqgKzFZFsTNleQ3x
SR4i8QnNLk0hwtR+NREJZRlIdMGBwV7elJa+8v8Zw6lbC1J8OghNseggGcBOoG6v
OOwnEy6DK7hS3qfnHhFvR2zr9R5iQLHBIeVaVFZiLMKffRZnyHc3Dt5ozFMvpnzH
TBaHzydI57mrYZKv3s8hEgVJqMA9d1zCd9bwPwDIqiR/tYgPadwagQwHE4d4Pg8f
K8anfghelduKB0qIfeuQIEKmErEDK/qHj8HUC4nYUQy7hIo4F9D/HB22IfD6rM4D
BrswLjnIDcW9ox8Sv2wT5FsRJqdYE80gmG68QjrGPcwqwkO6WhgAfr/LXx2kJ2HI
BAMmkAYoyOaGK82XYKC14wIDAQAB
-----END PUBLIC KEY-----
`
var clientPublicKeyPEM4096 = `
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmtXoHjZ4Ky7gMqp9gY4f
TQ+EhtgGxlkn3b48doQXhHemq/QyrcVj5FcHr9Um6pxop/HDQX2N7DEKX52ShFwa
Ccv7iWWcZ8secope3nNouO80v9umb0LqWqVvfZSP4QbwDZa231baFWtnn2yiiOmA
SkLmexLU+fmGht2Df9Q0gQLofGeE6YzLrdvnwa1NJHEiowgWaS5dsvsxoZV6zDXG
428drRQ/JVt7soQbZENn0jiGSM+Tm77eXjMSu1oK8tnr7vm8ylBXj4rw6P4ONp50
Dd+lERsdJFK5EaKN4xnWVVKayUlZTFE8ZAMXckF48dG8i9IgRkkEf7UcKekB/+hT
1zIKHwmFjUy81jAmU5jySHFHfaGkIQKoKGFXQQt6st9rPLSLOFi8jLHYbJAO/Zs5
DTaOoGLwDYcPMsgZswUyxySBUPDXDzg31sIJYl35GmZf6AX7vWvcX3C0NJxhnEFy
eXnyJMe3yUHOJmJmYT91V/IKmUl51xdCdb8Gy9wM2oee9QEvM8BJEctGrXmcCuVb
V7qkA79D3UK9QTbOthHsPWeWbaJDsmaxlwwp+crGTpcTLOyzwZdLaOr4bmNCQKUW
OC0hPqiwhHsxPwA8Je98EvjLT9YC23+dCN2OoN4cpnRtl/rYNtlCHnIQ1l+n4hvs
LMsDcJ/rlaak4OADM1YvNxUCAwEAAQ==
-----END PUBLIC KEY-----
`
var clientPrivateKeyPEM4096 = `
-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEAmtXoHjZ4Ky7gMqp9gY4fTQ+EhtgGxlkn3b48doQXhHemq/Qy
rcVj5FcHr9Um6pxop/HDQX2N7DEKX52ShFwaCcv7iWWcZ8secope3nNouO80v9um
b0LqWqVvfZSP4QbwDZa231baFWtnn2yiiOmASkLmexLU+fmGht2Df9Q0gQLofGeE
6YzLrdvnwa1NJHEiowgWaS5dsvsxoZV6zDXG428drRQ/JVt7soQbZENn0jiGSM+T
m77eXjMSu1oK8tnr7vm8ylBXj4rw6P4ONp50Dd+lERsdJFK5EaKN4xnWVVKayUlZ
TFE8ZAMXckF48dG8i9IgRkkEf7UcKekB/+hT1zIKHwmFjUy81jAmU5jySHFHfaGk
IQKoKGFXQQt6st9rPLSLOFi8jLHYbJAO/Zs5DTaOoGLwDYcPMsgZswUyxySBUPDX
Dzg31sIJYl35GmZf6AX7vWvcX3C0NJxhnEFyeXnyJMe3yUHOJmJmYT91V/IKmUl5
1xdCdb8Gy9wM2oee9QEvM8BJEctGrXmcCuVbV7qkA79D3UK9QTbOthHsPWeWbaJD
smaxlwwp+crGTpcTLOyzwZdLaOr4bmNCQKUWOC0hPqiwhHsxPwA8Je98EvjLT9YC
23+dCN2OoN4cpnRtl/rYNtlCHnIQ1l+n4hvsLMsDcJ/rlaak4OADM1YvNxUCAwEA
AQKCAgBNNDenSPWmYps76DLodJs662/jZLgMEsyEDqVLWxX24UpkF0Fl0DS82IBm
tlvPQ+oTQ8NeVmJ70QAhKQqzoNEC7Ykgu1+/iVJHPqOLO/SNsgiVWcqlU7JTPIZZ
EcikJbdwryPEPSRE5ecnYR2yMuvbG3ydBYjYlAj2GmHFTWRYp8CQt3VYlvHAYRQw
SF9cumTQ8elqzMm/wuy+azBtvqrLIM6lTKEn2XPWUXTvC4UrFzAuAgLR99wdEE5Y
yM8IxIyV/kSahHEEi/0P0A36QgwQFuHRo7lmMTFCj9E72dg7dxLjJwW1vhPksn3w
ZKEPwsrG1SFuql3p576BT0PF/GxA6KdiAR++DjP8w9Fj9TUlduNH7md7FLuu8zRe
lHqT9SyFsDGmpJWPuw5Xl9+PvLfZBXDiqDOhczKWmd+DglLKJQiQphUKVJCpJ0In
jHtLgPFFciPFJjTrlW6ROaK/1mFkaIXvpzj50reKrq2u/zD1SNSFGa5JpbWkN288
HrpGTB++dLMkYmhAzZc2HO58qz9Kr4VdCZP9EMLFruQLrnZprz/wpplgj+n382Nd
rpPbX4TSTOBgll4oaU5YwUuYegGa0G/uY9j3DG+bnaXy993wTC7VyupaI2jqxo3t
BfpJJk3i8Of+sidVzAR+FVkPCyYmW28XkEXEjL4DNsKV47snQQKCAQEA8rsRITYv
m3JePEKM2u/sGvxIGsXxge1G7y7bbOzfn1r22yThlasYocbF9kdxnyUffzmY/yQo
6FLK6L+B2o5c/U5OKSvy8Lk6tYpZPiZ1cCeScwxc0y2jiKodXrxStPTdTNB0JToX
RGVUhUMvlI40e7TQ4egucy8opd/LjdyC9OCe1fyK4p90b0TAwI20fOILs9nXhACr
rd3FZeiidm4xtYo1Z09kKjkozbgaOSWIzMXdY+jbAwfEqIWD0VAp2p2ryV4qAiaL
zk9XEYLXkmuK/5vgv16cJc67CjVSVBT+wG0IzzUMCbBeuoFsEiMPh0aM6+v0YRkc
9MkRjXvYoCI8KQKCAQEAo0zG/W65Yd6KIQ85vavqNfr/fYDG2rigfraWBuTflTsy
TjnNxkdNS5NYfm4BzlydWD5bQJaP0XP9W4lHgq23wh6FAfC0Yzwh3sBBXhi+R4v3
mgnwsgxuNLOxLXn4JP5hI8pu7fmC9PQlBywEhWjdubmOspeiL4rJQk0H76EQyCvR
/V8+C3SJnnCbI2fqMOpPn7GV06BFvYxohACNE+KCCe7Dt/QjzAxSgDl9yPyed+b7
8p/1dTxVkDAPcJXucubQR2moHqu6nnJxdOiGVMjlRouP6ji5pESMmIqOAtn9Vwke
svhzkm6zLAi7ZtxbWGTfVIsrl2IUBg3ino01h0YBDQKCAQAMCX7V+Mvvl4JY1qwJ
h3Bb/jrNKRfK66ti3R4AjtagHnCzeWa+d1enXiYfCnf1/m9Lbd3KeU6WBtUNKcIU
xo6R+TojDIzlpynkKtI2JM4aG7xFfE12I4NCmb0PH6OyWZpH3uaDmhfhSm0glq5b
XZn4sITTTyJOj/4iC7Eafd74qdL2pal1h5bMlcpBQkW7E7Kk3p6zax0YaDEL1reH
y/snF42CbAt5lJATc5fJUbUxAnbyJ3AE/HOiL8zTqngI4VzNhZ/rr2Grf3+/3I84
MaEY/+/rTZPMxC2+WdqVVN01SbLwI59PM7He6eAkHhz9BmCiqnbaAdbPxNDcBVI+
zrPRAoIBAAm5AogIVaVMGLFLNMbkO3enUBrq1ewj3fptaJVUfzNlaONbcbMCf8mm
Jjiw2A6vWPbuD4TS8hEodMdEbyuKqEw4gPbSnArkg6e9jqbJllqwLLfRK7GOJ+mf
YUcx4eJh+uqknOIyXueyuZmpt0MyMTFjqOldOdzWyJDYAUb1MgiZA1GwoAMSlzcF
wVbkUv9ClCcP7bnB6yUT/Q0O81dhvxhUTPbg5Fi7yxWzVpfm4pCFAi858uVeCEIj
emfbpWzV7USzN71LwDq62aJ6TbUymOQQXys04Wi0ZCKY7UeiLwFFm7xQKqFnUeen
RXEkYZPrvZhNCPVkc4jAvuNtyOga9OkCggEAHt/Jr+pbw2CSGo+ttXyF70W1st7Y
xrzqHzEdNiYH8EILTBI2Xaen2HUka8dnjBdMwnaB7KXfu1J3Fg3Ot7Wp8JhOQKWF
tY/F9sKbAeF2s8AdsMlq3zBkwtobwhI6vx/NWmQ0AP01uP3h1uFWRmPXc3NweOjk
T7ntGmUrRQUKCGE9lUL1QwOnp5y3ZwPD9goa/h+Hh6Z8Ax4UqIC2wj0wgLgExbCk
BNCyKXHWawjvYMCmqOOAlLzgVfgljFVgV3DfJKgGZ4d3jQEb3XMfoWpyz5d2yjZu
SO3B+gGCaaT1MkalPcH+j8EldrU2xTvmeaQUSndlCIR1hOugae0cNaaKBA==
-----END RSA PRIVATE KEY-----
`

View file

@ -14,13 +14,11 @@ package setting
import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"github.com/documize/community/core/env"
"github.com/documize/community/core/event"
"github.com/documize/community/core/request"
"github.com/documize/community/core/response"
"github.com/documize/community/core/streamutil"
@ -128,96 +126,6 @@ func (h *Handler) SetSMTP(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, result)
}
// License returns product license
func (h *Handler) License(w http.ResponseWriter, r *http.Request) {
ctx := domain.GetRequestContext(r)
if !ctx.GlobalAdmin {
response.WriteForbiddenError(w)
return
}
config, _ := h.Store.Setting.Get("EDITION-LICENSE", "")
if len(config) == 0 {
config = "{}"
}
x := &licenseXML{Key: "", Signature: ""}
lj := licenseJSON{}
err := json.Unmarshal([]byte(config), &lj)
if err == nil {
x.Key = lj.Key
x.Signature = lj.Signature
} else {
h.Runtime.Log.Error("failed to JSON unmarshal EDITION-LICENSE", err)
}
output, err := xml.Marshal(x)
if err != nil {
h.Runtime.Log.Error("failed to XML marshal EDITION-LICENSE", err)
}
response.WriteBytes(w, output)
}
// SetLicense persists product license
func (h *Handler) SetLicense(w http.ResponseWriter, r *http.Request) {
method := "setting.SetLicense"
ctx := domain.GetRequestContext(r)
if !ctx.GlobalAdmin {
response.WriteForbiddenError(w)
return
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
return
}
var config string
config = string(body)
lj := licenseJSON{}
x := licenseXML{Key: "", Signature: ""}
err1 := xml.Unmarshal([]byte(config), &x)
if err1 == nil {
lj.Key = x.Key
lj.Signature = x.Signature
} else {
h.Runtime.Log.Error("failed to XML unmarshal EDITION-LICENSE", err)
}
j, err2 := json.Marshal(lj)
js := "{}"
if err2 == nil {
js = string(j)
} else {
h.Runtime.Log.Error("failed to JSON marshal EDITION-LICENSE", err2)
}
h.Store.Setting.Set("EDITION-LICENSE", js)
/* ctx.Transaction, err = h.Runtime.Db.Beginx()*/
//if err != nil {
//response.WriteServerError(w, method, err)
//return
//}
/*ctx.Transaction.Commit()*/
h.Runtime.Log.Info("License changed")
event.Handler().Publish(string(event.TypeSystemLicenseChange))
h.Store.Audit.Record(ctx, audit.EventTypeSystemLicense)
response.WriteEmpty(w)
}
// AuthConfig returns installation-wide auth configuration
func (h *Handler) AuthConfig(w http.ResponseWriter, r *http.Request) {
method := "global.auth"

View file

@ -12,27 +12,7 @@
// Package setting manages both global and user level settings
package setting
import "encoding/xml"
type licenseXML struct {
XMLName xml.Name `xml:"DocumizeLicense"`
Key string
Signature string
}
type licenseJSON struct {
Key string `json:"key"`
Signature string `json:"signature"`
}
type authData struct {
AuthProvider string `json:"authProvider"`
AuthConfig string `json:"authConfig"`
}
/*
<DocumizeLicense>
<Key>some key</Key>
<Signature>some signature</Signature>
</DocumizeLicense>
*/

View file

@ -57,7 +57,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "space.add"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -582,7 +582,7 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
method := "space.remove"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -675,7 +675,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "space.delete"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}

View file

@ -12,7 +12,6 @@
package store
import (
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/model/account"
"github.com/documize/community/model/activity"
@ -123,7 +122,7 @@ type UserStorer interface {
UpdateUserPassword(ctx domain.RequestContext, userID, salt, password string) (err error)
DeactiveUser(ctx domain.RequestContext, userID string) (err error)
ForgotUserPassword(ctx domain.RequestContext, email, token string) (err error)
CountActiveUsers() (c []env.LicenseUserAcount)
CountActiveUsers() (c []domain.SubscriptionUserAccount)
MatchUsers(ctx domain.RequestContext, text string, maxMatches int) (u []user.User, err error)
}

View file

@ -90,7 +90,7 @@ func (h *Handler) SaveAs(w http.ResponseWriter, r *http.Request) {
method := "template.saved"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
return
}
@ -343,7 +343,7 @@ func (h *Handler) Use(w http.ResponseWriter, r *http.Request) {
d.UserID = ctx.UserID
d.Name = docTitle
if h.Runtime.Product.Edition == env.CommunityEdition {
if h.Runtime.Product.Edition == domain.CommunityEdition {
d.Lifecycle = workflow.LifecycleLive
} else {
d.Lifecycle = sp.Lifecycle

View file

@ -35,10 +35,6 @@ func startRuntime() (rt *env.Runtime, s *store.Store) {
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = "Test"
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
rt.Product.License = env.License{}
rt.Product.License.Seats = 1
rt.Product.License.Trial = false
rt.Product.License.Edition = env.CommunityEdition
// parse settings from command line and environment
rt.Flags = env.ParseFlags()

View file

@ -51,10 +51,9 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "user.Add"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) {
if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w)
}
if !ctx.Administrator {
response.WriteForbiddenError(w)
return
@ -101,7 +100,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
requestedPassword := secrets.GenerateRandomPassword()
userModel.Salt = secrets.GenerateSalt()
userModel.Password = secrets.GeneratePassword(requestedPassword, userModel.Salt)
userModel.LastVersion = ctx.AppVersion
userModel.LastVersion = fmt.Sprintf("v%s", h.Runtime.Product.Version)
// only create account if not dupe
addUser := true

View file

@ -14,7 +14,6 @@ package user
import (
"database/sql"
"fmt"
"github.com/documize/community/core/env"
"strconv"
"strings"
"time"
@ -313,7 +312,7 @@ func (s Store) ForgotUserPassword(ctx domain.RequestContext, email, token string
}
// CountActiveUsers returns the number of active users in the system.
func (s Store) CountActiveUsers() (c []env.LicenseUserAcount) {
func (s Store) CountActiveUsers() (c []domain.SubscriptionUserAccount) {
err := s.Runtime.Db.Select(&c, "SELECT c_orgid AS orgid, COUNT(*) AS users FROM dmz_user_account WHERE c_active=true GROUP BY c_orgid ORDER BY c_orgid")
if err != nil && err != sql.ErrNoRows {

View file

@ -64,7 +64,7 @@ func InitRuntime(r *env.Runtime, s *store.Store) bool {
// Open connection to database
db, err := sqlx.Open(r.StoreProvider.DriverName(), r.StoreProvider.MakeConnectionString()) //r.Flags.DBConn
if err != nil {
r.Log.Error("unable to setup database", err)
r.Log.Error("unable to open database", err)
}
// Database handle
@ -94,6 +94,3 @@ func InitRuntime(r *env.Runtime, s *store.Store) bool {
return true
}
// Clever way to detect database type:
// https://github.com/golang-sql/sqlexp/blob/c2488a8be21d20d31abf0d05c2735efd2d09afe4/quoter.go#L46

View file

@ -14,9 +14,10 @@ package main
import (
"fmt"
"time"
"os"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/domain/section"
"github.com/documize/community/domain/store"
"github.com/documize/community/edition/boot"
@ -37,26 +38,27 @@ func main() {
web.Embed = embed.NewEmbedder()
// product details
rt.Product = env.Product{}
rt.Product = domain.Product{}
rt.Product.Major = "1"
rt.Product.Minor = "72"
rt.Product.Patch = "1"
rt.Product.Minor = "73"
rt.Product.Patch = "0"
rt.Product.Revision = 181022154519
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = env.CommunityEdition
rt.Product.Edition = domain.CommunityEdition
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
// Community edition is good to go with no user limits.
rt.Product.License = env.License{Edition: env.CommunityEdition, Seats: env.Seats6, Trial: false,
Start: time.Now().UTC(), End: time.Now().UTC().Add(time.Hour * 24 * 7 * time.Duration(52))}
// Setup data store.
s := store.Store{}
// parse settings from command line and environment
rt.Flags = env.ParseFlags()
flagsOK := boot.InitRuntime(&rt, &s)
if flagsOK {
// Parse flags/envvars.
flagsOK := false
rt.Flags, flagsOK = env.ParseFlags()
if !flagsOK {
os.Exit(0)
}
bootOK := boot.InitRuntime(&rt, &s)
if bootOK {
// runtime.Log = runtime.Log.SetDB(runtime.Db)
}

File diff suppressed because one or more lines are too long

View file

@ -43,9 +43,9 @@ export default Service.extend({
// for bugfix releases, only admin is made aware of new release and end users see no What's New messaging
updateAvailable: false,
invalidLicense() {
return this.valid === false;
},
// invalidLicense() {
// return this.valid === false;
// },
getBaseUrl(endpoint) {
return [this.get('endpoint'), endpoint].join('/');

View file

@ -1,6 +1,6 @@
{
"name": "documize",
"version": "1.72.0",
"version": "1.73.0",
"description": "The Document IDE",
"private": true,
"repository": "",

View file

@ -16,6 +16,7 @@ import (
"time"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/model"
"github.com/documize/community/model/org"
)
@ -30,7 +31,7 @@ type Manifest struct {
OrgID string `json:"org"`
// Product edition at the time of the backup.
Edition env.Edition `json:"edition"`
Edition domain.Edition `json:"edition"`
// When the backup took place.
Created time.Time `json:"created"`

6
model/doc.go Normal file
View file

@ -0,0 +1,6 @@
package model
// Model contains data structures used throughout Documize.
//
// Models can be used by any other package hence this package
// should not import other packages to avoid cyclical dependencies.

View file

@ -15,6 +15,7 @@ import (
"time"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
)
// SitemapDocument details a document that can be exposed via Sitemap.
@ -38,9 +39,8 @@ type SiteMeta struct {
Version string `json:"version"`
Revision int `json:"revision"`
MaxTags int `json:"maxTags"`
Edition env.Edition `json:"edition"`
Valid bool `json:"valid"`
Edition domain.Edition `json:"edition"`
ConversionEndpoint string `json:"conversionEndpoint"`
License env.License `json:"license"`
Storage env.StoreType `json:"storageProvider"`
Location string `json:"location"` // reserved for internal use
}

View file

@ -13,7 +13,7 @@ package org
import "github.com/documize/community/model"
// Organization defines a company that uses this app.
// Organization defines a tenant that uses this app.
type Organization struct {
model.BaseEntity
Company string `json:"company"`
@ -28,4 +28,5 @@ type Organization struct {
MaxTags int `json:"maxTags"`
Serial string `json:"serial"`
Active bool `json:"active"`
Subscription string
}

View file

@ -19,6 +19,7 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/documize/community/core/env"
"github.com/documize/community/core/response"
@ -39,7 +40,7 @@ func (m *middleware) cors(w http.ResponseWriter, r *http.Request, next http.Hand
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS, PATCH")
w.Header().Set("Access-Control-Allow-Headers", "host, content-type, accept, authorization, origin, referer, user-agent, cache-control, x-requested-with")
w.Header().Set("Access-Control-Expose-Headers", "x-documize-version, x-documize-status, x-documize-filename, Content-Disposition, Content-Length")
w.Header().Set("Access-Control-Expose-Headers", "x-documize-version, x-documize-status, x-documize-filename, x-documize-subscription, Content-Disposition, Content-Length")
if r.Method == "OPTIONS" {
w.Header().Add("X-Documize-Version", m.Runtime.Product.Version)
@ -53,16 +54,6 @@ func (m *middleware) cors(w http.ResponseWriter, r *http.Request, next http.Hand
next(w, r)
}
func (m *middleware) metrics(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Add("X-Documize-Version", m.Runtime.Product.Version)
w.Header().Add("Cache-Control", "no-cache")
// Prevent page from being displayed in an iframe
w.Header().Add("X-Frame-Options", "DENY")
next(w, r)
}
// Authorize secure API calls by inspecting authentication token.
// request.Context provides caller user information.
// Site meta sent back as HTTP custom headers.
@ -97,7 +88,6 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
m.Runtime.Log.Info(fmt.Sprintf("unable to find org (domain: %s, orgID: %s)", dom, rc.OrgID))
return
}
response.WriteForbiddenError(w)
m.Runtime.Log.Error(method, err)
return
@ -110,14 +100,6 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
}
rc.Subdomain = org.Domain
// dom := organization.GetSubdomainFromHost(r)
// dom2 := organization.GetRequestSubdomain(r)
// if org.Domain != dom && org.Domain != dom2 {
// m.Runtime.Log.Info(fmt.Sprintf("domain mismatch %s vs. %s vs. %s", dom, dom2, org.Domain))
// response.WriteUnauthorizedError(w)
// return
// }
// If we have bad auth token and the domain allows anon access
// then we generate guest context.
@ -148,7 +130,6 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
rc.AppURL = r.Host
rc.Subdomain = organization.GetSubdomainFromHost(r)
rc.SSL = r.TLS != nil
rc.AppVersion = fmt.Sprintf("v%s", m.Runtime.Product.Version)
// get user IP from request
i := strings.LastIndex(r.RemoteAddr, ":")
@ -163,6 +144,38 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
rc.ClientIP = fip
}
// Product subscription checks for both product editions.
weeks := 52
if m.Runtime.Product.Edition == domain.CommunityEdition {
// Subscription for Community edition is always valid.
rc.Subscription = domain.Subscription{Edition: domain.CommunityEdition,
Seats: domain.Seats6,
Trial: false,
Start: time.Now().UTC(),
End: time.Now().UTC().Add(time.Hour * 24 * 7 * time.Duration(weeks))}
} else {
// Enterprise edition requires valid subscription data.
if len(strings.TrimSpace(org.Subscription)) > 0 {
sd := domain.SubscriptionData{}
es1 := json.Unmarshal([]byte(org.Subscription), &sd)
if es1 == nil {
rc.Subscription, err = domain.DecodeSubscription(sd)
if err != nil {
m.Runtime.Log.Error("unable to decode subscription for org "+rc.OrgID, err)
}
} else {
m.Runtime.Log.Error("unable to load subscription for org "+rc.OrgID, es1)
}
}
}
// Tag all HTTP calls with subscription statu
subs := "false"
if m.Runtime.Product.IsValid(rc) {
subs = "true"
}
w.Header().Add("X-Documize-Subscription", subs)
// Fetch user permissions for this org
if rc.Authenticated {
u, err := user.GetSecuredUser(rc, *m.Store, org.RefID, rc.UserID)

View file

@ -210,8 +210,6 @@ func RegisterEndpoints(rt *env.Runtime, s *store.Store) {
// global admin routes
AddPrivate(rt, "global/smtp", []string{"GET", "OPTIONS"}, nil, setting.SMTP)
AddPrivate(rt, "global/smtp", []string{"PUT", "OPTIONS"}, nil, setting.SetSMTP)
AddPrivate(rt, "global/license", []string{"GET", "OPTIONS"}, nil, setting.License)
AddPrivate(rt, "global/license", []string{"PUT", "OPTIONS"}, nil, setting.SetLicense)
AddPrivate(rt, "global/auth", []string{"GET", "OPTIONS"}, nil, setting.AuthConfig)
AddPrivate(rt, "global/auth", []string{"PUT", "OPTIONS"}, nil, setting.SetAuthConfig)
AddPrivate(rt, "global/search/status", []string{"GET", "OPTIONS"}, nil, meta.SearchStatus)

View file

@ -80,7 +80,6 @@ func Start(rt *env.Runtime, s *store.Store, ready chan struct{}) {
n := negroni.New()
n.Use(negroni.NewStatic(web.StaticAssetsFileSystem()))
n.Use(negroni.HandlerFunc(cm.cors))
n.Use(negroni.HandlerFunc(cm.metrics))
n.UseHandler(router)
// tell caller we are ready to serve HTTP