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 ## 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 ## OS support
@ -100,7 +100,7 @@ Documize supports the following (evergreen) browsers:
Documize is built with the following technologies: Documize is built with the following technologies:
- EmberJS (v3.1.2) - EmberJS (v3.1.2)
- Go (v1.11.1) - Go (v1.11.2)
## Authentication options ## Authentication options

View file

@ -32,15 +32,6 @@ func InstallUpgrade(runtime *env.Runtime, existingDB bool) (err error) {
return 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. // Get current database version.
currentVersion := 0 currentVersion := 0
if existingDB { 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)) 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. // Make a list of scripts to execute based upon current database state.
toProcess := []Script{} toProcess := []Script{}
for _, s := range dbTypeScripts { for _, s := range dbTypeScripts {
@ -90,52 +90,6 @@ func InstallUpgrade(runtime *env.Runtime, existingDB bool) (err error) {
tx.Commit() tx.Commit()
return nil 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. // 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 { type Flags struct {
DBConn string // database connection string DBConn string // database connection string
Salt string // the salt string used to encode JWT tokens 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 SSLCertFile string // (optional) name of SSL certificate PEM file
SSLKeyFile string // (optional) name of SSL key PEM file SSLKeyFile string // (optional) name of SSL key PEM file
HTTPPort string // (optional) HTTP or HTTPS port HTTPPort string // (optional) HTTP or HTTPS port
ForceHTTPPort2SSL string // (optional) HTTP that should be redirected to HTTPS ForceHTTPPort2SSL string // (optional) HTTP that should be redirected to HTTPS
SiteMode string // (optional) if 1 then serve offline web page 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. // SSLEnabled returns true if both cert and key were provided at runtime.
@ -71,8 +72,9 @@ var flagList progFlags
var loadMutex sync.Mutex var loadMutex sync.Mutex
// ParseFlags loads command line and OS environment variables required by the program to function. // ParseFlags loads command line and OS environment variables required by the program to function.
func ParseFlags() (f Flags) { func ParseFlags() (f Flags, ok bool) {
var dbConn, dbType, jwtKey, siteMode, port, certFile, keyFile, forcePort2SSL string 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(&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") 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(&siteMode, "offline", false, "set to '1' for OFFLINE mode")
register(&dbType, "dbtype", true, "specify the database provider: mysql|percona|mariadb|postgresql") 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(&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.DBConn = dbConn
f.ForceHTTPPort2SSL = forcePort2SSL f.ForceHTTPPort2SSL = forcePort2SSL
@ -94,7 +99,13 @@ func ParseFlags() (f Flags) {
f.SSLKeyFile = keyFile f.SSLKeyFile = keyFile
f.DBType = strings.ToLower(dbType) 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 // 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 // parse loads flags from OS environment and command line switches
func parse(doFirst string) { func parse(doFirst string) (ok bool) {
loadMutex.Lock() loadMutex.Lock()
defer loadMutex.Unlock() defer loadMutex.Unlock()
@ -141,10 +152,12 @@ func parse(doFirst string) {
} }
fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr)
flag.Usage() 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 package env
import ( import (
"github.com/documize/community/domain"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -23,7 +24,7 @@ type Runtime struct {
Db *sqlx.DB Db *sqlx.DB
StoreProvider StoreProvider StoreProvider StoreProvider
Log Logger Log Logger
Product Product Product domain.Product
} }
const ( const (

View file

@ -14,7 +14,6 @@ package uniqueid
import ( import (
"github.com/documize/community/core/uniqueid/xid" "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. // Generate creates a randomly generated string suitable for use as part of an URI.
@ -22,16 +21,3 @@ import (
func Generate() string { func Generate() string {
return xid.New().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) { func (b backerHandler) manifest(id string) (string, error) {
m := m.Manifest{ m := m.Manifest{
ID: id, ID: id,
Edition: b.Runtime.Product.License.Edition, Edition: b.Runtime.Product.Edition,
Version: b.Runtime.Product.Version, Version: b.Runtime.Product.Version,
Major: b.Runtime.Product.Major, Major: b.Runtime.Product.Major,
Minor: b.Runtime.Product.Minor, 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)) r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
for i := range c { 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) err = r.Store.Setting.Set(c[i].ConfigKey, c[i].ConfigValue)
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, c[i].ConfigKey)) 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" method := "block.add"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }

View file

@ -20,8 +20,7 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
// RequestContext provides per request scoped values required // RequestContext provides per request scoped values required for HTTP handlers.
// by HTTP handlers.
type RequestContext struct { type RequestContext struct {
AllowAnonymousAccess bool AllowAnonymousAccess bool
Authenticated bool Authenticated bool
@ -36,14 +35,13 @@ type RequestContext struct {
Expires time.Time Expires time.Time
Fullname string Fullname string
Transaction *sqlx.Tx Transaction *sqlx.Tx
AppVersion string Administrator bool
Analytics bool
Administrator bool Active bool
Analytics bool Editor bool
Active bool GlobalAdmin bool
Editor bool ViewUsers bool
GlobalAdmin bool Subscription Subscription
ViewUsers bool
} }
//GetAppURL returns full HTTP url for the app //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() documentID := uniqueid.Generate()
document.RefID = documentID document.RefID = documentID
if r.Product.Edition == env.CommunityEdition { if r.Product.Edition == domain.CommunityEdition {
document.Lifecycle = workflow.LifecycleLive document.Lifecycle = workflow.LifecycleLive
} else { } else {
document.Lifecycle = sp.Lifecycle document.Lifecycle = sp.Lifecycle

View file

@ -83,7 +83,6 @@ func (store *LocalStorageProvider) Convert(params api.ConversionJobRequest) (fil
defer func() { os.RemoveAll(inputFolder) }() defer func() { os.RemoveAll(inputFolder) }()
for _, v := range list { for _, v := range list {
if v.Size() > 0 && !strings.HasPrefix(v.Name(), ".") && v.Mode().IsRegular() { if v.Size() > 0 && !strings.HasPrefix(v.Name(), ".") && v.Mode().IsRegular() {
filename = inputFolder + v.Name() filename = inputFolder + v.Name()
fileData, err := ioutil.ReadFile(filename) fileData, err := ioutil.ReadFile(filename)
@ -100,8 +99,6 @@ func (store *LocalStorageProvider) Convert(params api.ConversionJobRequest) (fil
fileRequest.LicenseKey = params.LicenseKey fileRequest.LicenseKey = params.LicenseKey
fileRequest.LicenseSignature = params.LicenseSignature fileRequest.LicenseSignature = params.LicenseSignature
fileRequest.ServiceEndpoint = params.ServiceEndpoint fileRequest.ServiceEndpoint = params.ServiceEndpoint
//fileRequest.Job = params.OrgID + string(os.PathSeparator) + params.Job
//fileRequest.OrgID = params.OrgID
bits := strings.Split(filename, ".") bits := strings.Split(filename, ".")
xtn := strings.ToLower(bits[len(bits)-1]) 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.Version = h.Runtime.Product.Version
data.Revision = h.Runtime.Product.Revision data.Revision = h.Runtime.Product.Revision
data.Edition = h.Runtime.Product.Edition data.Edition = h.Runtime.Product.Edition
data.Valid = h.Runtime.Product.License.IsValid(org.RefID)
data.ConversionEndpoint = org.ConversionEndpoint data.ConversionEndpoint = org.ConversionEndpoint
data.License = h.Runtime.Product.License
data.Storage = h.Runtime.StoreProvider.Type() data.Storage = h.Runtime.StoreProvider.Type()
data.Location = h.Runtime.Flags.Location // reserved
// Strip secrets // Strip secrets
data.AuthConfig = auth.StripAuthSecrets(h.Runtime, org.AuthProvider, org.AuthConfig) 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. // AddOrganization inserts the passed organization record into the organization table.
func (s Store) AddOrganization(ctx domain.RequestContext, org org.Organization) (err error) { 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_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), _, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.Domain), o.RefID, o.Company, o.Title, o.Message, strings.ToLower(o.Domain),
strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.MaxTags, org.Created, org.Revised) strings.ToLower(o.Email), o.AllowAnonymousAccess, o.Serial, o.MaxTags, o.Subscription, o.Created, o.Revised)
if err != nil { if err != nil {
err = errors.Wrap(err, "unable to execute insert for org") 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_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_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider, c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, c_maxtags AS maxtags, coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_created AS created, c_revised AS revised c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org FROM dmz_org
WHERE c_refid=?`), WHERE c_refid=?`),
id) 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_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_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider, c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, c_maxtags AS maxtags, coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_created AS created, c_revised AS revised c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org FROM dmz_org
WHERE c_domain=? AND c_active=true`), WHERE c_domain=? AND c_active=true`),
subdomain) 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_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_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider, c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, c_maxtags AS maxtags, coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_created AS created, c_revised AS revised c_maxtags AS maxtags, c_created AS created, c_revised AS revised
FROM dmz_org FROM dmz_org
WHERE c_domain='' AND c_active=true`)) 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 _, err = ctx.Transaction.NamedExec(`UPDATE dmz_org SET
c_title=:title, c_message=:message, c_service=:conversionendpoint, c_email=:email, 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`, WHERE c_refid=:refid`,
&org) &org)

View file

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

View file

@ -45,7 +45,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return return
} }
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }
@ -154,7 +154,7 @@ func (h *Handler) DeleteUserPin(w http.ResponseWriter, r *http.Request) {
return return
} }
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }
@ -198,7 +198,7 @@ func (h *Handler) UpdatePinSequence(w http.ResponseWriter, r *http.Request) {
return return
} }
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return 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-----
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
-----END RSA PRIVATE KEY-----
`

View file

@ -14,13 +14,11 @@ package setting
import ( import (
"encoding/json" "encoding/json"
"encoding/xml"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/core/event"
"github.com/documize/community/core/request" "github.com/documize/community/core/request"
"github.com/documize/community/core/response" "github.com/documize/community/core/response"
"github.com/documize/community/core/streamutil" "github.com/documize/community/core/streamutil"
@ -128,96 +126,6 @@ func (h *Handler) SetSMTP(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, result) 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 // AuthConfig returns installation-wide auth configuration
func (h *Handler) AuthConfig(w http.ResponseWriter, r *http.Request) { func (h *Handler) AuthConfig(w http.ResponseWriter, r *http.Request) {
method := "global.auth" method := "global.auth"

View file

@ -12,27 +12,7 @@
// Package setting manages both global and user level settings // Package setting manages both global and user level settings
package setting 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 { type authData struct {
AuthProvider string `json:"authProvider"` AuthProvider string `json:"authProvider"`
AuthConfig string `json:"authConfig"` 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" method := "space.add"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }
@ -582,7 +582,7 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
method := "space.remove" method := "space.remove"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }
@ -675,7 +675,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "space.delete" method := "space.delete"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }

View file

@ -12,7 +12,6 @@
package store package store
import ( import (
"github.com/documize/community/core/env"
"github.com/documize/community/domain" "github.com/documize/community/domain"
"github.com/documize/community/model/account" "github.com/documize/community/model/account"
"github.com/documize/community/model/activity" "github.com/documize/community/model/activity"
@ -123,7 +122,7 @@ type UserStorer interface {
UpdateUserPassword(ctx domain.RequestContext, userID, salt, password string) (err error) UpdateUserPassword(ctx domain.RequestContext, userID, salt, password string) (err error)
DeactiveUser(ctx domain.RequestContext, userID string) (err error) DeactiveUser(ctx domain.RequestContext, userID string) (err error)
ForgotUserPassword(ctx domain.RequestContext, email, token 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) 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" method := "template.saved"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid(ctx.OrgID) { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }
@ -343,7 +343,7 @@ func (h *Handler) Use(w http.ResponseWriter, r *http.Request) {
d.UserID = ctx.UserID d.UserID = ctx.UserID
d.Name = docTitle d.Name = docTitle
if h.Runtime.Product.Edition == env.CommunityEdition { if h.Runtime.Product.Edition == domain.CommunityEdition {
d.Lifecycle = workflow.LifecycleLive d.Lifecycle = workflow.LifecycleLive
} else { } else {
d.Lifecycle = sp.Lifecycle d.Lifecycle = sp.Lifecycle

View file

@ -35,12 +35,8 @@ 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.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = "Test" rt.Product.Edition = "Test"
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition) 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 // parse settings from command line and environment
rt.Flags = env.ParseFlags() rt.Flags = env.ParseFlags()
boot.InitRuntime(rt, s) boot.InitRuntime(rt, s)

View file

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

View file

@ -14,7 +14,6 @@ package user
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/documize/community/core/env"
"strconv" "strconv"
"strings" "strings"
"time" "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. // 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") 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 { 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 // Open connection to database
db, err := sqlx.Open(r.StoreProvider.DriverName(), r.StoreProvider.MakeConnectionString()) //r.Flags.DBConn db, err := sqlx.Open(r.StoreProvider.DriverName(), r.StoreProvider.MakeConnectionString()) //r.Flags.DBConn
if err != nil { if err != nil {
r.Log.Error("unable to setup database", err) r.Log.Error("unable to open database", err)
} }
// Database handle // Database handle
@ -94,6 +94,3 @@ func InitRuntime(r *env.Runtime, s *store.Store) bool {
return true 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 ( import (
"fmt" "fmt"
"time" "os"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/domain/section" "github.com/documize/community/domain/section"
"github.com/documize/community/domain/store" "github.com/documize/community/domain/store"
"github.com/documize/community/edition/boot" "github.com/documize/community/edition/boot"
@ -37,26 +38,27 @@ func main() {
web.Embed = embed.NewEmbedder() web.Embed = embed.NewEmbedder()
// product details // product details
rt.Product = env.Product{} rt.Product = domain.Product{}
rt.Product.Major = "1" rt.Product.Major = "1"
rt.Product.Minor = "72" rt.Product.Minor = "73"
rt.Product.Patch = "1" rt.Product.Patch = "0"
rt.Product.Revision = 181022154519 rt.Product.Revision = 181022154519
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch) 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) 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. // Setup data store.
s := store.Store{} s := store.Store{}
// parse settings from command line and environment // Parse flags/envvars.
rt.Flags = env.ParseFlags() flagsOK := false
flagsOK := boot.InitRuntime(&rt, &s) rt.Flags, flagsOK = env.ParseFlags()
if flagsOK { if !flagsOK {
os.Exit(0)
}
bootOK := boot.InitRuntime(&rt, &s)
if bootOK {
// runtime.Log = runtime.Log.SetDB(runtime.Db) // 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 // for bugfix releases, only admin is made aware of new release and end users see no What's New messaging
updateAvailable: false, updateAvailable: false,
invalidLicense() { // invalidLicense() {
return this.valid === false; // return this.valid === false;
}, // },
getBaseUrl(endpoint) { getBaseUrl(endpoint) {
return [this.get('endpoint'), endpoint].join('/'); return [this.get('endpoint'), endpoint].join('/');

View file

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

View file

@ -16,6 +16,7 @@ import (
"time" "time"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/model" "github.com/documize/community/model"
"github.com/documize/community/model/org" "github.com/documize/community/model/org"
) )
@ -30,7 +31,7 @@ type Manifest struct {
OrgID string `json:"org"` OrgID string `json:"org"`
// Product edition at the time of the backup. // Product edition at the time of the backup.
Edition env.Edition `json:"edition"` Edition domain.Edition `json:"edition"`
// When the backup took place. // When the backup took place.
Created time.Time `json:"created"` 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" "time"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/domain"
) )
// SitemapDocument details a document that can be exposed via Sitemap. // SitemapDocument details a document that can be exposed via Sitemap.
@ -28,19 +29,18 @@ type SitemapDocument struct {
// SiteMeta holds information associated with an Organization. // SiteMeta holds information associated with an Organization.
type SiteMeta struct { type SiteMeta struct {
OrgID string `json:"orgId"` OrgID string `json:"orgId"`
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message"` Message string `json:"message"`
URL string `json:"url"` URL string `json:"url"`
AllowAnonymousAccess bool `json:"allowAnonymousAccess"` AllowAnonymousAccess bool `json:"allowAnonymousAccess"`
AuthProvider string `json:"authProvider"` AuthProvider string `json:"authProvider"`
AuthConfig string `json:"authConfig"` AuthConfig string `json:"authConfig"`
Version string `json:"version"` Version string `json:"version"`
Revision int `json:"revision"` Revision int `json:"revision"`
MaxTags int `json:"maxTags"` MaxTags int `json:"maxTags"`
Edition env.Edition `json:"edition"` Edition domain.Edition `json:"edition"`
Valid bool `json:"valid"` ConversionEndpoint string `json:"conversionEndpoint"`
ConversionEndpoint string `json:"conversionEndpoint"` Storage env.StoreType `json:"storageProvider"`
License env.License `json:"license"` Location string `json:"location"` // reserved for internal use
Storage env.StoreType `json:"storageProvider"`
} }

View file

@ -13,7 +13,7 @@ package org
import "github.com/documize/community/model" 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 { type Organization struct {
model.BaseEntity model.BaseEntity
Company string `json:"company"` Company string `json:"company"`
@ -28,4 +28,5 @@ type Organization struct {
MaxTags int `json:"maxTags"` MaxTags int `json:"maxTags"`
Serial string `json:"serial"` Serial string `json:"serial"`
Active bool `json:"active"` Active bool `json:"active"`
Subscription string
} }

View file

@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/core/response" "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-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS, PATCH") 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-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" { if r.Method == "OPTIONS" {
w.Header().Add("X-Documize-Version", m.Runtime.Product.Version) 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) 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. // Authorize secure API calls by inspecting authentication token.
// request.Context provides caller user information. // request.Context provides caller user information.
// Site meta sent back as HTTP custom headers. // 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)) m.Runtime.Log.Info(fmt.Sprintf("unable to find org (domain: %s, orgID: %s)", dom, rc.OrgID))
return return
} }
response.WriteForbiddenError(w) response.WriteForbiddenError(w)
m.Runtime.Log.Error(method, err) m.Runtime.Log.Error(method, err)
return return
@ -110,14 +100,6 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
} }
rc.Subdomain = org.Domain 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 // If we have bad auth token and the domain allows anon access
// then we generate guest context. // 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.AppURL = r.Host
rc.Subdomain = organization.GetSubdomainFromHost(r) rc.Subdomain = organization.GetSubdomainFromHost(r)
rc.SSL = r.TLS != nil rc.SSL = r.TLS != nil
rc.AppVersion = fmt.Sprintf("v%s", m.Runtime.Product.Version)
// get user IP from request // get user IP from request
i := strings.LastIndex(r.RemoteAddr, ":") i := strings.LastIndex(r.RemoteAddr, ":")
@ -163,6 +144,38 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
rc.ClientIP = fip 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 // Fetch user permissions for this org
if rc.Authenticated { if rc.Authenticated {
u, err := user.GetSecuredUser(rc, *m.Store, org.RefID, rc.UserID) 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 // global admin routes
AddPrivate(rt, "global/smtp", []string{"GET", "OPTIONS"}, nil, setting.SMTP) AddPrivate(rt, "global/smtp", []string{"GET", "OPTIONS"}, nil, setting.SMTP)
AddPrivate(rt, "global/smtp", []string{"PUT", "OPTIONS"}, nil, setting.SetSMTP) 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{"GET", "OPTIONS"}, nil, setting.AuthConfig)
AddPrivate(rt, "global/auth", []string{"PUT", "OPTIONS"}, nil, setting.SetAuthConfig) AddPrivate(rt, "global/auth", []string{"PUT", "OPTIONS"}, nil, setting.SetAuthConfig)
AddPrivate(rt, "global/search/status", []string{"GET", "OPTIONS"}, nil, meta.SearchStatus) 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 := negroni.New()
n.Use(negroni.NewStatic(web.StaticAssetsFileSystem())) n.Use(negroni.NewStatic(web.StaticAssetsFileSystem()))
n.Use(negroni.HandlerFunc(cm.cors)) n.Use(negroni.HandlerFunc(cm.cors))
n.Use(negroni.HandlerFunc(cm.metrics))
n.UseHandler(router) n.UseHandler(router)
// tell caller we are ready to serve HTTP // tell caller we are ready to serve HTTP