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

Merge pull request #185 from documize/app-subscription

App subscription
This commit is contained in:
Harvey Kandola 2018-11-11 18:37:13 +00:00 committed by GitHub
commit cb9fd0940d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1715 additions and 1195 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
} }

73
core/env/product.go vendored
View file

@ -1,73 +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"
)
// ProdInfo describes a product
type ProdInfo struct {
Edition string
Title string
Version string
Major string
Minor string
Patch string
Revision int
License License
}
// License holds details of product license.
type License struct {
Name string `json:"name"`
Email string `json:"email"`
Edition string `json:"edition"`
Package string `json:"package"`
Plan string `json:"plan"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Seats int `json:"seats"`
Trial bool `json:"trial"`
Valid bool `json:"valid"`
}
// IsEmpty determines if we have a license.
func (l *License) IsEmpty() bool {
return l.Seats == 0 && 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() string {
lp := "populated"
if l.IsEmpty() {
lp = "empty"
}
lv := "invalid"
if l.Valid {
lv = "valid"
}
return fmt.Sprintf("License is %s and %s", lp, lv)
}
// IsValid returns if license is valid
func (l *License) IsValid() bool {
return l.Valid == true
}
// LicenseData holds encrypted data and is unpacked into License.
type LicenseData struct {
Key string `json:"key"`
Signature string `json:"signature"`
}

11
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 ProdInfo Product domain.Product
} }
const ( const (
@ -39,11 +40,3 @@ const (
// SiteModeBadDB redirects to db-error.html page // SiteModeBadDB redirects to db-error.html page
SiteModeBadDB = "3" SiteModeBadDB = "3"
) )
const (
// CommunityEdition is AGPL product variant
CommunityEdition = "Community"
// EnterpriseEdition is commercial licensed product variant
EnterpriseEdition = "Enterprise"
)

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

@ -23,6 +23,7 @@ func StripAuthSecrets(r *env.Runtime, provider, config string) string {
switch provider { switch provider {
case auth.AuthProviderDocumize: case auth.AuthProviderDocumize:
return config return config
case auth.AuthProviderKeycloak: case auth.AuthProviderKeycloak:
c := auth.KeycloakConfig{} c := auth.KeycloakConfig{}
err := json.Unmarshal([]byte(config), &c) err := json.Unmarshal([]byte(config), &c)
@ -41,6 +42,7 @@ func StripAuthSecrets(r *env.Runtime, provider, config string) string {
} }
return string(j) return string(j)
case auth.AuthProviderLDAP: case auth.AuthProviderLDAP:
c := auth.LDAPConfig{} c := auth.LDAPConfig{}
err := json.Unmarshal([]byte(config), &c) err := json.Unmarshal([]byte(config), &c)

View file

@ -21,6 +21,8 @@ package backup
// the file is deleted at the end of the process. // the file is deleted at the end of the process.
// //
// The backup file contains a manifest file that describes the backup. // The backup file contains a manifest file that describes the backup.
//
// TODO: explore writing straight to HTTP response via https://github.com/mholt/archiver
import ( import (
"archive/zip" "archive/zip"
@ -266,7 +268,10 @@ func (b backerHandler) dmzConfig(files *[]backupItem) (err error) {
if err != nil { if err != nil {
return return
} }
if b.Spec.SystemBackup() {
*files = append(*files, backupItem{Filename: "dmz_config.json", Content: content}) *files = append(*files, backupItem{Filename: "dmz_config.json", Content: content})
}
w := "" w := ""
if !b.Spec.SystemBackup() { if !b.Spec.SystemBackup() {

View file

@ -136,10 +136,12 @@ func (r *restoreHandler) PerformRestore(b []byte, l int64) (err error) {
} }
// Config. // Config.
if r.Context.GlobalAdmin {
err = r.dmzConfig() err = r.dmzConfig()
if err != nil { if err != nil {
return return
} }
}
// Audit Log. // Audit Log.
err = r.dmzAudit() err = r.dmzAudit()
@ -449,6 +451,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))
@ -1644,8 +1651,7 @@ func (r *restoreHandler) dmzUser() (err error) {
err = errors.Wrap(err, fmt.Sprintf("unable to check email %s", u[i].Email)) err = errors.Wrap(err, fmt.Sprintf("unable to check email %s", u[i].Email))
return return
} }
// Existing userID from database overrides all incoming userID values // Existing userID from database overrides all incoming userID values by using remapUser().
// by using remapUser().
if len(userID) > 0 { if len(userID) > 0 {
r.MapUserID[u[i].RefID] = userID r.MapUserID[u[i].RefID] = userID
insert = false insert = false

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() { 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 Administrator bool
Analytics bool Analytics bool
Active bool Active bool
Editor bool Editor bool
GlobalAdmin bool GlobalAdmin bool
ViewUsers bool ViewUsers bool
Subscription Subscription
} }
//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.Valid
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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }

318
domain/product.go Normal file
View file

@ -0,0 +1,318 @@
// 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
}
} else {
// First 10 is free for Enterprise edition.
if Seats1 == ctx.Subscription.Seats && time.Now().UTC().Before(ctx.Subscription.End) {
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 ( 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() { 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() { 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() { if !h.Runtime.Product.IsValid(ctx) {
response.WriteBadLicense(w) response.WriteBadLicense(w)
return return
} }

View file

@ -122,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 int) 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() { 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

@ -2,6 +2,7 @@ package test
import ( import (
"fmt" "fmt"
"github.com/documize/community/domain/store"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/domain" "github.com/documize/community/domain"
@ -13,31 +14,27 @@ import (
) )
// SetupTest prepares test environment // SetupTest prepares test environment
func SetupTest() (rt *env.Runtime, s *domain.Store, ctx domain.RequestContext) { func SetupTest() (rt *env.Runtime, s *store
.Store, ctx domain.RequestContext) {
rt, s = startRuntime() rt, s = startRuntime()
ctx = setupContext() ctx = setupContext()
return rt, s, ctx return rt, s, ctx
} }
func startRuntime() (rt *env.Runtime, s *domain.Store) { func startRuntime() (rt *env.Runtime, s *store.Store) {
rt = new(env.Runtime) rt = new(env.Runtime)
s = new(domain.Store) s = new(store.Store)
rt.Log = logging.NewLogger(false) rt.Log = logging.NewLogger(false)
web.Embed = embed.NewEmbedder() web.Embed = embed.NewEmbedder()
rt.Product = env.ProdInfo{} rt.Product = env.Product{}
rt.Product.Major = "0" rt.Product.Major = "0"
rt.Product.Minor = "0" rt.Product.Minor = "0"
rt.Product.Patch = "0" rt.Product.Patch = "0"
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.Valid = true
rt.Product.License.Trial = false
rt.Product.License.Edition = "Community"
// parse settings from command line and environment // parse settings from command line and environment
rt.Flags = env.ParseFlags() rt.Flags = env.ParseFlags()
@ -56,7 +53,7 @@ func setupContext() domain.RequestContext {
ctx.Administrator = true ctx.Administrator = true
ctx.Guest = false ctx.Guest = false
ctx.Editor = true ctx.Editor = true
ctx.Global = true ctx.GlobalAdmin = true
ctx.UserID = "test" ctx.UserID = "test"
ctx.OrgID = "test" ctx.OrgID = "test"
return ctx return ctx

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

@ -312,17 +312,11 @@ 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 int) { func (s Store) CountActiveUsers() (c []domain.SubscriptionUserAccount) {
row := s.Runtime.Db.QueryRow("SELECT count(*) FROM dmz_user WHERE c_refid IN (SELECT c_userid FROM dmz_user_account WHERE c_active=true)") 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 := row.Scan(&c)
if err == sql.ErrNoRows {
return 0
}
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
s.Runtime.Log.Error("CountActiveUsers", err) s.Runtime.Log.Error("CountActiveUsers", err)
return 0
} }
return return

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,8 +14,10 @@ package main
import ( import (
"fmt" "fmt"
"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"
@ -36,27 +38,27 @@ func main() {
web.Embed = embed.NewEmbedder() web.Embed = embed.NewEmbedder()
// product details // product details
rt.Product = env.ProdInfo{} 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 = 181111110016
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 = "Community" 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)
rt.Product.License = env.License{}
rt.Product.License.Seats = 1
rt.Product.License.Valid = true
rt.Product.License.Trial = false
rt.Product.License.Edition = "Community"
// setup store // Setup data store.
s := store.Store{} s := store.Store{}
// parse settings from command line and environment // Parse flags/envars.
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

@ -0,0 +1,39 @@
// 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
import $ from 'jquery';
import { inject as service } from '@ember/service';
import Notifier from '../../mixins/notifier';
import Component from '@ember/component';
export default Component.extend(Notifier, {
appMeta: service(),
global: service(),
changelog: '',
init() {
this._super(...arguments);
let self = this;
let cacheBuster = + new Date();
$.ajax({
url: `https://storage.googleapis.com/documize/news/summary.html?cb=${cacheBuster}`,
type: 'GET',
dataType: 'html',
success: function (response) {
self.set('changelog', response);
}
});
},
actions: {
}
});

View file

@ -9,29 +9,30 @@
// //
// https://documize.com // https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed'; import { empty } from '@ember/object/computed';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Notifier from '../../mixins/notifier'; import Notifier from '../../mixins/notifier';
import Modals from '../../mixins/modal';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend(Notifier, { export default Component.extend(Notifier, Modals, {
appMeta: service(), appMeta: service(),
global: service(), global: service(),
LicenseError: empty('license'), licenseError: empty('license'),
changelog: '', subscription: null,
planCloud: false,
planSelfhost: false,
init() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
this.get('global').getSubscription().then((subs) => {
let self = this; this.set('subscription', subs);
let cacheBuster = + new Date(); if (subs.plan === 'Installed') {
$.ajax({ this.set('planCloud', false);
url: `https://storage.googleapis.com/documize/news/summary.html?cb=${cacheBuster}`, this.set('planSelfhost', true);
type: 'GET', } else {
dataType: 'html', this.set('planCloud', true);
success: function (response) { this.set('planSelfhost', false);
self.set('changelog', response);
} }
}); });
}, },
@ -40,10 +41,17 @@ export default Component.extend(Notifier, {
saveLicense() { saveLicense() {
this.showWait(); this.showWait();
this.get('global').saveLicense(this.get('license')).then(() => { this.get('global').setLicense(this.get('license')).then(() => {
this.showDone(); this.showDone();
window.location.reload(); window.location.reload();
}); });
},
onDeactivate() {
this.get('global').deactivate().then(() => {
this.showDone();
this.modalOpen("#deactivation-modal", {"show": true});
});
} }
} }
}); });

View file

@ -13,9 +13,10 @@ import $ from 'jquery';
import { notEmpty } from '@ember/object/computed'; import { notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service' import { inject as service } from '@ember/service'
import ModalMixin from '../../mixins/modal'; import ModalMixin from '../../mixins/modal';
import TooltipMixin from '../../mixins/tooltip';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend(ModalMixin, { export default Component.extend(ModalMixin, TooltipMixin, {
classNames: ['layout-header', 'non-printable'], classNames: ['layout-header', 'non-printable'],
tagName: 'header', tagName: 'header',
folderService: service('folder'), folderService: service('folder'),
@ -69,6 +70,8 @@ export default Component.extend(ModalMixin, {
this.eventBus.subscribe('pinChange', this, 'setupPins'); this.eventBus.subscribe('pinChange', this, 'setupPins');
this.setupPins(); this.setupPins();
} }
this.renderTooltips();
}, },
setupPins() { setupPins() {
@ -87,6 +90,7 @@ export default Component.extend(ModalMixin, {
willDestroyElement() { willDestroyElement() {
this._super(...arguments); this._super(...arguments);
this.removeTooltips();
this.eventBus.unsubscribe('pinChange'); this.eventBus.unsubscribe('pinChange');
}, },
@ -113,6 +117,14 @@ export default Component.extend(ModalMixin, {
this.get('session').seenNewVersion(); this.get('session').seenNewVersion();
this.set('hasWhatsNew', false); this.set('hasWhatsNew', false);
} }
},
onBilling() {
if (!this.get('session.isAdmin')) {
return;
}
this.get('router').transitionTo('customize.billing');
} }
} }
}); });

View file

@ -141,6 +141,58 @@ let constants = EmberObject.extend({
MySQL: 'MySQL', MySQL: 'MySQL',
PostgreSQL: 'PostgreSQL', PostgreSQL: 'PostgreSQL',
}, },
// Product is where we try to balance the fine line between useful open core
// and the revenue-generating proprietary edition.
Product: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
// CommunityEdition is AGPL licensed open core of product.
CommunityEdition: 'Community',
// EnterpriseEdition is proprietary closed-source product.
EnterpriseEdition: 'Enterprise',
// PackageEssentials provides core capabilities.
PackageEssentials: "Essentials",
// PackageAdvanced provides analytics, reporting,
// content lifecycle, content verisoning, and audit logs.
PackageAdvanced: "Advanced",
// PackagePremium provides actions, feedback capture,
// approvals workflow, secure external sharing.
PackagePremium: "Premium",
// PackageDataCenter provides multi-tenanting
// and a bunch of professional services.
PackageDataCenter: "Data Center",
// PlanCloud represents *.documize.com hosting.
PlanCloud: "Cloud",
// PlanSelfHost represents privately hosted Documize instance.
PlanSelfHost: "Self-host",
// Seats0 is 0 users.
Seats0: 0,
// Seats1 is 10 users.
Seats1: 10,
// Seats2 is 25 users.
Seats2: 25,
//Seats3 is 50 users.
Seats3: 50,
// Seats4 is 100 users.
Seats4: 100,
//Seats5 is 250 users.
Seats5: 250,
// Seats6 is unlimited.
Seats6: 9999
}
}); });
export default { constants } export default { constants }

View file

@ -0,0 +1,32 @@
// 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
import { helper } from '@ember/component/helper';
export function formattedPrice(params) {
let pence = params[0];
if(is.not.number(pence)) {
return '$0'
}
let p = parseInt(pence);
if(p === 0) {
return '$0'
}
let a = pence / 100;
return `$` + a;
}
export default helper(formattedPrice);

View file

@ -1 +0,0 @@
{{customize/license-key license=model}}

View file

@ -10,8 +10,8 @@
// https://documize.com // https://documize.com
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import Route from '@ember/routing/route';
export default Route.extend(AuthenticatedRouteMixin, { export default Route.extend(AuthenticatedRouteMixin, {
appMeta: service(), appMeta: service(),
@ -19,16 +19,15 @@ export default Route.extend(AuthenticatedRouteMixin, {
global: service(), global: service(),
beforeModel() { beforeModel() {
if (!this.get("session.isGlobalAdmin")) { if (!this.get("session.isAdmin")) {
this.transitionTo('auth.login'); this.transitionTo('auth.login');
} }
}, },
model() { model() {
return this.get('global').getLicense();
}, },
activate() { activate() {
this.get('browser').setTitle('Product Licensing & Updates'); this.get('browser').setTitle('Product Changelog');
} }
}); });

View file

@ -0,0 +1 @@
{{customize/change-log}}

View file

@ -10,27 +10,49 @@
<div id="sidebar" class="sidebar"> <div id="sidebar" class="sidebar">
<h1>Settings</h1> <h1>Settings</h1>
<p>Configure authentication, SMTP, licensing and manage user accounts</p> <p>Configure authentication, SMTP, licensing and manage user accounts</p>
<ul class="tabnav-control tabnav-control-centered w-75"> <ul class="tabnav-control tabnav-control-centered w-75">
{{#link-to 'customize.general' activeClass='selected' class="tab tab-vertical" tagName="li" }}General{{/link-to}} {{#link-to 'customize.general' activeClass='selected' class="tab tab-vertical" tagName="li" }}General{{/link-to}}
{{#link-to 'customize.folders' activeClass='selected' class="tab tab-vertical" tagName="li" }}Spaces{{/link-to}}
{{#link-to 'customize.groups' activeClass='selected' class="tab tab-vertical" tagName="li" }}Groups{{/link-to}}
{{#link-to 'customize.users' activeClass='selected' class="tab tab-vertical" tagName="li" }}Users{{/link-to}}
{{#link-to 'customize.integrations' activeClass='selected' class="tab tab-vertical" tagName="li" }}Integrations{{/link-to}} {{#link-to 'customize.integrations' activeClass='selected' class="tab tab-vertical" tagName="li" }}Integrations{{/link-to}}
{{#if session.isGlobalAdmin}} {{#if session.isGlobalAdmin}}
{{#link-to 'customize.smtp' activeClass='selected' class="tab tab-vertical" tagName="li" }}SMTP{{/link-to}} {{#link-to 'customize.smtp' activeClass='selected' class="tab tab-vertical" tagName="li" }}Mail{{/link-to}}
{{/if}}
</ul>
<div class="mt-4" />
<ul class="tabnav-control tabnav-control-centered w-75">
{{#link-to 'customize.groups' activeClass='selected' class="tab tab-vertical" tagName="li" }}Groups{{/link-to}}
{{#link-to 'customize.users' activeClass='selected' class="tab tab-vertical" tagName="li" }}Users{{/link-to}}
{{#if session.isGlobalAdmin}}
{{#link-to 'customize.auth' activeClass='selected' class="tab tab-vertical" tagName="li" }}Authentication{{/link-to}} {{#link-to 'customize.auth' activeClass='selected' class="tab tab-vertical" tagName="li" }}Authentication{{/link-to}}
{{/if}}
</ul>
<div class="mt-4" />
<ul class="tabnav-control tabnav-control-centered w-75">
{{#link-to 'customize.folders' activeClass='selected' class="tab tab-vertical" tagName="li" }}Spaces{{/link-to}}
{{#if session.isGlobalAdmin}}
{{#link-to 'customize.search' activeClass='selected' class="tab tab-vertical" tagName="li" }}Search{{/link-to}} {{#link-to 'customize.search' activeClass='selected' class="tab tab-vertical" tagName="li" }}Search{{/link-to}}
{{#if (eq appMeta.edition 'Enterprise')}} {{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}
{{#link-to 'customize.audit' activeClass='selected' class="tab tab-vertical" tagName="li" }}Audit{{/link-to}} {{#link-to 'customize.audit' activeClass='selected' class="tab tab-vertical" tagName="li" }}Audit{{/link-to}}
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if (eq appMeta.edition 'Enterprise')}} {{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}
{{#link-to 'customize.archive' activeClass='selected' class="tab tab-vertical" tagName="li" }}Archive{{/link-to}} {{#link-to 'customize.archive' activeClass='selected' class="tab tab-vertical" tagName="li" }}Archive{{/link-to}}
{{/if}} {{/if}}
{{#if session.isGlobalAdmin}} </ul>
{{#link-to 'customize.license' activeClass='selected' class="tab tab-vertical" tagName="li" }}Product{{/link-to}}
<div class="mt-4" />
<ul class="tabnav-control tabnav-control-centered w-75">
{{#link-to 'customize.backup' activeClass='selected' class="tab tab-vertical" tagName="li" }}Backup // Restore{{/link-to}}
</ul>
<div class="mt-4" />
<ul class="tabnav-control tabnav-control-centered w-75">
{{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}
{{#link-to 'customize.billing' activeClass='selected' class="tab tab-vertical" tagName="li" }}Billing{{/link-to}}
{{/if}} {{/if}}
{{#link-to 'customize.backup' activeClass='selected' class="tab tab-vertical" tagName="li" }}Backup & Restore{{/link-to}} {{#link-to 'customize.product' activeClass='selected' class="tab tab-vertical" tagName="li" }}Changelog{{/link-to}}
</ul> </ul>
</div> </div>
{{/layout/middle-zone-sidebar}} {{/layout/middle-zone-sidebar}}

View file

@ -14,6 +14,11 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout
export default Route.extend(AuthenticatedRouteMixin, { export default Route.extend(AuthenticatedRouteMixin, {
beforeModel() { beforeModel() {
this.transitionTo('folders'); // this.transitionTo('folders');
},
activate: function () {
this._super(...arguments);
this.browser.setTitleWithoutSuffix('Aw, Snap!');
} }
}); });

View file

@ -1,2 +1,7 @@
<h1>Not found</h1> <div class="not-found">
<h1>Oops! That page couldn't be found.</h1>
<p>Maybe the content you're looking for is no longer available?</p>
<p>&nbsp;</p>
<a href="/">Return to Documize home page</a>
</div>
{{outlet}} {{outlet}}

View file

@ -80,8 +80,8 @@ export default Router.map(function () {
this.route('smtp', { this.route('smtp', {
path: 'smtp' path: 'smtp'
}); });
this.route('license', { this.route('product', {
path: 'license' path: 'product'
}); });
this.route('auth', { this.route('auth', {
path: 'auth' path: 'auth'
@ -101,6 +101,9 @@ export default Router.map(function () {
this.route('backup', { this.route('backup', {
path: 'backup' path: 'backup'
}); });
this.route('billing', {
path: 'billing'
});
} }
); );

View file

@ -17,6 +17,7 @@ import AjaxService from 'ember-ajax/services/ajax';
export default AjaxService.extend({ export default AjaxService.extend({
session: service(), session: service(),
localStorage: service(), localStorage: service(),
appMeta: service(),
host: config.apiHost, host: config.apiHost,
namespace: config.apiNamespace, namespace: config.apiNamespace,
@ -34,20 +35,27 @@ export default AjaxService.extend({
handleResponse(status, headers /*, payload*/) { handleResponse(status, headers /*, payload*/) {
try { try {
// Handle user permission changes.
let user = this.get('session.session.content.authenticated.user'); let user = this.get('session.session.content.authenticated.user');
let userUpdate = headers['x-documize-status']; let userUpdate = headers['x-documize-status'];
let appVersion = headers['x-documize-version']; let appVersion = headers['x-documize-version'];
// when unauthorized on local API AJAX calls, redirect to app root // Unauthorized local API AJAX calls redirect to app root.
if (status === 401 && is.not.undefined(appVersion) && is.not.include(window.location.href, '/auth')) { if (status === 401 && is.not.undefined(appVersion) && is.not.include(window.location.href, '/auth')) {
this.get('localStorage').clearAll(); this.get('localStorage').clearAll();
window.location.href = 'auth/login'; window.location.href = 'auth/login';
} }
// Handle billing/licensing issue.
if (status === 402 || headers['x-documize-subscription'] === 'false') {
this.set('appMeta.valid', false);
}
if (this.get('session.authenticated') && is.not.empty(userUpdate) && is.not.undefined(userUpdate)) { if (this.get('session.authenticated') && is.not.empty(userUpdate) && is.not.undefined(userUpdate)) {
let latest = JSON.parse(userUpdate); let latest = JSON.parse(userUpdate);
// Permission change means re-validation.
if (!latest.active || user.editor !== latest.editor || user.admin !== latest.admin || user.analytics !== latest.analytics || user.viewUsers !== latest.viewUsers) { if (!latest.active || user.editor !== latest.editor || user.admin !== latest.admin ||
user.analytics !== latest.analytics || user.viewUsers !== latest.viewUsers) {
this.get('localStorage').clearAll(); this.get('localStorage').clearAll();
window.location.href = 'auth/login'; window.location.href = 'auth/login';
} }

View file

@ -38,15 +38,10 @@ export default Service.extend({
secureMode: false, secureMode: false,
maxTags: 3, maxTags: 3,
storageProvider: '', storageProvider: '',
location: 'selfhost',
// for major.minor semver release detection
// 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() {
return this.valid === false;
},
getBaseUrl(endpoint) { getBaseUrl(endpoint) {
return [this.get('endpoint'), endpoint].join('/'); return [this.get('endpoint'), endpoint].join('/');
}, },

View file

@ -41,10 +41,23 @@ export default Service.extend({
} }
}, },
// Returns product subscription.
getSubscription() {
if(this.get('sessionService.isAdmin')) {
return this.get('ajax').request(`subscription`, {
method: 'GET',
dataType: 'JSON'
}).then((response) => {
return response;
});
}
},
// Returns product license. // Returns product license.
getLicense() { getLicense() {
if(this.get('sessionService.isGlobalAdmin')) { if(this.get('sessionService.isAdmin')) {
return this.get('ajax').request(`global/license`, { return this.get('ajax').request(`license`, {
method: 'GET', method: 'GET',
dataType: "text" dataType: "text"
}).then((response) => { }).then((response) => {
@ -53,10 +66,10 @@ export default Service.extend({
} }
}, },
// Saves product license. // Saves product subscription data.
saveLicense(license) { setLicense(license) {
if(this.get('sessionService.isGlobalAdmin')) { if(this.get('sessionService.isAdmin')) {
return this.get('ajax').request(`global/license`, { return this.get('ajax').request(`license`, {
method: 'PUT', method: 'PUT',
dataType: 'text', dataType: 'text',
data: license data: license
@ -221,5 +234,15 @@ export default Service.extend({
xhr.send(data); xhr.send(data);
}); });
},
deactivate() {
if(this.get('sessionService.isAdmin')) {
return this.get('ajax').request(`deactivate`, {
method: 'POST',
}).then(() => {
return;
});
} }
},
}); });

File diff suppressed because one or more lines are too long

View file

@ -25,3 +25,10 @@
color: $color-stroke; color: $color-stroke;
} }
} }
.not-found {
text-align: center;
font-size: 2rem;
color: $color-gray;
margin: 5rem 0;
}

View file

@ -222,4 +222,13 @@
} }
} }
} }
> .deactivation-zone {
@include border-radius(3px);
border: 1px solid $color-red;
margin: 30px 0;
padding: 20px 20px;
background-color: lighten($color-red, 60%);
color: $color-off-black;
}
} }

View file

@ -0,0 +1,37 @@
<div class="row">
<div class="col">
<div class="view-customize">
<h1 class="admin-heading">{{appMeta.edition}} Edition {{appMeta.version}}</h1>
<h2 class="sub-heading">Enterprise Edition unlocks
<a class="" href="https://documize.com/pricing">premium capabilities and product support</a>
</h2>
</div>
</div>
</div>
<div class="product-update">
<div class="update-summary">
{{#if appMeta.updateAvailable}}
<a href="https://documize.com/downloads" class="caption">New version available</a>
<p class="instructions">
To upgrade, replace existing binary and restart Documize. Migrate between Community and Enterprise editions seamlessly.
</p>
{{else}}
<div class="caption">Release Summary</div>
{{/if}}
<p>
<span class="color-off-black">Community Edition {{appMeta.communityLatest}}</span>&nbsp;&nbsp;&nbsp;
<a href="https://storage.googleapis.com/documize/downloads/documize-community-windows-amd64.exe" class="font-weight-bold">Windows</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-community-linux-amd64" class="font-weight-bold">Linux</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-community-darwin-amd64" class="font-weight-bold">macOS</a>&nbsp;
</p>
<p>
<span class="color-off-black">Enterprise Edition {{appMeta.enterpriseLatest}}</span>&nbsp;&nbsp;&nbsp;
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-windows-amd64.exe" class="font-weight-bold color-blue">Windows</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-linux-amd64" class="font-weight-bold color-blue">Linux</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-darwin-amd64" class="font-weight-bold color-blue">macOS</a>&nbsp;
</p>
<div class="my-5" />
{{{changelog}}}
</div>
</div>

View file

@ -1,69 +1,167 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="view-customize"> <div class="view-customize">
<h1 class="admin-heading">{{appMeta.edition}} Edition {{appMeta.version}}</h1> <h1 class="admin-heading">Product Billing & Subscription</h1>
<h2 class="sub-heading">Enterprise Edition unlocks <h2 class="sub-heading">Active subscription details</h2>
<a class="" href="https://documize.com/pricing">premium capabilities and product support</a>
</h2>
</div> </div>
</div> </div>
</div> </div>
<div class="view-customize"> <div class="view-customize">
<form class="mt-5 "> <form class="my-5 ">
<div class="form-group"> <div class="form-group row">
<label for="product-license-xml">Optional Enterprise Edition License Key</label> <label for="sub-name" class="col-sm-4 col-form-label"></label>
{{textarea id="product-license-xml" value=license rows="18" class=(if LicenseError 'form-control is-invalid' 'form-control')}} <div class="col-sm-7">
{{#if appMeta.valid}} {{#if (eq subscription.status 0)}}
{{#if (eq appMeta.edition "Enterprise")}} <h3 class="text-danger">Enjoy the Documize free plan!</h3>
<p class="mt-2 color-green">Registered to {{appMeta.license.email}} @ {{appMeta.license.name}}</p> <a class="btn btn-success" href="https://documize.com/checkout?ref=app&l={{appMeta.location}}&id={{subscription.id}}&o={{appMeta.orgId}}&u={{subscription.seats}}&a={{subscription.activeUsers}}&e={{subscription.email}}">upgrade now &xrarr;</a>
<p class="mt-2 color-green">{{appMeta.license.package}} package up to {{appMeta.license.seats}} users</p>
{{#if appMeta.license.trial}}
<p class="mt-2 color-red">Trial expiry {{formatted-date appMeta.license.end}}</p>
{{else}}
<p class="mt-2 color-green">Subscribed to {{formatted-date appMeta.license.end}}</p>
{{/if}} {{/if}}
{{else}} {{#if (eq subscription.status 1)}}
<small class="form-text text-muted">License key is XML format and activates Enterprise edition</small> <h3 class="text-success">Nice, you have an active product subscription!</h3>
<a class="btn btn-success" href="https://documize.com/checkout?ref=app&l={{appMeta.location}}&id={{subscription.id}}&o={{appMeta.orgId}}&u={{subscription.seats}}&a={{subscription.activeUsers}}&e={{subscription.email}}">
change plan &xrarr;
</a>
{{/if}} {{/if}}
{{else}} {{#if (eq subscription.status 2)}}
<p class="mt-2 color-red">License is not valid &mdash; check user count and expiry date</p> <h3 class="text-danger">Hmm, your product subscription has expired</h3>
<p class="mt-2 color-gray">Registered to {{appMeta.license.email}} @ {{appMeta.license.name}}</p> <a class="btn btn-success" href="https://documize.com/checkout?ref=app&l={{appMeta.location}}&id={{subscription.id}}&o={{appMeta.orgId}}&u={{subscription.seats}}&a={{subscription.activeUsers}}&e={{subscription.email}}">
<p class="mt-2 color-gray">{{appMeta.license.package}} package up to {{appMeta.license.seats}} users</p> renew &xrarr;
{{#if appMeta.license.trial}} </a>
<p class="mt-2 color-gray">Trial expiry {{formatted-date appMeta.license.end}}</p>
{{else}}
<p class="mt-2 color-gray">Subscribed to {{formatted-date appMeta.license.end}}</p>
{{/if}} {{/if}}
{{#if (eq subscription.status 3)}}
<h3 class="text-danger">Hmm, {{subscription.activeUsers}} active user count exceeds permitted {{subscription.seats}} user limit</h3>
<a class="btn btn-success" href="https://documize.com/checkout?ref=app&l={{appMeta.location}}&id={{subscription.id}}&o={{appMeta.orgId}}&u={{subscription.seats}}&a={{subscription.activeUsers}}&e={{subscription.email}}">
upgrade &xrarr;
</a>
{{/if}} {{/if}}
{{#if (eq subscription.status 4)}}
<h3 class="text-danger">Hmm, your product subscription is not valid</h3>
<a class="btn btn-success" href="https://documize.com/pricing?ref=app">upgrade &xrarr;</a>
{{/if}}
<br/>
<br/>
</div> </div>
<div class="btn btn-success mt-3" {{action 'saveLicense'}}>Save</div> </div>
<div class="form-group row">
<label for="sub-id" class="col-sm-4 col-form-label">Customer ID</label>
<div class="col-sm-7">
{{input id="sub-id" type="text" value=subscription.id class='form-control' readonly=true}}
<small class="form-text text-muted">Quote this ID when contacting us</small>
</div>
</div>
<div class="form-group row">
<label for="sub-name" class="col-sm-4 col-form-label">Customer Name</label>
<div class="col-sm-7">
{{input id="sub-name" type="text" value=subscription.name class='form-control' readonly=true}}
<small class="form-text text-muted">The business or personal name of our customer</small>
</div>
</div>
<div class="form-group row">
<label for="sub-email" class="col-sm-4 col-form-label">Contact Email</label>
<div class="col-sm-7">
{{input id="sub-email" type="email" value=subscription.email class='form-control' readonly=true}}
<small class="form-text text-muted">Where we will send product update and billing notices</small>
</div>
</div>
<div class="form-group row">
<label for="sub-seats" class="col-sm-4 col-form-label">Maximum Users</label>
<div class="col-sm-7">
{{input id="sub-seats" type="number" value=subscription.seats class='form-control' readonly=true}}
<small class="form-text text-muted">Your user pack size &mdash; you have {{subscription.activeUsers}} active users at the moment</small>
</div>
</div>
{{#if (eq appMeta.location 'selfhost')}}
<div class="form-group row">
<label for="sub-start" class="col-sm-4 col-form-label">Start Date</label>
<div class="col-sm-7">
{{input id="sub-start" type="text" value=(formatted-date subscription.start) class='form-control' readonly=true}}
<small class="form-text text-muted">When you (re)signed up</small>
</div>
</div>
<div class="form-group row">
<label for="sub-end" class="col-sm-4 col-form-label">Renewal Date</label>
<div class="col-sm-7">
{{input id="sub-end" type="text" value=(formatted-date subscription.end) class='form-control' readonly=true}}
<small class="form-text text-muted">The renewal date of your annual subscription</small>
</div>
</div>
<div class="form-group row">
<label for="sub-price" class="col-sm-4 col-form-label">Annual Billing</label>
<div class="col-sm-7">
{{input id="sub-price" type="text" value=(formatted-price subscription.price) class='form-control' readonly=true}}
<small class="form-text text-muted">What you pay each year</small>
</div>
</div>
{{else}}
<div class="form-group row">
<label for="sub-start" class="col-sm-4 col-form-label">Start of Billing</label>
<div class="col-sm-7">
{{input id="sub-start" type="text" value=(formatted-date subscription.start) class='form-control' readonly=true}}
<small class="form-text text-muted">When we first charged your credit card &mdash; charged every 30 days thereafter</small>
</div>
</div>
<div class="form-group row">
<label for="sub-price" class="col-sm-4 col-form-label">Monthly Amount</label>
<div class="col-sm-7">
{{input id="sub-price" type="text" value=(formatted-price subscription.price) class='form-control' readonly=true}}
<small class="form-text text-muted">What you pay us each month</small>
</div>
</div>
{{/if}}
{{#if subscription.trial}}
<div class="form-group row">
<label for="sub-trial" class="col-sm-4 col-form-label">Trial?</label>
<div class="col-sm-7">
{{input id="sub-trial" type="text" value="Yes" class='form-control' readonly=true}}
</div>
</div>
{{/if}}
{{#if (eq appMeta.location 'selfhost')}}
<div class="form-group row">
<label for="sub-license" class="col-sm-4 col-form-label">Activation Key</label>
<div class="col-sm-7">
{{focus-textarea id="sub-license" value=license rows="10" class=(if licenseError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">The activation key you received after sign-up</small>
<div class="btn btn-secondary mt-3" {{action 'saveLicense'}}>Activate</div>
</div>
</div>
{{/if}}
</form> </form>
</div> </div>
<div class="product-update"> {{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}
<div class="update-summary"> {{#if (eq appMeta.location 'cloud')}}
{{#if appMeta.updateAvailable}} <div class="row">
<a href="https://documize.com/downloads" class="caption">New version available</a> <div class="col-sm-4"></div>
<p class="instructions"> <div class="col-sm-7">
To upgrade, replace existing binary and restart Documize. Migrate between Community and Enterprise editions seamlessly. <div class="view-customize">
</p> <div class="deactivation-zone">
{{else}} <p>Let us know if you would like to close your account or cancel your subscription.</p>
<div class="caption">Release Summary</div> <p><span class="font-weight-bold">WARNING: </span>All data will be deleted so please download a complete backup of all your data.</p>
<p>Requests can take up to 24 hours to process.</p>
{{#link-to 'customize.backup' class="btn btn-success"}}PERFORM BACKUP{{/link-to}}
<div class="button-gap" />
<button class="btn btn-danger" {{action 'onDeactivate'}}>REQUEST ACCOUNT CLOSURE</button>
</div>
</div>
</div>
</div>
{{/if}} {{/if}}
<p> {{/if}}
<span class="color-off-black">Community Edition {{appMeta.communityLatest}}</span>&nbsp;&nbsp;&nbsp;
<a href="https://storage.googleapis.com/documize/downloads/documize-community-windows-amd64.exe" class="font-weight-bold">Windows</a>&nbsp;&middot; <div id="deactivation-modal" class="modal" tabindex="-1" role="dialog">
<a href="https://storage.googleapis.com/documize/downloads/documize-community-linux-amd64" class="font-weight-bold">Linux</a>&nbsp;&middot; <div class="modal-dialog" role="document">
<a href="https://storage.googleapis.com/documize/downloads/documize-community-darwin-amd64" class="font-weight-bold">macOS</a>&nbsp; <div class="modal-content">
</p> <div class="modal-header">Deactivation Requested</div>
<p> <div class="modal-body">
<span class="color-off-black">Enterprise Edition {{appMeta.enterpriseLatest}}</span>&nbsp;&nbsp;&nbsp; <p>Your request has been sent and will be processed shortly.</p>
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-windows-amd64.exe" class="font-weight-bold color-blue">Windows</a>&nbsp;&middot; <p>If you haven't already, perform a backup to download all your data.</p>
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-linux-amd64" class="font-weight-bold color-blue">Linux</a>&nbsp;&middot; </div>
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-darwin-amd64" class="font-weight-bold color-blue">macOS</a>&nbsp; <div class="modal-footer">
</p> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<div class="my-5" /> </div>
{{{changelog}}} </div>
</div> </div>
</div> </div>

View file

@ -7,12 +7,12 @@
<li class="item cursor-auto"> <li class="item cursor-auto">
<img class="logo" src="/assets/img/icon-white-64x64.png" /> <img class="logo" src="/assets/img/icon-white-64x64.png" />
</li> </li>
{{#if (eq appMeta.edition 'Community')}} {{#if (eq appMeta.edition constants.Product.CommunityEdition)}}
<li class="item"> <li class="item">
{{#link-to "folders" class=(if (eq selectItem 'spaces') 'link selected' 'link')}}SPACES{{/link-to}} {{#link-to "folders" class=(if (eq selectItem 'spaces') 'link selected' 'link')}}SPACES{{/link-to}}
</li> </li>
{{/if}} {{/if}}
{{#if (eq appMeta.edition 'Enterprise')}} {{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}
{{#if session.viewDashboard}} {{#if session.viewDashboard}}
<li class="item"> <li class="item">
{{#link-to "dashboard" class=(if (eq selectItem 'dashboard') 'link selected' 'link')}}ACTIONS{{/link-to}} {{#link-to "dashboard" class=(if (eq selectItem 'dashboard') 'link selected' 'link')}}ACTIONS{{/link-to}}
@ -36,10 +36,10 @@
<i class="material-icons">menu</i> <i class="material-icons">menu</i>
</div> </div>
<div class="dropdown-menu" aria-labelledby="top-nav-hamburger"> <div class="dropdown-menu" aria-labelledby="top-nav-hamburger">
{{#if (eq appMeta.edition 'Community')}} {{#if (eq appMeta.edition constants.Product.CommunityEdition)}}
{{#link-to "folders" class="dropdown-item"}}Spaces{{/link-to}} {{#link-to "folders" class="dropdown-item"}}Spaces{{/link-to}}
{{/if}} {{/if}}
{{#if (eq appMeta.edition 'Enterprise')}} {{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}
{{#link-to "folders" class="dropdown-item"}}Spaces{{/link-to}} {{#link-to "folders" class="dropdown-item"}}Spaces{{/link-to}}
{{#if session.viewDashboard}} {{#if session.viewDashboard}}
{{#link-to "dashboard" class="dropdown-item"}}Actions{{/link-to}} {{#link-to "dashboard" class="dropdown-item"}}Actions{{/link-to}}
@ -55,12 +55,22 @@
<div class="col col-6 col-md-3"> <div class="col col-6 col-md-3">
<div class="top-bar"> <div class="top-bar">
<div class="buttons d-flex flex-wrap align-items-center"> <div class="buttons d-flex flex-wrap align-items-center">
{{#unless appMeta.valid}}
<div class="btn-group">
<div class="button-icon-gold animated infinite wobble slow delay-2s"
data-toggle="tooltip" data-placement="bottom" title="Please select product plan"
{{action 'onBilling'}}>
<i class="material-icons">report</i>
</div>
</div>
<div class="button-icon-gap" />
{{/unless}}
<div class="btn-group"> <div class="btn-group">
{{#link-to "search" class="button-icon-white" }} {{#link-to "search" class="button-icon-white" }}
<i class="material-icons">search</i> <i class="material-icons">search</i>
{{/link-to}} {{/link-to}}
</div> </div>
{{#if session.authenticated}} {{#if session.authenticated}}
{{#if hasPins}} {{#if hasPins}}
<div class="button-icon-gap" /> <div class="button-icon-gap" />
@ -100,14 +110,17 @@
</div> </div>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="profile-button"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="profile-button">
{{#if session.isAdmin}} {{#if session.isAdmin}}
{{#link-to 'customize.general' class="dropdown-item" }}Settings{{/link-to}} {{#link-to 'customize.general' class="dropdown-item"}}Settings{{/link-to}}
{{#unless appMeta.valid}}
{{#link-to 'customize.billing' class="dropdown-item font-weight-bold color-red"}}Update Billing{{/link-to}}
{{/unless}}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
{{/if}} {{/if}}
{{#link-to 'profile' class="dropdown-item" }}Profile{{/link-to}} {{#link-to 'profile' class="dropdown-item" }}Profile{{/link-to}}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
{{#if session.isGlobalAdmin}} {{#if session.isGlobalAdmin}}
{{#if appMeta.updateAvailable}} {{#if appMeta.updateAvailable}}
{{#link-to 'customize.license' class="dropdown-item font-weight-bold color-orange" }}Update available{{/link-to}} {{#link-to 'customize.product' class="dropdown-item font-weight-bold color-orange" }}Update available{{/link-to}}
{{/if}} {{/if}}
{{/if}} {{/if}}
<a href="#" class="dropdown-item {{if hasWhatsNew 'color-whats-new font-weight-bold'}}" {{action 'onShowWhatsNewModal'}}>What's New</a> <a href="#" class="dropdown-item {{if hasWhatsNew 'color-whats-new font-weight-bold'}}" {{action 'onShowWhatsNewModal'}}>What's New</a>
@ -178,7 +191,7 @@
<div class="dotcom"> <div class="dotcom">
<a href="https://documize.com">https://documize.com</a> <a href="https://documize.com">https://documize.com</a>
</div> </div>
{{#if (eq appMeta.edition 'Community')}} {{#if (eq appMeta.edition constants.Product.CommunityEdition)}}
<div class="copyright"> <div class="copyright">
&copy; Documize Inc. All rights reserved. &copy; Documize Inc. All rights reserved.
</div> </div>

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 string `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.
@ -38,9 +39,8 @@ type SiteMeta struct {
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 string `json:"edition"` Edition domain.Edition `json:"edition"`
Valid bool `json:"valid"`
ConversionEndpoint string `json:"conversionEndpoint"` ConversionEndpoint string `json:"conversionEndpoint"`
License env.License `json:"license"`
Storage env.StoreType `json:"storageProvider"` 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" 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 status
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