mirror of
https://github.com/documize/community.git
synced 2025-07-18 20:59:43 +02:00
major code repair from old to new API -- WIP
This commit is contained in:
parent
25b576f861
commit
792c3e2ce8
46 changed files with 3403 additions and 171 deletions
|
@ -8,7 +8,7 @@ The mission is to bring software dev inspired features (refactoring, testing, li
|
|||
|
||||
## Latest version
|
||||
|
||||
v1.50.2
|
||||
v1.51.0
|
||||
|
||||
## OS Support
|
||||
|
||||
|
|
|
@ -24,10 +24,10 @@ import (
|
|||
"github.com/documize/community/core/api/entity"
|
||||
"github.com/documize/community/core/api/request"
|
||||
"github.com/documize/community/core/api/util"
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/log"
|
||||
"github.com/documize/community/core/secrets"
|
||||
"github.com/documize/community/domain/section/provider"
|
||||
"github.com/documize/community/server/web"
|
||||
)
|
||||
|
||||
// Authenticate user based up HTTP Authorization header.
|
||||
|
@ -98,7 +98,7 @@ func Authenticate(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Attach user accounts and work out permissions
|
||||
attachUserAccounts(p, org.RefID, &user)
|
||||
AttachUserAccounts(p, org.RefID, &user)
|
||||
|
||||
// active check
|
||||
|
||||
|
@ -201,7 +201,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
|||
|
||||
// Fetch user permissions for this org
|
||||
if context.Authenticated {
|
||||
user, err := getSecuredUser(p, org.RefID, context.UserID)
|
||||
user, err := GetSecuredUser(p, org.RefID, context.UserID)
|
||||
|
||||
if err != nil {
|
||||
writeServerError(w, method, err)
|
||||
|
@ -242,8 +242,6 @@ func Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
|||
|
||||
// ValidateAuthToken finds and validates authentication token.
|
||||
func ValidateAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Info("cb gh")
|
||||
// TODO should this go after token validation?
|
||||
if s := r.URL.Query().Get("section"); s != "" {
|
||||
if err := provider.Callback(s, w, r); err != nil {
|
||||
|
@ -325,7 +323,7 @@ func ValidateAuthToken(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
user, err := getSecuredUser(p, org.RefID, context.UserID)
|
||||
user, err := GetSecuredUser(p, org.RefID, context.UserID)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
@ -349,7 +347,7 @@ func preAuthorizeStaticAssets(r *http.Request) bool {
|
|||
strings.ToLower(r.URL.Path) == "/robots.txt" ||
|
||||
strings.ToLower(r.URL.Path) == "/version" ||
|
||||
strings.HasPrefix(strings.ToLower(r.URL.Path), "/api/public/") ||
|
||||
((api.Runtime.Flags.SiteMode == web.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) {
|
||||
((api.Runtime.Flags.SiteMode == env.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) {
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ func AuthenticateKeycloak(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Attach user accounts and work out permissions.
|
||||
attachUserAccounts(p, org.RefID, &user)
|
||||
AttachUserAccounts(p, org.RefID, &user)
|
||||
|
||||
// No accounts signals data integrity problem
|
||||
// so we reject login request.
|
||||
|
@ -301,7 +301,7 @@ func addUser(p request.Persister, u *entity.User, addSpace bool) (err error) {
|
|||
return err
|
||||
}
|
||||
} else {
|
||||
attachUserAccounts(p, p.Context.OrgID, &userDupe)
|
||||
AttachUserAccounts(p, p.Context.OrgID, &userDupe)
|
||||
|
||||
for _, a := range userDupe.Accounts {
|
||||
if a.OrgID == p.Context.OrgID {
|
||||
|
|
|
@ -136,7 +136,7 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
log.Info("Adding user")
|
||||
} else {
|
||||
attachUserAccounts(p, p.Context.OrgID, &userDupe)
|
||||
AttachUserAccounts(p, p.Context.OrgID, &userDupe)
|
||||
|
||||
for _, a := range userDupe.Accounts {
|
||||
if a.OrgID == p.Context.OrgID {
|
||||
|
@ -206,7 +206,7 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Send back new user record
|
||||
userModel, err = getSecuredUser(p, p.Context.OrgID, userID)
|
||||
userModel, err = GetSecuredUser(p, p.Context.OrgID, userID)
|
||||
|
||||
json, err := json.Marshal(userModel)
|
||||
if err != nil {
|
||||
|
@ -254,7 +254,7 @@ func GetOrganizationUsers(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
for i := range users {
|
||||
attachUserAccounts(p, p.Context.OrgID, &users[i])
|
||||
AttachUserAccounts(p, p.Context.OrgID, &users[i])
|
||||
}
|
||||
|
||||
json, err := json.Marshal(users)
|
||||
|
@ -333,7 +333,7 @@ func GetUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
user, err := getSecuredUser(p, p.Context.OrgID, userID)
|
||||
user, err := GetSecuredUser(p, p.Context.OrgID, userID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
writeNotFoundError(w, method, userID)
|
||||
|
@ -719,13 +719,14 @@ func ResetUserPassword(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Get user object contain associated accounts but credentials are wiped.
|
||||
func getSecuredUser(p request.Persister, orgID, user string) (u entity.User, err error) {
|
||||
func GetSecuredUser(p request.Persister, orgID, user string) (u entity.User, err error) {
|
||||
u, err = p.GetUser(user)
|
||||
attachUserAccounts(p, orgID, &u)
|
||||
AttachUserAccounts(p, orgID, &u)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func attachUserAccounts(p request.Persister, orgID string, user *entity.User) {
|
||||
func AttachUserAccounts(p request.Persister, orgID string, user *entity.User) {
|
||||
user.ProtectSecrets()
|
||||
a, err := p.GetUserAccounts(user.RefID)
|
||||
|
||||
|
|
|
@ -19,9 +19,9 @@ import (
|
|||
|
||||
"github.com/documize/community/core/api"
|
||||
"github.com/documize/community/core/api/entity"
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/log"
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/server/web"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
|
@ -83,7 +83,7 @@ func (p *Persister) GetOrganizationByDomain(subdomain string) (org entity.Organi
|
|||
err = nil
|
||||
subdomain = strings.ToLower(subdomain)
|
||||
|
||||
if api.Runtime.Flags.SiteMode == web.SiteModeNormal { // only return an organization when running normally
|
||||
if api.Runtime.Flags.SiteMode == env.SiteModeNormal { // only return an organization when running normally
|
||||
|
||||
var stmt *sqlx.Stmt
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ func Check(runtime *env.Runtime) bool {
|
|||
if err != nil {
|
||||
runtime.Log.Error("Can't get MySQL configuration", err)
|
||||
web.SiteInfo.Issue = "Can't get MySQL configuration: " + err.Error()
|
||||
runtime.Flags.SiteMode = web.SiteModeBadDB
|
||||
runtime.Flags.SiteMode = env.SiteModeBadDB
|
||||
return false
|
||||
}
|
||||
defer streamutil.Close(rows)
|
||||
|
@ -65,7 +65,7 @@ func Check(runtime *env.Runtime) bool {
|
|||
if err != nil {
|
||||
runtime.Log.Error("no MySQL configuration returned", err)
|
||||
web.SiteInfo.Issue = "no MySQL configuration return issue: " + err.Error()
|
||||
runtime.Flags.SiteMode = web.SiteModeBadDB
|
||||
runtime.Flags.SiteMode = env.SiteModeBadDB
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ func Check(runtime *env.Runtime) bool {
|
|||
want := fmt.Sprintf("%d.%d.%d", verInts[0], verInts[1], verInts[2])
|
||||
runtime.Log.Error("MySQL version element "+strconv.Itoa(k+1)+" of '"+version+"' not high enough, need at least version "+want, errors.New("bad MySQL version"))
|
||||
web.SiteInfo.Issue = "MySQL version element " + strconv.Itoa(k+1) + " of '" + version + "' not high enough, need at least version " + want
|
||||
runtime.Flags.SiteMode = web.SiteModeBadDB
|
||||
runtime.Flags.SiteMode = env.SiteModeBadDB
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -101,13 +101,13 @@ func Check(runtime *env.Runtime) bool {
|
|||
if charset != "utf8" {
|
||||
runtime.Log.Error("MySQL character set not utf8:", errors.New(charset))
|
||||
web.SiteInfo.Issue = "MySQL character set not utf8: " + charset
|
||||
runtime.Flags.SiteMode = web.SiteModeBadDB
|
||||
runtime.Flags.SiteMode = env.SiteModeBadDB
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(collation, "utf8") {
|
||||
runtime.Log.Error("MySQL collation sequence not utf8...:", errors.New(collation))
|
||||
web.SiteInfo.Issue = "MySQL collation sequence not utf8...: " + collation
|
||||
runtime.Flags.SiteMode = web.SiteModeBadDB
|
||||
runtime.Flags.SiteMode = env.SiteModeBadDB
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -119,12 +119,12 @@ func Check(runtime *env.Runtime) bool {
|
|||
`' and TABLE_TYPE='BASE TABLE'`); err != nil {
|
||||
runtime.Log.Error("Can't get MySQL number of tables", err)
|
||||
web.SiteInfo.Issue = "Can't get MySQL number of tables: " + err.Error()
|
||||
runtime.Flags.SiteMode = web.SiteModeBadDB
|
||||
runtime.Flags.SiteMode = env.SiteModeBadDB
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(flds[0]) == "0" {
|
||||
runtime.Log.Info("Entering database set-up mode because the database is empty.....")
|
||||
runtime.Flags.SiteMode = web.SiteModeSetup
|
||||
runtime.Flags.SiteMode = env.SiteModeSetup
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -140,13 +140,13 @@ func Check(runtime *env.Runtime) bool {
|
|||
if err := runtime.Db.Select(&dummy, "SELECT 1 FROM "+table+" LIMIT 1;"); err != nil {
|
||||
runtime.Log.Error("Entering bad database mode because: SELECT 1 FROM "+table+" LIMIT 1;", err)
|
||||
web.SiteInfo.Issue = "MySQL database is not empty, but does not contain table: " + table
|
||||
runtime.Flags.SiteMode = web.SiteModeBadDB
|
||||
runtime.Flags.SiteMode = env.SiteModeBadDB
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime.Flags.SiteMode = web.SiteModeNormal // actually no need to do this (as already ""), this for documentation
|
||||
runtime.Flags.SiteMode = env.SiteModeNormal // actually no need to do this (as already ""), this for documentation
|
||||
web.SiteInfo.DBname = "" // do not give this info when not in set-up mode
|
||||
dbCheckOK = true
|
||||
return true
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/documize/community/core/api"
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/log"
|
||||
"github.com/documize/community/core/secrets"
|
||||
"github.com/documize/community/core/stringutil"
|
||||
|
@ -65,7 +66,7 @@ func Create(w http.ResponseWriter, r *http.Request) {
|
|||
target := "/setup"
|
||||
status := http.StatusBadRequest
|
||||
|
||||
if api.Runtime.Flags.SiteMode == web.SiteModeNormal {
|
||||
if api.Runtime.Flags.SiteMode == env.SiteModeNormal {
|
||||
target = "/"
|
||||
status = http.StatusOK
|
||||
}
|
||||
|
@ -133,7 +134,7 @@ func Create(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
api.Runtime.Flags.SiteMode = web.SiteModeNormal
|
||||
api.Runtime.Flags.SiteMode = env.SiteModeNormal
|
||||
}
|
||||
|
||||
// The result of completing the onboarding process.
|
||||
|
|
5
core/env/product.go
vendored
5
core/env/product.go
vendored
|
@ -58,6 +58,11 @@ func (l *License) Status() string {
|
|||
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"`
|
||||
|
|
11
core/env/runtime.go
vendored
11
core/env/runtime.go
vendored
|
@ -22,3 +22,14 @@ type Runtime struct {
|
|||
Log Logger
|
||||
Product ProdInfo
|
||||
}
|
||||
|
||||
const (
|
||||
// SiteModeNormal serves app
|
||||
SiteModeNormal = ""
|
||||
// SiteModeOffline serves offline.html
|
||||
SiteModeOffline = "1"
|
||||
// SiteModeSetup tells Ember to serve setup route
|
||||
SiteModeSetup = "2"
|
||||
// SiteModeBadDB redirects to db-error.html page
|
||||
SiteModeBadDB = "3"
|
||||
)
|
||||
|
|
|
@ -18,6 +18,12 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Param returns the requested paramater from route request.
|
||||
func Param(r *http.Request, p string) string {
|
||||
params := mux.Vars(r)
|
||||
return params[p]
|
||||
}
|
||||
|
||||
// Params returns the paramaters from route request.
|
||||
func Params(r *http.Request) map[string]string {
|
||||
return mux.Vars(r)
|
||||
|
|
|
@ -14,10 +14,7 @@ package response
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/documize/community/core/log"
|
||||
)
|
||||
|
||||
// Helper for writing consistent headers back to HTTP client
|
||||
|
@ -29,55 +26,43 @@ func writeStatus(w http.ResponseWriter, status int) {
|
|||
// WriteMissingDataError notifies HTTP client of missing data in request.
|
||||
func WriteMissingDataError(w http.ResponseWriter, method, parameter string) {
|
||||
writeStatus(w, http.StatusBadRequest)
|
||||
_, err := w.Write([]byte("{Error: 'Missing data'}"))
|
||||
log.IfErr(err)
|
||||
log.Info(fmt.Sprintf("Missing data %s for method %s", parameter, method))
|
||||
w.Write([]byte("{Error: 'Missing data'}"))
|
||||
}
|
||||
|
||||
// WriteNotFoundError notifies HTTP client of 'record not found' error.
|
||||
func WriteNotFoundError(w http.ResponseWriter, method string, id string) {
|
||||
writeStatus(w, http.StatusNotFound)
|
||||
_, err := w.Write([]byte("{Error: 'Not found'}"))
|
||||
log.IfErr(err)
|
||||
log.Info(fmt.Sprintf("Not found ID %s for method %s", id, method))
|
||||
w.Write([]byte("{Error: 'Not found'}"))
|
||||
}
|
||||
|
||||
// WriteServerError notifies HTTP client of general application error.
|
||||
func WriteServerError(w http.ResponseWriter, method string, err error) {
|
||||
writeStatus(w, http.StatusBadRequest)
|
||||
_, err2 := w.Write([]byte("{Error: 'Internal server error'}"))
|
||||
log.IfErr(err2)
|
||||
log.Error(fmt.Sprintf("Internal server error for method %s", method), err)
|
||||
w.Write([]byte("{Error: 'Internal server error'}"))
|
||||
}
|
||||
|
||||
// WriteDuplicateError notifies HTTP client of duplicate data that has been rejected.
|
||||
func WriteDuplicateError(w http.ResponseWriter, method, entity string) {
|
||||
writeStatus(w, http.StatusConflict)
|
||||
_, err := w.Write([]byte("{Error: 'Duplicate record'}"))
|
||||
log.IfErr(err)
|
||||
log.Info(fmt.Sprintf("Duplicate %s record detected for method %s", entity, method))
|
||||
w.Write([]byte("{Error: 'Duplicate record'}"))
|
||||
}
|
||||
|
||||
// WriteUnauthorizedError notifies HTTP client of rejected unauthorized request.
|
||||
func WriteUnauthorizedError(w http.ResponseWriter) {
|
||||
writeStatus(w, http.StatusUnauthorized)
|
||||
_, err := w.Write([]byte("{Error: 'Unauthorized'}"))
|
||||
log.IfErr(err)
|
||||
w.Write([]byte("{Error: 'Unauthorized'}"))
|
||||
}
|
||||
|
||||
// WriteForbiddenError notifies HTTP client of request that is not allowed.
|
||||
func WriteForbiddenError(w http.ResponseWriter) {
|
||||
writeStatus(w, http.StatusForbidden)
|
||||
_, err := w.Write([]byte("{Error: 'Forbidden'}"))
|
||||
log.IfErr(err)
|
||||
w.Write([]byte("{Error: 'Forbidden'}"))
|
||||
}
|
||||
|
||||
// WriteBadRequestError notifies HTTP client of rejected request due to bad data within request.
|
||||
func WriteBadRequestError(w http.ResponseWriter, method, message string) {
|
||||
writeStatus(w, http.StatusBadRequest)
|
||||
_, err := w.Write([]byte("{Error: 'Bad Request'}"))
|
||||
log.IfErr(err)
|
||||
log.Info(fmt.Sprintf("Bad Request %s for method %s", message, method))
|
||||
w.Write([]byte("{Error: 'Bad Request'}"))
|
||||
}
|
||||
|
||||
// WriteBadLicense notifies HTTP client of invalid license (402)
|
||||
|
@ -87,44 +72,34 @@ func WriteBadLicense(w http.ResponseWriter) {
|
|||
var e struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
e.Reason = "invalid or expired Documize license"
|
||||
|
||||
j, _ := json.Marshal(e)
|
||||
_, err := w.Write(j)
|
||||
log.IfErr(err)
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
// WriteBytes dumps bytes to HTTP response
|
||||
func WriteBytes(w http.ResponseWriter, data []byte) {
|
||||
writeStatus(w, http.StatusOK)
|
||||
_, err := w.Write(data)
|
||||
log.IfErr(err)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// WriteString writes string to HTTP response
|
||||
func WriteString(w http.ResponseWriter, data string) {
|
||||
writeStatus(w, http.StatusOK)
|
||||
_, err := w.Write([]byte(data))
|
||||
log.IfErr(err)
|
||||
w.Write([]byte(data))
|
||||
}
|
||||
|
||||
// WriteEmpty writes empty JSON HTTP response
|
||||
func WriteEmpty(w http.ResponseWriter) {
|
||||
writeStatus(w, http.StatusOK)
|
||||
_, err := w.Write([]byte("{}"))
|
||||
log.IfErr(err)
|
||||
w.Write([]byte("{}"))
|
||||
}
|
||||
|
||||
// WriteJSON serializes data as JSON to HTTP response.
|
||||
func WriteJSON(w http.ResponseWriter, v interface{}) {
|
||||
writeStatus(w, http.StatusOK)
|
||||
|
||||
j, err := json.Marshal(v)
|
||||
|
||||
if err != nil {
|
||||
log.IfErr(err)
|
||||
}
|
||||
|
||||
_, err = w.Write(j)
|
||||
log.IfErr(err)
|
||||
j, _ := json.Marshal(v)
|
||||
w.Write(j)
|
||||
}
|
||||
|
|
42
core/timeutil/nulltime.go
Normal file
42
core/timeutil/nulltime.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Source: https://github.com/lib/pq/blob/b269bd035a727d6c1081f76e7a239a1b00674c40/encode.go#L521
|
||||
//
|
||||
// Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
// Package timeutil provides date and time related types and helpers.
|
||||
package timeutil
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NullTime represents a time.Time that may be null. NullTime implements the
|
||||
// sql.Scanner interface so it can be used as a scan destination, similar to
|
||||
// sql.NullString.
|
||||
type NullTime struct {
|
||||
Time time.Time
|
||||
Valid bool // Valid is true if Time is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (nt *NullTime) Scan(value interface{}) error {
|
||||
nt.Time, nt.Valid = value.(time.Time)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (nt NullTime) Value() (driver.Value, error) {
|
||||
if !nt.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return nt.Time, nil
|
||||
}
|
36
domain/account/model.go
Normal file
36
domain/account/model.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
// 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 account
|
||||
|
||||
import (
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
)
|
||||
|
||||
// Handler contains the runtime information such as logging and database.
|
||||
type Handler struct {
|
||||
Runtime env.Runtime
|
||||
}
|
||||
|
||||
// Account links a User to an Organization.
|
||||
type Account struct {
|
||||
domain.BaseEntity
|
||||
Admin bool `json:"admin"`
|
||||
Editor bool `json:"editor"`
|
||||
UserID string `json:"userId"`
|
||||
OrgID string `json:"orgId"`
|
||||
Company string `json:"company"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Domain string `json:"domain"`
|
||||
Active bool `json:"active"`
|
||||
}
|
144
domain/account/store.go
Normal file
144
domain/account/store.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/store/mysql"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Add inserts the given record into the datbase account table.
|
||||
func Add(s domain.StoreContext, account Account) (err error) {
|
||||
account.Created = time.Now().UTC()
|
||||
account.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.Preparex("INSERT INTO account (refid, orgid, userid, admin, editor, active, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to prepare insert for account")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(account.RefID, account.OrgID, account.UserID, account.Admin, account.Editor, account.Active, account.Created, account.Revised)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to execute insert for account")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserAccount returns the database account record corresponding to the given userID, using the client's current organizaion.
|
||||
func GetUserAccount(s domain.StoreContext, userID string) (account Account, err error) {
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT a.*, b.company, b.title, b.message, b.domain FROM account a, organization b WHERE b.refid=a.orgid and a.orgid=? and a.userid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare select for account by user %s", userID))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&account, s.Context.OrgID, userID)
|
||||
if err != sql.ErrNoRows && err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute select for account by user %s", userID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserAccounts returns a slice of database account records, for all organizations that the userID is a member of, in organization title order.
|
||||
func GetUserAccounts(s domain.StoreContext, userID string) (t []Account, err error) {
|
||||
err = s.Runtime.Db.Select(&t, "SELECT a.*, b.company, b.title, b.message, b.domain FROM account a, organization b WHERE a.userid=? AND a.orgid=b.refid AND a.active=1 ORDER BY b.title", userID)
|
||||
|
||||
if err != sql.ErrNoRows && err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("Unable to execute select account for user %s", userID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetAccountsByOrg returns a slice of database account records, for all users in the client's organization.
|
||||
func GetAccountsByOrg(s domain.StoreContext) (t []Account, err error) {
|
||||
err = s.Runtime.Db.Select(&t, "SELECT a.*, b.company, b.title, b.message, b.domain FROM account a, organization b WHERE a.orgid=b.refid AND a.orgid=? AND a.active=1", s.Context.OrgID)
|
||||
|
||||
if err != sql.ErrNoRows && err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute select account for org %s", s.Context.OrgID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CountOrgAccounts returns the numnber of active user accounts for specified organization.
|
||||
func CountOrgAccounts(s domain.StoreContext) (c int) {
|
||||
row := s.Runtime.Db.QueryRow("SELECT count(*) FROM account WHERE orgid=? AND active=1", s.Context.OrgID)
|
||||
|
||||
err := row.Scan(&c)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return 0
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "count org accounts")
|
||||
return 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateAccount updates the database record for the given account to the given values.
|
||||
func UpdateAccount(s domain.StoreContext, account Account) (err error) {
|
||||
account.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE account SET userid=:userid, admin=:admin, editor=:editor, active=:active, revised=:revised WHERE orgid=:orgid AND refid=:refid")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare update for account %s", account.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&account)
|
||||
if err != sql.ErrNoRows && err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute update for account %s", account.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// HasOrgAccount returns if the given orgID has valid userID.
|
||||
func HasOrgAccount(s domain.StoreContext, orgID, userID string) bool {
|
||||
row := s.Runtime.Db.QueryRow("SELECT count(*) FROM account WHERE orgid=? and userid=?", orgID, userID)
|
||||
|
||||
var count int
|
||||
err := row.Scan(&count)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "HasOrgAccount")
|
||||
return false
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteAccount deletes the database record in the account table for user ID.
|
||||
func DeleteAccount(s domain.StoreContext, ID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
return b.DeleteConstrained(s.Context.Transaction, "account", s.Context.OrgID, ID)
|
||||
}
|
207
domain/auth/endpoint.go
Normal file
207
domain/auth/endpoint.go
Normal file
|
@ -0,0 +1,207 @@
|
|||
// 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 auth
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/documize/community/core/response"
|
||||
"github.com/documize/community/core/secrets"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/organization"
|
||||
"github.com/documize/community/domain/section/provider"
|
||||
"github.com/documize/community/domain/user"
|
||||
)
|
||||
|
||||
// Authenticate user based up HTTP Authorization header.
|
||||
// An encrypted authentication token is issued with an expiry date.
|
||||
func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||
method := "Authenticate"
|
||||
|
||||
s := domain.StoreContext{Runtime: h.Runtime, Context: domain.GetRequestContext(r)}
|
||||
|
||||
// check for http header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if len(authHeader) == 0 {
|
||||
response.WriteBadRequestError(w, method, "Missing Authorization header")
|
||||
return
|
||||
}
|
||||
|
||||
// decode what we received
|
||||
data := strings.Replace(authHeader, "Basic ", "", 1)
|
||||
|
||||
decodedBytes, err := secrets.DecodeBase64([]byte(data))
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "Unable to decode authentication token")
|
||||
return
|
||||
}
|
||||
|
||||
decoded := string(decodedBytes)
|
||||
|
||||
// check that we have domain:email:password (but allow for : in password field!)
|
||||
credentials := strings.SplitN(decoded, ":", 3)
|
||||
|
||||
if len(credentials) != 3 {
|
||||
response.WriteBadRequestError(w, method, "Bad authentication token, expecting domain:email:password")
|
||||
return
|
||||
}
|
||||
|
||||
dom := strings.TrimSpace(strings.ToLower(credentials[0]))
|
||||
dom = organization.CheckDomain(s, dom) // TODO optimize by removing this once js allows empty domains
|
||||
email := strings.TrimSpace(strings.ToLower(credentials[1]))
|
||||
password := credentials[2]
|
||||
h.Runtime.Log.Info("logon attempt " + email + " @ " + dom)
|
||||
|
||||
u, err := user.GetByDomain(s, dom, email)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(u.Reset) > 0 || len(u.Password) == 0 {
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Password correct and active user
|
||||
if email != strings.TrimSpace(strings.ToLower(u.Email)) || !secrets.MatchPassword(u.Password, password, u.Salt) {
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
||||
org, err := organization.GetOrganizationByDomain(s, dom)
|
||||
if err != nil {
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Attach user accounts and work out permissions
|
||||
user.AttachUserAccounts(s, org.RefID, &u)
|
||||
|
||||
// active check
|
||||
|
||||
if len(u.Accounts) == 0 {
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
||||
authModel := AuthenticationModel{}
|
||||
authModel.Token = GenerateJWT(h.Runtime, u.RefID, org.RefID, dom)
|
||||
authModel.User = u
|
||||
|
||||
response.WriteJSON(w, authModel)
|
||||
}
|
||||
|
||||
// ValidateAuthToken finds and validates authentication token.
|
||||
func (h *Handler) ValidateAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO should this go after token validation?
|
||||
if s := r.URL.Query().Get("section"); s != "" {
|
||||
if err := provider.Callback(s, w, r); err != nil {
|
||||
h.Runtime.Log.Error("section validation failure", err)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s := domain.StoreContext{Runtime: h.Runtime, Context: domain.GetRequestContext(r)}
|
||||
|
||||
token := FindJWT(r)
|
||||
rc, _, tokenErr := DecodeJWT(h.Runtime, token)
|
||||
|
||||
var org = organization.Organization{}
|
||||
var err = errors.New("")
|
||||
|
||||
// We always grab the org record regardless of token status.
|
||||
// Why? If bad token we might be OK to alow anonymous access
|
||||
// depending upon the domain in question.
|
||||
if len(rc.OrgID) == 0 {
|
||||
org, err = organization.GetOrganizationByDomain(s, organization.GetRequestSubdomain(s, r))
|
||||
} else {
|
||||
org, err = organization.GetOrganization(s, rc.OrgID)
|
||||
}
|
||||
|
||||
rc.Subdomain = org.Domain
|
||||
|
||||
// Inability to find org record spells the end of this request.
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have bad auth token and the domain does not allow anon access
|
||||
if !org.AllowAnonymousAccess && tokenErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dom := organization.GetSubdomainFromHost(s, r)
|
||||
dom2 := organization.GetRequestSubdomain(s, r)
|
||||
if org.Domain != dom && org.Domain != dom2 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have bad auth token and the domain allows anon access
|
||||
// then we generate guest rc.
|
||||
if org.AllowAnonymousAccess {
|
||||
// So you have a bad token
|
||||
if len(token) > 1 {
|
||||
if tokenErr != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Just grant anon user guest access
|
||||
rc.UserID = "0"
|
||||
rc.OrgID = org.RefID
|
||||
rc.Authenticated = false
|
||||
rc.Guest = true
|
||||
}
|
||||
}
|
||||
|
||||
rc.AllowAnonymousAccess = org.AllowAnonymousAccess
|
||||
rc.OrgName = org.Title
|
||||
rc.Administrator = false
|
||||
rc.Editor = false
|
||||
rc.Global = false
|
||||
rc.AppURL = r.Host
|
||||
rc.Subdomain = organization.GetSubdomainFromHost(s, r)
|
||||
rc.SSL = r.TLS != nil
|
||||
|
||||
// Fetch user permissions for this org
|
||||
if !rc.Authenticated {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := user.GetSecuredUser(s, org.RefID, rc.UserID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rc.Administrator = u.Admin
|
||||
rc.Editor = u.Editor
|
||||
rc.Global = u.Global
|
||||
|
||||
response.WriteJSON(w, u)
|
||||
return
|
||||
}
|
133
domain/auth/jwt.go
Normal file
133
domain/auth/jwt.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
// 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
)
|
||||
|
||||
// GenerateJWT generates JSON Web Token (http://jwt.io)
|
||||
func GenerateJWT(rt env.Runtime, user, org, domain string) string {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"iss": "Documize",
|
||||
"sub": "webapp",
|
||||
"exp": time.Now().Add(time.Hour * 168).Unix(),
|
||||
"user": user,
|
||||
"org": org,
|
||||
"domain": domain,
|
||||
})
|
||||
|
||||
tokenString, _ := token.SignedString([]byte(rt.Flags.Salt))
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
// FindJWT looks for 'Authorization' request header OR query string "?token=XXX".
|
||||
func FindJWT(r *http.Request) (token string) {
|
||||
header := r.Header.Get("Authorization")
|
||||
|
||||
if header != "" {
|
||||
header = strings.Replace(header, "Bearer ", "", 1)
|
||||
}
|
||||
|
||||
if len(header) > 1 {
|
||||
token = header
|
||||
} else {
|
||||
query := r.URL.Query()
|
||||
token = query.Get("token")
|
||||
}
|
||||
|
||||
if token == "null" {
|
||||
token = ""
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeJWT decodes raw token.
|
||||
func DecodeJWT(rt env.Runtime, tokenString string) (c domain.RequestContext, claims jwt.Claims, err error) {
|
||||
// sensible defaults
|
||||
c.UserID = ""
|
||||
c.OrgID = ""
|
||||
c.Authenticated = false
|
||||
c.Guest = false
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(rt.Flags.Salt), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("bad authorization token")
|
||||
return
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
if ve, ok := err.(*jwt.ValidationError); ok {
|
||||
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
|
||||
err = fmt.Errorf("bad token")
|
||||
return
|
||||
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
|
||||
err = fmt.Errorf("expired token")
|
||||
return
|
||||
} else {
|
||||
err = fmt.Errorf("bad token")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("bad token")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c = domain.RequestContext{}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
c.UserID = claims["user"].(string)
|
||||
c.OrgID = claims["org"].(string)
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
if len(c.UserID) == 0 || len(c.OrgID) == 0 {
|
||||
err = fmt.Errorf("unable parse token data")
|
||||
return
|
||||
}
|
||||
|
||||
c.Authenticated = true
|
||||
c.Guest = false
|
||||
|
||||
return c, token.Claims, nil
|
||||
}
|
||||
|
||||
// DecodeKeycloakJWT takes in Keycloak token string and decodes it.
|
||||
func DecodeKeycloakJWT(t, pk string) (c jwt.MapClaims, err error) {
|
||||
token, err := jwt.Parse(t, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return jwt.ParseRSAPublicKeyFromPEM([]byte(pk))
|
||||
})
|
||||
|
||||
if c, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
28
domain/auth/model.go
Normal file
28
domain/auth/model.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// 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 auth
|
||||
|
||||
import (
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain/user"
|
||||
)
|
||||
|
||||
// Handler contains the runtime information such as logging and database.
|
||||
type Handler struct {
|
||||
Runtime env.Runtime
|
||||
}
|
||||
|
||||
// AuthenticationModel details authentication token and user details.
|
||||
type AuthenticationModel struct {
|
||||
Token string `json:"token"`
|
||||
User user.User `json:"user"`
|
||||
}
|
64
domain/context.go
Normal file
64
domain/context.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// RequestContext provides per request scoped values required
|
||||
// by HTTP handlers.
|
||||
type RequestContext struct {
|
||||
AllowAnonymousAccess bool
|
||||
Authenticated bool
|
||||
Administrator bool
|
||||
Guest bool
|
||||
Editor bool
|
||||
Global bool
|
||||
UserID string
|
||||
OrgID string
|
||||
OrgName string
|
||||
SSL bool
|
||||
AppURL string // e.g. https://{url}.documize.com
|
||||
Subdomain string
|
||||
ClientIP string
|
||||
Expires time.Time
|
||||
Fullname string
|
||||
Transaction *sqlx.Tx
|
||||
}
|
||||
|
||||
//GetAppURL returns full HTTP url for the app
|
||||
func (c *RequestContext) GetAppURL(endpoint string) string {
|
||||
scheme := "http://"
|
||||
|
||||
if c.SSL {
|
||||
scheme = "https://"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s/%s", scheme, c.AppURL, endpoint)
|
||||
}
|
||||
|
||||
type key string
|
||||
|
||||
// DocumizeContextKey prevents key name collisions.
|
||||
const DocumizeContextKey key = "documize context key"
|
||||
|
||||
// GetRequestContext returns RequestContext from context.Context
|
||||
func GetRequestContext(r *http.Request) RequestContext {
|
||||
return r.Context().Value(DocumizeContextKey).(RequestContext)
|
||||
}
|
||||
|
||||
// StoreContext provides data persistence methods with runtime and request context.
|
||||
type StoreContext struct {
|
||||
Runtime env.Runtime
|
||||
Context RequestContext
|
||||
}
|
||||
|
||||
// NewContexts returns request scoped user context and store context for persistence logic.
|
||||
func NewContexts(rt env.Runtime, r *http.Request) (RequestContext, StoreContext) {
|
||||
ctx := GetRequestContext(r)
|
||||
return ctx, StoreContext{Runtime: rt, Context: ctx}
|
||||
}
|
39
domain/document/store.go
Normal file
39
domain/document/store.go
Normal 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
|
||||
|
||||
package document
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// MoveDocumentSpace changes the label for client's organization's documents which have space "id", to "move".
|
||||
func MoveDocumentSpace(s domain.StoreContext, id, move string) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("UPDATE document SET labelid=? WHERE orgid=? AND labelid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare document space move %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(move, s.Context.OrgID, id)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute document space move %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
70
domain/eventing/model.go
Normal file
70
domain/eventing/model.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
// 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 eventing records and propagates events based on user actions.
|
||||
package eventing
|
||||
|
||||
import "time"
|
||||
|
||||
// AppEvent represents an event initiated by a user.
|
||||
type AppEvent struct {
|
||||
ID uint64 `json:"-"`
|
||||
OrgID string `json:"orgId"`
|
||||
UserID string `json:"userId"`
|
||||
Type string `json:"eventType"`
|
||||
IP string `json:"ip"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
// EventType defines valid event entry types
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeDocumentAdd EventType = "added-document"
|
||||
EventTypeDocumentUpload EventType = "uploaded-document"
|
||||
EventTypeDocumentView EventType = "viewed-document"
|
||||
EventTypeDocumentUpdate EventType = "updated-document"
|
||||
EventTypeDocumentDelete EventType = "removed-document"
|
||||
EventTypeDocumentRevisions EventType = "viewed-document-revisions"
|
||||
EventTypeSpaceAdd EventType = "added-space"
|
||||
EventTypeSpaceUpdate EventType = "updated-space"
|
||||
EventTypeSpaceDelete EventType = "removed-space"
|
||||
EventTypeSpacePermission EventType = "changed-space-permissions"
|
||||
EventTypeSpaceJoin EventType = "joined-space"
|
||||
EventTypeSpaceInvite EventType = "invited-space"
|
||||
EventTypeSectionAdd EventType = "added-document-section"
|
||||
EventTypeSectionUpdate EventType = "updated-document-section"
|
||||
EventTypeSectionDelete EventType = "removed-document-section"
|
||||
EventTypeSectionRollback EventType = "rolled-back-document-section"
|
||||
EventTypeSectionResequence EventType = "resequenced-document-section"
|
||||
EventTypeSectionCopy EventType = "copied-document-section"
|
||||
EventTypeAttachmentAdd EventType = "added-attachment"
|
||||
EventTypeAttachmentDownload EventType = "downloaded-attachment"
|
||||
EventTypeAttachmentDelete EventType = "removed-attachment"
|
||||
EventTypePinAdd EventType = "added-pin"
|
||||
EventTypePinDelete EventType = "removed-pin"
|
||||
EventTypePinResequence EventType = "resequenced-pin"
|
||||
EventTypeBlockAdd EventType = "added-reusable-block"
|
||||
EventTypeBlockUpdate EventType = "updated-reusable-block"
|
||||
EventTypeBlockDelete EventType = "removed-reusable-block"
|
||||
EventTypeTemplateAdd EventType = "added-document-template"
|
||||
EventTypeTemplateUse EventType = "used-document-template"
|
||||
EventTypeUserAdd EventType = "added-user"
|
||||
EventTypeUserUpdate EventType = "updated-user"
|
||||
EventTypeUserDelete EventType = "removed-user"
|
||||
EventTypeUserPasswordReset EventType = "reset-user-password"
|
||||
EventTypeAccountAdd EventType = "added-account"
|
||||
EventTypeSystemLicense EventType = "changed-system-license"
|
||||
EventTypeSystemAuth EventType = "changed-system-auth"
|
||||
EventTypeSystemSMTP EventType = "changed-system-smtp"
|
||||
EventTypeSessionStart EventType = "started-session"
|
||||
EventTypeSearch EventType = "searched"
|
||||
)
|
55
domain/eventing/store.go
Normal file
55
domain/eventing/store.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
// 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 eventing records user events.
|
||||
package eventing
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Record adds event entry for specified user.
|
||||
func Record(s domain.StoreContext, t EventType) {
|
||||
e := AppEvent{}
|
||||
e.OrgID = s.Context.OrgID
|
||||
e.UserID = s.Context.UserID
|
||||
e.Created = time.Now().UTC()
|
||||
e.IP = s.Context.ClientIP
|
||||
e.Type = string(t)
|
||||
|
||||
tx, err := s.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "start transaction")
|
||||
return
|
||||
}
|
||||
|
||||
stmt, err := tx.Preparex("INSERT INTO userevent (orgid, userid, eventtype, ip, created) VALUES (?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
err = errors.Wrap(err, "prepare insert RecordEvent")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(e.OrgID, e.UserID, e.Type, e.IP, e.Created)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "execute insert RecordEvent")
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
|
||||
stmt.Close()
|
||||
tx.Commit()
|
||||
|
||||
return
|
||||
}
|
34
domain/model.go
Normal file
34
domain/model.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
// 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 ...
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BaseEntity contains the database fields used in every table.
|
||||
type BaseEntity struct {
|
||||
ID uint64 `json:"-"`
|
||||
RefID string `json:"id"`
|
||||
Created time.Time `json:"created"`
|
||||
Revised time.Time `json:"revised"`
|
||||
}
|
||||
|
||||
// BaseEntityObfuscated is a mirror of BaseEntity,
|
||||
// but with the fields invisible to JSON.
|
||||
type BaseEntityObfuscated struct {
|
||||
ID uint64 `json:"-"`
|
||||
RefID string `json:"-"`
|
||||
Created time.Time `json:"-"`
|
||||
Revised time.Time `json:"-"`
|
||||
}
|
39
domain/organization/model.go
Normal file
39
domain/organization/model.go
Normal 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
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
)
|
||||
|
||||
// Handler contains the runtime information such as logging and database.
|
||||
type Handler struct {
|
||||
Runtime env.Runtime
|
||||
}
|
||||
|
||||
// Organization defines a company that uses this app.
|
||||
type Organization struct {
|
||||
domain.BaseEntity
|
||||
Company string `json:"-"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
Domain string `json:"domain"`
|
||||
Email string `json:"email"`
|
||||
AllowAnonymousAccess bool `json:"allowAnonymousAccess"`
|
||||
AuthProvider string `json:"authProvider"`
|
||||
AuthConfig string `json:"authConfig"`
|
||||
ConversionEndpoint string `json:"conversionEndpoint"`
|
||||
Serial string `json:"-"`
|
||||
Active bool `json:"-"`
|
||||
}
|
46
domain/organization/organization.go
Normal file
46
domain/organization/organization.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// 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 organization
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/documize/community/domain"
|
||||
)
|
||||
|
||||
// GetRequestSubdomain extracts subdomain from referring URL.
|
||||
func GetRequestSubdomain(s domain.StoreContext, r *http.Request) string {
|
||||
return urlSubdomain(s, r.Referer())
|
||||
}
|
||||
|
||||
// GetSubdomainFromHost extracts the subdomain from the requesting URL.
|
||||
func GetSubdomainFromHost(s domain.StoreContext, r *http.Request) string {
|
||||
return urlSubdomain(s, r.Host)
|
||||
}
|
||||
|
||||
// Find the subdomain (which is actually the organisation).
|
||||
func urlSubdomain(s domain.StoreContext, url string) string {
|
||||
url = strings.ToLower(url)
|
||||
url = strings.Replace(url, "https://", "", 1)
|
||||
url = strings.Replace(url, "http://", "", 1)
|
||||
|
||||
parts := strings.Split(url, ".")
|
||||
|
||||
if len(parts) >= 2 {
|
||||
url = parts[0]
|
||||
} else {
|
||||
url = ""
|
||||
}
|
||||
|
||||
return CheckDomain(s, url)
|
||||
}
|
183
domain/organization/store.go
Normal file
183
domain/organization/store.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
// 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 organization
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/store/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// AddOrganization inserts the passed organization record into the organization table.
|
||||
func AddOrganization(s domain.StoreContext, org Organization) error {
|
||||
org.Created = time.Now().UTC()
|
||||
org.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.Preparex(
|
||||
"INSERT INTO organization (refid, company, title, message, url, domain, email, allowanonymousaccess, serial, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to prepare insert for org")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.URL), strings.ToLower(org.Domain),
|
||||
strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.Created, org.Revised)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to execute insert for org")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrganization returns the Organization reocrod from the organization database table with the given id.
|
||||
func GetOrganization(s domain.StoreContext, id string) (org Organization, err error) {
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for org %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&org, id)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to get org %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetOrganizationByDomain returns the organization matching a given URL subdomain.
|
||||
func GetOrganizationByDomain(s domain.StoreContext, subdomain string) (org Organization, err error) {
|
||||
err = nil
|
||||
subdomain = strings.ToLower(subdomain)
|
||||
|
||||
if s.Runtime.Flags.SiteMode == env.SiteModeNormal { // only return an organization when running normally
|
||||
var stmt *sqlx.Stmt
|
||||
|
||||
stmt, err = s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE domain=? AND active=1")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for subdomain %s", subdomain))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&org, subdomain)
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for subdomain %s", subdomain))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateOrganization updates the given organization record in the database to the values supplied.
|
||||
func UpdateOrganization(s domain.StoreContext, org Organization) (err error) {
|
||||
org.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE organization SET title=:title, message=:message, service=:conversionendpoint, email=:email, allowanonymousaccess=:allowanonymousaccess, revised=:revised WHERE refid=:refid")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare update for org %s", org.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&org)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute update for org %s", org.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteOrganization deletes the orgID organization from the organization table.
|
||||
func DeleteOrganization(s domain.StoreContext, orgID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
return b.Delete(s.Context.Transaction, "organization", orgID)
|
||||
}
|
||||
|
||||
// RemoveOrganization sets the orgID organization to be inactive, thus executing a "soft delete" operation.
|
||||
func RemoveOrganization(s domain.StoreContext, rc domain.RequestContext, orgID string) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("UPDATE organization SET active=0 WHERE refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare soft delete for org %s", orgID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(orgID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute soft delete for org %s", orgID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateAuthConfig updates the given organization record in the database with the auth config details.
|
||||
func UpdateAuthConfig(s domain.StoreContext, org Organization) (err error) {
|
||||
org.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE organization SET allowanonymousaccess=:allowanonymousaccess, authprovider=:authprovider, authconfig=:authconfig, revised=:revised WHERE refid=:refid")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare UpdateAuthConfig %s", org.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&org)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute UpdateAuthConfig %s", org.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CheckDomain makes sure there is an organisation with the correct domain
|
||||
func CheckDomain(s domain.StoreContext, domain string) string {
|
||||
row := s.Runtime.Db.QueryRow("SELECT COUNT(*) FROM organization WHERE domain=? AND active=1", domain)
|
||||
|
||||
var count int
|
||||
err := row.Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
return domain
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
33
domain/pin/model.go
Normal file
33
domain/pin/model.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// 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 pin
|
||||
|
||||
import (
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
)
|
||||
|
||||
// Handler contains the runtime information such as logging and database.
|
||||
type Handler struct {
|
||||
Runtime env.Runtime
|
||||
}
|
||||
|
||||
// Pin defines a saved link to a document or space
|
||||
type Pin struct {
|
||||
domain.BaseEntity
|
||||
OrgID string `json:"orgId"`
|
||||
UserID string `json:"userId"`
|
||||
FolderID string `json:"folderId"`
|
||||
DocumentID string `json:"documentId"`
|
||||
Pin string `json:"pin"`
|
||||
Sequence int `json:"sequence"`
|
||||
}
|
145
domain/pin/store.go
Normal file
145
domain/pin/store.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
// 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 pin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/api/entity"
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/store/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Add saves pinned item.
|
||||
func Add(s domain.StoreContext, pin Pin) (err error) {
|
||||
row := s.Runtime.Db.QueryRow("SELECT max(sequence) FROM pin WHERE orgid=? AND userid=?", s.Context.OrgID, s.Context.UserID)
|
||||
var maxSeq int
|
||||
err = row.Scan(&maxSeq)
|
||||
|
||||
if err != nil {
|
||||
maxSeq = 99
|
||||
}
|
||||
|
||||
pin.Created = time.Now().UTC()
|
||||
pin.Revised = time.Now().UTC()
|
||||
pin.Sequence = maxSeq + 1
|
||||
|
||||
stmt, err := s.Context.Transaction.Preparex("INSERT INTO pin (refid, orgid, userid, labelid, documentid, pin, sequence, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "prepare pin insert")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(pin.RefID, pin.OrgID, pin.UserID, pin.FolderID, pin.DocumentID, pin.Pin, pin.Sequence, pin.Created, pin.Revised)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "execute pin insert")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetPin returns requested pinned item.
|
||||
func GetPin(s domain.StoreContext, id string) (pin Pin, err error) {
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, orgid, userid, labelid as folderid, documentid, pin, sequence, created, revised FROM pin WHERE orgid=? AND refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare select for pin %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&pin, s.Context.OrgID, id)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute select for pin %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserPins returns pinned items for specified user.
|
||||
func GetUserPins(s domain.StoreContext, userID string) (pins []Pin, err error) {
|
||||
err = s.Runtime.Db.Select(&pins, "SELECT id, refid, orgid, userid, labelid as folderid, documentid, pin, sequence, created, revised FROM pin WHERE orgid=? AND userid=? ORDER BY sequence", s.Context.OrgID, userID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute select pins for org %s and user %s", s.Context.OrgID, userID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdatePin updates existing pinned item.
|
||||
func UpdatePin(s domain.StoreContext, pin entity.Pin) (err error) {
|
||||
pin.Revised = time.Now().UTC()
|
||||
|
||||
var stmt *sqlx.NamedStmt
|
||||
stmt, err = s.Context.Transaction.PrepareNamed("UPDATE pin SET labelid=:folderid, documentid=:documentid, pin=:pin, sequence=:sequence, revised=:revised WHERE orgid=:orgid AND refid=:refid")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare pin update %s", pin.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&pin)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute pin update %s", pin.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdatePinSequence updates existing pinned item sequence number
|
||||
func UpdatePinSequence(s domain.StoreContext, pinID string, sequence int) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("UPDATE pin SET sequence=?, revised=? WHERE orgid=? AND userid=? AND refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare pin sequence update %s", pinID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(sequence, time.Now().UTC(), s.Context.OrgID, s.Context.UserID, pinID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute pin sequence update %s", pinID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeletePin removes folder from the store.
|
||||
func DeletePin(s domain.StoreContext, id string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
return b.DeleteConstrained(s.Context.Transaction, "pin", s.Context.OrgID, id)
|
||||
}
|
||||
|
||||
// DeletePinnedSpace removes any pins for specified space.
|
||||
func DeletePinnedSpace(s domain.StoreContext, spaceID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
return b.DeleteWhere(s.Context.Transaction, fmt.Sprintf("DELETE FROM pin WHERE orgid=\"%s\" AND labelid=\"%s\"", s.Context.OrgID, spaceID))
|
||||
}
|
||||
|
||||
// DeletePinnedDocument removes any pins for specified document.
|
||||
func DeletePinnedDocument(s domain.StoreContext, documentID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
return b.DeleteWhere(s.Context.Transaction, fmt.Sprintf("DELETE FROM pin WHERE orgid=\"%s\" AND documentid=\"%s\"", s.Context.OrgID, documentID))
|
||||
}
|
|
@ -12,3 +12,757 @@
|
|||
// Package space handles API calls and persistence for spaces.
|
||||
// Spaces in Documize contain documents.
|
||||
package space
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/documize/api/wordsmith/log"
|
||||
"github.com/documize/community/core/api/mail"
|
||||
"github.com/documize/community/core/request"
|
||||
"github.com/documize/community/core/response"
|
||||
"github.com/documize/community/core/secrets"
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/core/stringutil"
|
||||
"github.com/documize/community/core/uniqueid"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/account"
|
||||
"github.com/documize/community/domain/document"
|
||||
"github.com/documize/community/domain/eventing"
|
||||
"github.com/documize/community/domain/organization"
|
||||
"github.com/documize/community/domain/pin"
|
||||
"github.com/documize/community/domain/user"
|
||||
)
|
||||
|
||||
// Add creates a new space.
|
||||
func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
||||
method := "AddSpace"
|
||||
ctx, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
if !h.Runtime.Product.License.IsValid() {
|
||||
response.WriteBadLicense(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Editor {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var space = Space{}
|
||||
err = json.Unmarshal(body, &space)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(space.Name) == 0 {
|
||||
response.WriteMissingDataError(w, method, "name")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
space.RefID = uniqueid.Generate()
|
||||
space.OrgID = ctx.OrgID
|
||||
|
||||
err = addSpace(s, space)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
eventing.Record(s, eventing.EventTypeSpaceAdd)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
space, _ = Get(s, space.RefID)
|
||||
|
||||
response.WriteJSON(w, space)
|
||||
}
|
||||
|
||||
// Get returns the requested space.
|
||||
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
method := "Get"
|
||||
_, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
id := request.Param(r, "folderID")
|
||||
if len(id) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
sp, err := Get(s, id)
|
||||
if err == sql.ErrNoRows {
|
||||
response.WriteNotFoundError(w, method, id)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, sp)
|
||||
}
|
||||
|
||||
// GetAll returns spaces the user can see.
|
||||
func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) {
|
||||
method := "GetAll"
|
||||
_, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
sp, err := GetAll(s)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(sp) == 0 {
|
||||
sp = []Space{}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, sp)
|
||||
}
|
||||
|
||||
// GetSpaceViewers returns the users that can see the shared spaces.
|
||||
func (h *Handler) GetSpaceViewers(w http.ResponseWriter, r *http.Request) {
|
||||
method := "GetSpaceViewers"
|
||||
_, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
v, err := Viewers(s)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(v) == 0 {
|
||||
v = []Viewer{}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, v)
|
||||
}
|
||||
|
||||
// Update processes request to save space object to the database
|
||||
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
method := "space.Update"
|
||||
ctx, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
if !ctx.Editor {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
folderID := request.Param(r, "folderID")
|
||||
if len(folderID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var sp Space
|
||||
err = json.Unmarshal(body, &sp)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "marshal")
|
||||
return
|
||||
}
|
||||
|
||||
if len(sp.Name) == 0 {
|
||||
response.WriteMissingDataError(w, method, "name")
|
||||
return
|
||||
}
|
||||
|
||||
sp.RefID = folderID
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = Update(s, sp)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
eventing.Record(s, eventing.EventTypeSpaceUpdate)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
response.WriteJSON(w, sp)
|
||||
}
|
||||
|
||||
// Remove moves documents to another folder before deleting it
|
||||
func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
|
||||
method := "space.Remove"
|
||||
ctx, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
if !h.Runtime.Product.License.IsValid() {
|
||||
response.WriteBadLicense(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Editor {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
id := request.Param(r, "folderID")
|
||||
move := request.Param(r, "moveToId")
|
||||
|
||||
if len(id) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
if len(move) == 0 {
|
||||
response.WriteMissingDataError(w, method, "moveToId")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = Delete(s, id)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = document.MoveDocumentSpace(s, id, move)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = MoveSpaceRoles(s, id, move)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = pin.DeletePinnedSpace(s, id)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
eventing.Record(s, eventing.EventTypeSpaceDelete)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
||||
// Delete deletes empty space.
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
method := "space.Delete"
|
||||
ctx, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
if !h.Runtime.Product.License.IsValid() {
|
||||
response.WriteBadLicense(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Editor {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
id := request.Param(r, "folderID")
|
||||
if len(id) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = Delete(s, id)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = DeleteSpaceRoles(s, id)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = pin.DeletePinnedSpace(s, id)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
eventing.Record(s, eventing.EventTypeSpaceDelete)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
||||
// SetPermissions persists specified spac3 permissions
|
||||
func (h *Handler) SetPermissions(w http.ResponseWriter, r *http.Request) {
|
||||
method := "space.SetPermissions"
|
||||
ctx, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
if !ctx.Editor {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
id := request.Param(r, "folderID")
|
||||
if len(id) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
sp, err := Get(s, id)
|
||||
if err != nil {
|
||||
response.WriteNotFoundError(w, method, "No such space")
|
||||
return
|
||||
}
|
||||
|
||||
if sp.UserID != s.Context.UserID {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var model = RolesModel{}
|
||||
err = json.Unmarshal(body, &model)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// We compare new permisions to what we had before.
|
||||
// Why? So we can send out folder invitation emails.
|
||||
previousRoles, err := GetRoles(s, id)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Store all previous roles as map for easy querying
|
||||
previousRoleUsers := make(map[string]bool)
|
||||
|
||||
for _, v := range previousRoles {
|
||||
previousRoleUsers[v.UserID] = true
|
||||
}
|
||||
|
||||
// Who is sharing this folder?
|
||||
inviter, err := user.Get(s, s.Context.UserID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke all previous permissions for this folder
|
||||
_, err = DeleteSpaceRoles(s, id)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
me := false
|
||||
hasEveryoneRole := false
|
||||
roleCount := 0
|
||||
|
||||
url := s.Context.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
|
||||
|
||||
for _, role := range model.Roles {
|
||||
role.OrgID = s.Context.OrgID
|
||||
role.LabelID = id
|
||||
|
||||
// Ensure the folder owner always has access!
|
||||
if role.UserID == s.Context.UserID {
|
||||
me = true
|
||||
role.CanView = true
|
||||
role.CanEdit = true
|
||||
}
|
||||
|
||||
if len(role.UserID) == 0 && (role.CanView || role.CanEdit) {
|
||||
hasEveryoneRole = true
|
||||
}
|
||||
|
||||
// Only persist if there is a role!
|
||||
if role.CanView || role.CanEdit {
|
||||
roleID := uniqueid.Generate()
|
||||
role.RefID = roleID
|
||||
err = AddRole(s, role)
|
||||
roleCount++
|
||||
log.IfErr(err)
|
||||
|
||||
// We send out folder invitation emails to those users
|
||||
// that have *just* been given permissions.
|
||||
if _, isExisting := previousRoleUsers[role.UserID]; !isExisting {
|
||||
|
||||
// we skip 'everyone' (user id != empty string)
|
||||
if len(role.UserID) > 0 {
|
||||
var existingUser user.User
|
||||
existingUser, err = user.Get(s, role.UserID)
|
||||
|
||||
if err == nil {
|
||||
go mail.ShareFolderExistingUser(existingUser.Email, inviter.Fullname(), url, sp.Name, model.Message)
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, existingUser.Email))
|
||||
} else {
|
||||
response.WriteServerError(w, method, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do we need to ensure permissions for space owner when shared?
|
||||
if !me {
|
||||
role := Role{}
|
||||
role.LabelID = id
|
||||
role.OrgID = s.Context.OrgID
|
||||
role.UserID = s.Context.UserID
|
||||
role.CanEdit = true
|
||||
role.CanView = true
|
||||
roleID := uniqueid.Generate()
|
||||
role.RefID = roleID
|
||||
|
||||
err = AddRole(s, role)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Mark up folder type as either public, private or restricted access.
|
||||
if hasEveryoneRole {
|
||||
sp.Type = ScopePublic
|
||||
} else {
|
||||
if roleCount > 1 {
|
||||
sp.Type = ScopeRestricted
|
||||
} else {
|
||||
sp.Type = ScopePrivate
|
||||
}
|
||||
}
|
||||
|
||||
err = Update(s, sp)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
eventing.Record(s, eventing.EventTypeSpacePermission)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
||||
// GetPermissions returns user permissions for the requested folder.
|
||||
func (h *Handler) GetPermissions(w http.ResponseWriter, r *http.Request) {
|
||||
method := "space.GetPermissions"
|
||||
_, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
folderID := request.Param(r, "folderID")
|
||||
if len(folderID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
roles, err := GetRoles(s, folderID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(roles) == 0 {
|
||||
roles = []Role{}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, roles)
|
||||
}
|
||||
|
||||
// AcceptInvitation records the fact that a user has completed space onboard process.
|
||||
func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
|
||||
method := "space.AcceptInvitation"
|
||||
ctx, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
folderID := request.Param(r, "folderID")
|
||||
if len(folderID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
org, err := organization.GetOrganizationByDomain(s, ctx.Subdomain)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// AcceptShare does not authenticate the user hence the context needs to set up
|
||||
ctx.OrgID = org.RefID
|
||||
s.Context.OrgID = org.RefID
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var model = AcceptShareModel{}
|
||||
err = json.Unmarshal(body, &model)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(model.Serial) == 0 || len(model.Firstname) == 0 || len(model.Lastname) == 0 || len(model.Password) == 0 {
|
||||
response.WriteMissingDataError(w, method, "Serial, Firstname, Lastname, Password")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := user.GetBySerial(s, model.Serial)
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
response.WriteDuplicateError(w, method, "user")
|
||||
return
|
||||
}
|
||||
|
||||
// AcceptShare does not authenticate the user hence the context needs to set up
|
||||
ctx.UserID = u.RefID
|
||||
s.Context.UserID = u.RefID
|
||||
|
||||
u.Firstname = model.Firstname
|
||||
u.Lastname = model.Lastname
|
||||
u.Initials = stringutil.MakeInitials(u.Firstname, u.Lastname)
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = user.UpdateUser(s, u)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
salt := secrets.GenerateSalt()
|
||||
|
||||
err = user.UpdateUserPassword(s, u.RefID, salt, secrets.GeneratePassword(model.Password, salt))
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
eventing.Record(s, eventing.EventTypeSpaceJoin)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
response.WriteJSON(w, u)
|
||||
}
|
||||
|
||||
// Invite sends users folder invitation emails.
|
||||
func (h *Handler) Invite(w http.ResponseWriter, r *http.Request) {
|
||||
method := "space.Invite"
|
||||
ctx, s := domain.NewContexts(h.Runtime, r)
|
||||
|
||||
id := request.Param(r, "folderID")
|
||||
if len(id) == 0 {
|
||||
response.WriteMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
sp, err := Get(s, id)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if sp.UserID != s.Context.UserID {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "body")
|
||||
return
|
||||
}
|
||||
|
||||
var model = InvitationModel{}
|
||||
err = json.Unmarshal(body, &model)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "json")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
inviter, err := user.Get(s, ctx.UserID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, email := range model.Recipients {
|
||||
u, err := user.GetByEmail(s, email)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(u.RefID) > 0 {
|
||||
// Ensure they have access to this organization
|
||||
accounts, err2 := account.GetUserAccounts(s, u.RefID)
|
||||
if err2 != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// we create if they c
|
||||
hasAccess := false
|
||||
for _, a := range accounts {
|
||||
if a.OrgID == s.Context.OrgID {
|
||||
hasAccess = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAccess {
|
||||
var a account.Account
|
||||
a.UserID = u.RefID
|
||||
a.OrgID = s.Context.OrgID
|
||||
a.Admin = false
|
||||
a.Editor = false
|
||||
a.Active = true
|
||||
accountID := uniqueid.Generate()
|
||||
a.RefID = accountID
|
||||
|
||||
err = account.Add(s, a)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure they have space roles
|
||||
DeleteUserSpaceRoles(s, sp.RefID, u.RefID)
|
||||
|
||||
role := Role{}
|
||||
role.LabelID = sp.RefID
|
||||
role.OrgID = ctx.OrgID
|
||||
role.UserID = u.RefID
|
||||
role.CanEdit = false
|
||||
role.CanView = true
|
||||
roleID := uniqueid.Generate()
|
||||
role.RefID = roleID
|
||||
|
||||
err = AddRole(s, role)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
|
||||
go mail.ShareFolderExistingUser(email, inviter.Fullname(), url, sp.Name, model.Message)
|
||||
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, email))
|
||||
} else {
|
||||
// On-board new user
|
||||
if strings.Contains(email, "@") {
|
||||
url := ctx.GetAppURL(fmt.Sprintf("auth/share/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
|
||||
err = inviteNewUserToSharedSpace(s, email, inviter, url, sp, model.Message)
|
||||
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with new user %s", inviter.Email, sp.Name, email))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We ensure that the folder is marked as restricted as a minimum!
|
||||
if len(model.Recipients) > 0 && sp.Type == ScopePrivate {
|
||||
sp.Type = ScopeRestricted
|
||||
|
||||
err = Update(s, sp)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
eventing.Record(s, eventing.EventTypeSpaceInvite)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
|
|
@ -12,3 +12,92 @@
|
|||
// Package space handles API calls and persistence for spaces.
|
||||
// Spaces in Documize contain documents.
|
||||
package space
|
||||
|
||||
import (
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
)
|
||||
|
||||
// Handler contains the runtime information such as logging and database.
|
||||
type Handler struct {
|
||||
Runtime env.Runtime
|
||||
}
|
||||
|
||||
// Space defines a container for documents.
|
||||
type Space struct {
|
||||
domain.BaseEntity
|
||||
Name string `json:"name"`
|
||||
OrgID string `json:"orgId"`
|
||||
UserID string `json:"userId"`
|
||||
Type Scope `json:"folderType"`
|
||||
}
|
||||
|
||||
// Scope determines folder visibility.
|
||||
type Scope int
|
||||
|
||||
const (
|
||||
// ScopePublic can be seen by anyone
|
||||
ScopePublic Scope = 1
|
||||
|
||||
// ScopePrivate can only be seen by the person who owns it
|
||||
ScopePrivate Scope = 2
|
||||
|
||||
// ScopeRestricted can be seen by selected users
|
||||
ScopeRestricted Scope = 3
|
||||
)
|
||||
|
||||
// IsPublic means the folder can be seen by anyone.
|
||||
func (l *Space) IsPublic() bool {
|
||||
return l.Type == ScopePublic
|
||||
}
|
||||
|
||||
// IsPrivate means the folder can only be seen by the person who owns it.
|
||||
func (l *Space) IsPrivate() bool {
|
||||
return l.Type == ScopePrivate
|
||||
}
|
||||
|
||||
// IsRestricted means the folder can be seen by selected users.
|
||||
func (l *Space) IsRestricted() bool {
|
||||
return l.Type == ScopeRestricted
|
||||
}
|
||||
|
||||
// Role determines user permissions for a folder.
|
||||
type Role struct {
|
||||
domain.BaseEntityObfuscated
|
||||
OrgID string `json:"-"`
|
||||
LabelID string `json:"folderId"`
|
||||
UserID string `json:"userId"`
|
||||
CanView bool `json:"canView"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
}
|
||||
|
||||
// Viewer details who can see a particular space
|
||||
type Viewer struct {
|
||||
Name string `json:"name"`
|
||||
LabelID string `json:"folderId"`
|
||||
Type int `json:"folderType"`
|
||||
UserID string `json:"userId"`
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// RolesModel details which users have what permissions on a given space.
|
||||
type RolesModel struct {
|
||||
Message string
|
||||
Roles []Role
|
||||
}
|
||||
|
||||
// AcceptShareModel is used to setup a user who has accepted a shared space.
|
||||
type AcceptShareModel struct {
|
||||
Serial string `json:"serial"`
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// InvitationModel details which users have been invited to a space.
|
||||
type InvitationModel struct {
|
||||
Message string
|
||||
Recipients []string
|
||||
}
|
||||
|
|
|
@ -12,3 +12,51 @@
|
|||
// Package space handles API calls and persistence for spaces.
|
||||
// Spaces in Documize contain documents.
|
||||
package space
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/documize/community/domain"
|
||||
)
|
||||
|
||||
// CanViewSpace returns if the user has permission to view the given spaceID.
|
||||
func CanViewSpace(s domain.StoreContext, spaceID string) (hasPermission bool) {
|
||||
roles, err := GetRoles(s, spaceID)
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
s.Runtime.Log.Error(fmt.Sprintf("check space permissions %s", spaceID), err)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.LabelID == spaceID && (role.CanView || role.CanEdit) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CanViewSpaceDocuments returns if the user has permission to view a document within the specified space.
|
||||
func CanViewSpaceDocuments(s domain.StoreContext, spaceID string) (hasPermission bool) {
|
||||
roles, err := GetRoles(s, spaceID)
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.Runtime.Log.Error(fmt.Sprintf("check space permissions %s", spaceID), err)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.LabelID == spaceID && (role.CanView || role.CanEdit) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
103
domain/space/space.go
Normal file
103
domain/space/space.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
// 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 space
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/documize/community/core/api/mail"
|
||||
"github.com/documize/community/core/secrets"
|
||||
"github.com/documize/community/core/uniqueid"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/account"
|
||||
"github.com/documize/community/domain/user"
|
||||
)
|
||||
|
||||
// addSpace prepares and creates space record.
|
||||
func addSpace(s domain.StoreContext, sp Space) (err error) {
|
||||
sp.Type = ScopePrivate
|
||||
sp.UserID = s.Context.UserID
|
||||
|
||||
err = Add(s, sp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
role := Role{}
|
||||
role.LabelID = sp.RefID
|
||||
role.OrgID = sp.OrgID
|
||||
role.UserID = s.Context.UserID
|
||||
role.CanEdit = true
|
||||
role.CanView = true
|
||||
role.RefID = uniqueid.Generate()
|
||||
|
||||
err = AddRole(s, role)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Invite new user to a folder that someone has shared with them.
|
||||
// We create the user account with default values and then take them
|
||||
// through a welcome process designed to capture profile data.
|
||||
// We add them to the organization and grant them view-only folder access.
|
||||
func inviteNewUserToSharedSpace(s domain.StoreContext, email string, invitedBy user.User,
|
||||
baseURL string, sp Space, invitationMessage string) (err error) {
|
||||
|
||||
var u = user.User{}
|
||||
u.Email = email
|
||||
u.Firstname = email
|
||||
u.Lastname = ""
|
||||
u.Salt = secrets.GenerateSalt()
|
||||
requestedPassword := secrets.GenerateRandomPassword()
|
||||
u.Password = secrets.GeneratePassword(requestedPassword, u.Salt)
|
||||
userID := uniqueid.Generate()
|
||||
u.RefID = userID
|
||||
|
||||
err = user.Add(s, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Let's give this user access to the organization
|
||||
var a account.Account
|
||||
a.UserID = userID
|
||||
a.OrgID = s.Context.OrgID
|
||||
a.Admin = false
|
||||
a.Editor = false
|
||||
a.Active = true
|
||||
accountID := uniqueid.Generate()
|
||||
a.RefID = accountID
|
||||
|
||||
err = account.Add(s, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
role := Role{}
|
||||
role.LabelID = sp.RefID
|
||||
role.OrgID = s.Context.OrgID
|
||||
role.UserID = userID
|
||||
role.CanEdit = false
|
||||
role.CanView = true
|
||||
roleID := uniqueid.Generate()
|
||||
role.RefID = roleID
|
||||
|
||||
err = AddRole(s, role)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s", baseURL, u.Salt)
|
||||
go mail.ShareFolderNewUser(u.Email, invitedBy.Fullname(), url, sp.Name, invitationMessage)
|
||||
|
||||
return
|
||||
}
|
286
domain/space/store.go
Normal file
286
domain/space/store.go
Normal file
|
@ -0,0 +1,286 @@
|
|||
// 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 space handles API calls and persistence for spaces.
|
||||
// Spaces in Documize contain documents.
|
||||
package space
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/store/mysql"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Add adds new folder into the store.
|
||||
func Add(s domain.StoreContext, sp Space) (err error) {
|
||||
sp.UserID = s.Context.UserID
|
||||
sp.Created = time.Now().UTC()
|
||||
sp.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.Preparex("INSERT INTO label (refid, label, orgid, userid, type, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to prepare insert for label")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Created, sp.Revised)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to execute insert for label")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get returns a space from the store.
|
||||
func Get(s domain.StoreContext, id string) (sp Space, err error) {
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label WHERE orgid=? and refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for label %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&sp, s.Context.OrgID, id)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for label %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PublicSpaces returns folders that anyone can see.
|
||||
func PublicSpaces(s domain.StoreContext, orgID string) (sp []Space, err error) {
|
||||
sql := "SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=1"
|
||||
|
||||
err = s.Runtime.Db.Select(&sp, sql, orgID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("Unable to execute GetPublicFolders for org %s", orgID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetAll returns folders that the user can see.
|
||||
// Also handles which folders can be seen by anonymous users.
|
||||
func GetAll(s domain.StoreContext) (sp []Space, err error) {
|
||||
sql := `
|
||||
(SELECT id,refid,label as name,orgid,userid,type,created,revised from label WHERE orgid=? AND type=2 AND userid=?)
|
||||
UNION ALL
|
||||
(SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=1 AND refid in
|
||||
(SELECT labelid from labelrole WHERE orgid=? AND userid='' AND (canedit=1 OR canview=1)))
|
||||
UNION ALL
|
||||
(SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=3 AND refid in
|
||||
(SELECT labelid from labelrole WHERE orgid=? AND userid=? AND (canedit=1 OR canview=1)))
|
||||
ORDER BY name`
|
||||
|
||||
err = s.Runtime.Db.Select(&sp, sql,
|
||||
s.Context.OrgID,
|
||||
s.Context.UserID,
|
||||
s.Context.OrgID,
|
||||
s.Context.OrgID,
|
||||
s.Context.OrgID,
|
||||
s.Context.OrgID,
|
||||
s.Context.UserID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("Unable to execute select labels for org %s", s.Context.OrgID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Update saves space changes.
|
||||
func Update(s domain.StoreContext, sp Space) (err error) {
|
||||
sp.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE label SET label=:name, type=:type, userid=:userid, revised=:revised WHERE orgid=:orgid AND refid=:refid")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare update for label %s", sp.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&sp)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute update for label %s", sp.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ChangeOwner transfer space ownership.
|
||||
func ChangeOwner(s domain.StoreContext, currentOwner, newOwner string) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("UPDATE label SET userid=? WHERE userid=? AND orgid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare change space owner for %s", currentOwner))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(newOwner, currentOwner, s.Context.OrgID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute change space owner for %s", currentOwner))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Viewers returns the list of people who can see shared folders.
|
||||
func Viewers(s domain.StoreContext) (v []Viewer, err error) {
|
||||
sql := `
|
||||
SELECT a.userid,
|
||||
COALESCE(u.firstname, '') as firstname,
|
||||
COALESCE(u.lastname, '') as lastname,
|
||||
COALESCE(u.email, '') as email,
|
||||
a.labelid,
|
||||
b.label as name,
|
||||
b.type
|
||||
FROM labelrole a
|
||||
LEFT JOIN label b ON b.refid=a.labelid
|
||||
LEFT JOIN user u ON u.refid=a.userid
|
||||
WHERE a.orgid=? AND b.type != 2
|
||||
GROUP BY a.labelid,a.userid
|
||||
ORDER BY u.firstname,u.lastname`
|
||||
|
||||
err = s.Runtime.Db.Select(&v, sql, s.Context.OrgID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Delete removes space from the store.
|
||||
func Delete(s domain.StoreContext, id string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
return b.DeleteConstrained(s.Context.Transaction, "label", s.Context.OrgID, id)
|
||||
}
|
||||
|
||||
// AddRole inserts the given record into the labelrole database table.
|
||||
func AddRole(s domain.StoreContext, r Role) (err error) {
|
||||
r.Created = time.Now().UTC()
|
||||
r.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.Preparex("INSERT INTO labelrole (refid, labelid, orgid, userid, canview, canedit, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to prepare insert for space role")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(r.RefID, r.LabelID, r.OrgID, r.UserID, r.CanView, r.CanEdit, r.Created, r.Revised)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to execute insert for space role")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetRoles returns a slice of labelrole records, for the given labelID in the client's organization, grouped by user.
|
||||
func GetRoles(s domain.StoreContext, labelID string) (r []Role, err error) {
|
||||
query := `SELECT id, refid, labelid, orgid, userid, canview, canedit, created, revised FROM labelrole WHERE orgid=? AND labelid=?` // was + "GROUP BY userid"
|
||||
|
||||
err = s.Runtime.Db.Select(&r, query, s.Context.OrgID, labelID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for space roles %s", labelID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserRoles returns a slice of role records, for both the client's user and organization, and
|
||||
// those space roles that exist for all users in the client's organization.
|
||||
func GetUserRoles(s domain.StoreContext) (r []Role, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, refid, labelid, orgid, userid, canview, canedit, created, revised FROM labelrole WHERE orgid=? and userid=?
|
||||
UNION ALL
|
||||
SELECT id, refid, labelid, orgid, userid, canview, canedit, created, revised FROM labelrole WHERE orgid=? AND userid=''`,
|
||||
s.Context.OrgID, s.Context.UserID, s.Context.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for user space roles %s", s.Context.UserID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteRole deletes the labelRoleID record from the labelrole table.
|
||||
func DeleteRole(s domain.StoreContext, roleID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
|
||||
sql := fmt.Sprintf("DELETE FROM labelrole WHERE orgid='%s' AND refid='%s'", s.Context.OrgID, roleID)
|
||||
|
||||
return b.DeleteWhere(s.Context.Transaction, sql)
|
||||
}
|
||||
|
||||
// DeleteSpaceRoles deletes records from the labelrole table which have the given space ID.
|
||||
func DeleteSpaceRoles(s domain.StoreContext, spaceID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
|
||||
sql := fmt.Sprintf("DELETE FROM labelrole WHERE orgid='%s' AND labelid='%s'", s.Context.OrgID, spaceID)
|
||||
|
||||
return b.DeleteWhere(s.Context.Transaction, sql)
|
||||
}
|
||||
|
||||
// DeleteUserSpaceRoles removes all roles for the specified user, for the specified space.
|
||||
func DeleteUserSpaceRoles(s domain.StoreContext, spaceID, userID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
|
||||
sql := fmt.Sprintf("DELETE FROM labelrole WHERE orgid='%s' AND labelid='%s' AND userid='%s'",
|
||||
s.Context.OrgID, spaceID, userID)
|
||||
|
||||
return b.DeleteWhere(s.Context.Transaction, sql)
|
||||
}
|
||||
|
||||
// MoveSpaceRoles changes the space ID for space role records from previousLabel to newLabel.
|
||||
func MoveSpaceRoles(s domain.StoreContext, previousLabel, newLabel string) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("UPDATE labelrole SET labelid=? WHERE labelid=? AND orgid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare move space roles for label %s", previousLabel))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(newLabel, previousLabel, s.Context.OrgID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute move space roles for label %s", previousLabel))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -1,14 +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 space handles API calls and persistence for spaces.
|
||||
// Spaces in Documize contain documents.
|
||||
package space
|
|
@ -1,14 +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 space handles API calls and persistence for spaces.
|
||||
// Spaces in Documize contain documents.
|
||||
package space
|
100
domain/store/mysql/mysql.go
Normal file
100
domain/store/mysql/mysql.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
// 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 mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// BaseQuery provides common MySQL methods.
|
||||
type BaseQuery struct {
|
||||
}
|
||||
|
||||
// Delete record.
|
||||
func (m *BaseQuery) Delete(tx *sqlx.Tx, table string, id string) (rows int64, err error) {
|
||||
stmt, err := tx.Preparex("DELETE FROM " + table + " WHERE refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare delete of row in table %s", table))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := stmt.Exec(id)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
|
||||
return
|
||||
}
|
||||
|
||||
rows, err = result.RowsAffected()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteConstrained record constrained to Organization using refid.
|
||||
func (m *BaseQuery) DeleteConstrained(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) {
|
||||
stmt, err := tx.Preparex("DELETE FROM " + table + " WHERE orgid=? AND refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare constrained delete of row in table %s", table))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := stmt.Exec(orgID, id)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
|
||||
return
|
||||
}
|
||||
|
||||
rows, err = result.RowsAffected()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteConstrainedWithID record constrained to Organization using non refid.
|
||||
func (m *BaseQuery) DeleteConstrainedWithID(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) {
|
||||
stmt, err := tx.Preparex("DELETE FROM " + table + " WHERE orgid=? AND id=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare ConstrainedWithID delete of row in table %s", table))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := stmt.Exec(orgID, id)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
|
||||
return
|
||||
}
|
||||
|
||||
rows, err = result.RowsAffected()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteWhere free form query.
|
||||
func (m *BaseQuery) DeleteWhere(tx *sqlx.Tx, statement string) (rows int64, err error) {
|
||||
result, err := tx.Exec(statement)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to delete rows: %s", statement))
|
||||
return
|
||||
}
|
||||
|
||||
rows, err = result.RowsAffected()
|
||||
|
||||
return
|
||||
}
|
65
domain/user/model.go
Normal file
65
domain/user/model.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
// 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 user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/account"
|
||||
)
|
||||
|
||||
// Handler contains the runtime information such as logging and database.
|
||||
type Handler struct {
|
||||
Runtime env.Runtime
|
||||
}
|
||||
|
||||
// User defines a login.
|
||||
type User struct {
|
||||
domain.BaseEntity
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
Email string `json:"email"`
|
||||
Initials string `json:"initials"`
|
||||
Active bool `json:"active"`
|
||||
Editor bool `json:"editor"`
|
||||
Admin bool `json:"admin"`
|
||||
Global bool `json:"global"`
|
||||
Password string `json:"-"`
|
||||
Salt string `json:"-"`
|
||||
Reset string `json:"-"`
|
||||
Accounts []account.Account `json:"accounts"`
|
||||
}
|
||||
|
||||
// ProtectSecrets blanks sensitive data.
|
||||
func (user *User) ProtectSecrets() {
|
||||
user.Password = ""
|
||||
user.Salt = ""
|
||||
user.Reset = ""
|
||||
}
|
||||
|
||||
// Fullname returns Firstname + Lastname.
|
||||
func (user *User) Fullname() string {
|
||||
return fmt.Sprintf("%s %s", user.Firstname, user.Lastname)
|
||||
}
|
||||
|
||||
// GetAccount returns matching org account using orgID
|
||||
func (user *User) GetAccount(orgID string) (a account.Account, found bool) {
|
||||
for _, a := range user.Accounts {
|
||||
if a.OrgID == orgID {
|
||||
return a, true
|
||||
}
|
||||
}
|
||||
|
||||
return a, false
|
||||
}
|
294
domain/user/store.go
Normal file
294
domain/user/store.go
Normal file
|
@ -0,0 +1,294 @@
|
|||
// 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 user
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/streamutil"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Add adds the given user record to the user table.
|
||||
func Add(s domain.StoreContext, u User) (err error) {
|
||||
u.Created = time.Now().UTC()
|
||||
u.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := s.Context.Transaction.Preparex("INSERT INTO user (refid, firstname, lastname, email, initials, password, salt, reset, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "prepare user insert")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(u.RefID, u.Firstname, u.Lastname, strings.ToLower(u.Email), u.Initials, u.Password, u.Salt, "", u.Created, u.Revised)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "execute user insert")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get returns the user record for the given id.
|
||||
func Get(s domain.StoreContext, id string) (u User, err error) {
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for user %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&u, id)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for user %s", id))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetByDomain matches user by email and domain.
|
||||
func GetByDomain(s domain.StoreContext, domain, email string) (u User, err error) {
|
||||
email = strings.TrimSpace(strings.ToLower(email))
|
||||
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.global, u.password, u.salt, u.reset, u.created, u.revised FROM user u, account a, organization o WHERE TRIM(LOWER(u.email))=? AND u.refid=a.userid AND a.orgid=o.refid AND TRIM(LOWER(o.domain))=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("Unable to prepare GetUserByDomain %s %s", domain, email))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&u, email, domain)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, fmt.Sprintf("Unable to execute GetUserByDomain %s %s", domain, email))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetByEmail returns a single row match on email.
|
||||
func GetByEmail(s domain.StoreContext, email string) (u User, err error) {
|
||||
email = strings.TrimSpace(strings.ToLower(email))
|
||||
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE TRIM(LOWER(email))=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare select user by email %s", email))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&u, email)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute select user by email %s", email))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetByToken returns a user record given a reset token value.
|
||||
func GetByToken(s domain.StoreContext, token string) (u User, err error) {
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE reset=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare user select by token %s", token))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&u, token)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute user select by token %s", token))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetBySerial is used to retrieve a user via their temporary password salt value!
|
||||
// This occurs when we you share a folder with a new user and they have to complete
|
||||
// the onboarding process.
|
||||
func GetBySerial(s domain.StoreContext, serial string) (u User, err error) {
|
||||
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE salt=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare user select by serial %s", serial))
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&u, serial)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute user select by serial %s", serial))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetActiveUsersForOrganization returns a slice containing of active user records for the organization
|
||||
// identified in the Persister.
|
||||
func GetActiveUsersForOrganization(s domain.StoreContext) (u []User, err error) {
|
||||
err = s.Runtime.Db.Select(&u,
|
||||
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
|
||||
FROM user u
|
||||
WHERE u.refid IN (SELECT userid FROM account WHERE orgid = ? AND active=1) ORDER BY u.firstname,u.lastname`,
|
||||
s.Context.OrgID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", s.Context.OrgID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUsersForOrganization returns a slice containing all of the user records for the organizaiton
|
||||
// identified in the Persister.
|
||||
func GetUsersForOrganization(s domain.StoreContext) (u []User, err error) {
|
||||
err = s.Runtime.Db.Select(&u,
|
||||
"SELECT id, refid, firstname, lastname, email, initials, password, salt, reset, created, revised FROM user WHERE refid IN (SELECT userid FROM account where orgid = ?) ORDER BY firstname,lastname", s.Context.OrgID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf(" get users for org %s", s.Context.OrgID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetSpaceUsers returns a slice containing all user records for given folder.
|
||||
func GetSpaceUsers(s domain.StoreContext, folderID string) (u []User, err error) {
|
||||
err = s.Runtime.Db.Select(&u,
|
||||
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
|
||||
FROM user u, account a
|
||||
WHERE u.refid IN (SELECT userid from labelrole WHERE orgid=? AND labelid=?)
|
||||
AND a.orgid=? AND u.refid = a.userid AND a.active=1
|
||||
ORDER BY u.firstname, u.lastname`,
|
||||
s.Context.OrgID, folderID, s.Context.OrgID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("get space users for org %s", s.Context.OrgID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateUser updates the user table using the given replacement user record.
|
||||
func UpdateUser(s domain.StoreContext, u User) (err error) {
|
||||
u.Revised = time.Now().UTC()
|
||||
u.Email = strings.ToLower(u.Email)
|
||||
|
||||
stmt, err := s.Context.Transaction.PrepareNamed(
|
||||
"UPDATE user SET firstname=:firstname, lastname=:lastname, email=:email, revised=:revised, initials=:initials WHERE refid=:refid")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("prepare user update %s", u.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&u)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("execute user update %s", u.RefID))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateUserPassword updates a user record with new password and salt values.
|
||||
func UpdateUserPassword(s domain.StoreContext, userID, salt, password string) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("UPDATE user SET salt=?, password=?, reset='' WHERE refid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "prepare user update")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(salt, password, userID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "execute user update")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeactiveUser deletes the account record for the given userID and persister.Context.OrgID.
|
||||
func DeactiveUser(s domain.StoreContext, userID string) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("DELETE FROM account WHERE userid=? and orgid=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "prepare user deactivation")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(userID, s.Context.OrgID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "execute user deactivation")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ForgotUserPassword sets the password to '' and the reset field to token, for a user identified by email.
|
||||
func ForgotUserPassword(s domain.StoreContext, email, token string) (err error) {
|
||||
stmt, err := s.Context.Transaction.Preparex("UPDATE user SET reset=?, password='' WHERE LOWER(email)=?")
|
||||
defer streamutil.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "prepare password reset")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(token, strings.ToLower(email))
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "execute password reset")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CountActiveUsers returns the number of active users in the system.
|
||||
func CountActiveUsers(s domain.StoreContext) (c int) {
|
||||
row := s.Runtime.Db.QueryRow("SELECT count(*) FROM user u WHERE u.refid IN (SELECT userid FROM account WHERE active=1)")
|
||||
|
||||
err := row.Scan(&c)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return 0
|
||||
}
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
s.Runtime.Log.Error("CountActiveUsers", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
51
domain/user/user.go
Normal file
51
domain/user/user.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// 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 user
|
||||
|
||||
import (
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/account"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// GetSecuredUser contain associated accounts but credentials are wiped.
|
||||
func GetSecuredUser(s domain.StoreContext, orgID, q string) (u User, err error) {
|
||||
u, err = Get(s, q)
|
||||
AttachUserAccounts(s, orgID, &u)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AttachUserAccounts attachs user accounts to user object.
|
||||
func AttachUserAccounts(s domain.StoreContext, orgID string, u *User) {
|
||||
u.ProtectSecrets()
|
||||
|
||||
a, err := account.GetUserAccounts(s, u.RefID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "fetch user accounts")
|
||||
return
|
||||
}
|
||||
|
||||
u.Accounts = a
|
||||
u.Editor = false
|
||||
u.Admin = false
|
||||
u.Active = false
|
||||
|
||||
for _, account := range u.Accounts {
|
||||
if account.OrgID == orgID {
|
||||
u.Admin = account.Admin
|
||||
u.Editor = account.Editor
|
||||
u.Active = account.Active
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/documize/community/core/database"
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/secrets"
|
||||
"github.com/documize/community/server/web"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
|
@ -66,7 +65,7 @@ func InitRuntime(r *env.Runtime) bool {
|
|||
}
|
||||
|
||||
// go into setup mode if required
|
||||
if r.Flags.SiteMode != web.SiteModeOffline {
|
||||
if r.Flags.SiteMode != env.SiteModeOffline {
|
||||
if database.Check(r) {
|
||||
if err := database.Migrate(*r, true /* the config table exists */); err != nil {
|
||||
r.Log.Error("unable to run database migration", err)
|
||||
|
|
|
@ -38,8 +38,8 @@ func main() {
|
|||
// product details
|
||||
rt.Product = env.ProdInfo{}
|
||||
rt.Product.Major = "1"
|
||||
rt.Product.Minor = "50"
|
||||
rt.Product.Patch = "2"
|
||||
rt.Product.Minor = "51"
|
||||
rt.Product.Patch = "0"
|
||||
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
|
||||
rt.Product.Edition = "Community"
|
||||
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "documize",
|
||||
"version": "1.50.2",
|
||||
"version": "1.51.0",
|
||||
"description": "The Document IDE",
|
||||
"private": true,
|
||||
"repository": "",
|
||||
|
|
10
meta.json
10
meta.json
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"community":
|
||||
{
|
||||
"version": "1.50.2",
|
||||
"version": "1.51.0",
|
||||
"major": 1,
|
||||
"minor": 50,
|
||||
"patch": 2
|
||||
"patch": 0
|
||||
},
|
||||
"enterprise":
|
||||
{
|
||||
"version": "1.52.2",
|
||||
"version": "1.53.0",
|
||||
"major": 1,
|
||||
"minor": 52,
|
||||
"patch": 2
|
||||
"minor": 53,
|
||||
"patch": 0
|
||||
}
|
||||
}
|
210
server/middleware.go
Normal file
210
server/middleware.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
// 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 server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/response"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/auth"
|
||||
"github.com/documize/community/domain/organization"
|
||||
"github.com/documize/community/domain/user"
|
||||
)
|
||||
|
||||
type middleware struct {
|
||||
Runtime env.Runtime
|
||||
}
|
||||
|
||||
func (m *middleware) cors(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS, PATCH")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "host, content-type, accept, authorization, origin, referer, user-agent, cache-control, x-requested-with")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "x-documize-version, x-documize-status")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.Header().Add("X-Documize-Version", m.Runtime.Product.Version)
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
|
||||
w.Write([]byte(""))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
func (m *middleware) metrics(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
w.Header().Add("X-Documize-Version", m.Runtime.Product.Version)
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
|
||||
// Prevent page from being displayed in an iframe
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
// Authorize secure API calls by inspecting authentication token.
|
||||
// request.Context provides caller user information.
|
||||
// Site meta sent back as HTTP custom headers.
|
||||
func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
method := "Authorize"
|
||||
|
||||
s := domain.StoreContext{Runtime: m.Runtime, Context: domain.RequestContext{}}
|
||||
|
||||
// Let certain requests pass straight through
|
||||
authenticated := preAuthorizeStaticAssets(m.Runtime, r)
|
||||
|
||||
if !authenticated {
|
||||
token := auth.FindJWT(r)
|
||||
rc, _, tokenErr := auth.DecodeJWT(m.Runtime, token)
|
||||
|
||||
var org = organization.Organization{}
|
||||
var err = errors.New("")
|
||||
|
||||
if len(rc.OrgID) == 0 {
|
||||
org, err = organization.GetOrganizationByDomain(s, organization.GetRequestSubdomain(s, r))
|
||||
} else {
|
||||
org, err = organization.GetOrganization(s, rc.OrgID)
|
||||
}
|
||||
|
||||
// Inability to find org record spells the end of this request.
|
||||
if err != nil {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have bad auth token and the domain does not allow anon access
|
||||
if !org.AllowAnonymousAccess && tokenErr != nil {
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
||||
rc.Subdomain = org.Domain
|
||||
dom := organization.GetSubdomainFromHost(s, r)
|
||||
dom2 := organization.GetRequestSubdomain(s, r)
|
||||
|
||||
if org.Domain != dom && org.Domain != dom2 {
|
||||
m.Runtime.Log.Info(fmt.Sprintf("domain mismatch %s vs. %s vs. %s", dom, dom2, org.Domain))
|
||||
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have bad auth token and the domain allows anon access
|
||||
// then we generate guest context.
|
||||
if org.AllowAnonymousAccess {
|
||||
// So you have a bad token
|
||||
if len(token) > 1 {
|
||||
if tokenErr != nil {
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Just grant anon user guest access
|
||||
rc.UserID = "0"
|
||||
rc.OrgID = org.RefID
|
||||
rc.Authenticated = false
|
||||
rc.Guest = true
|
||||
}
|
||||
}
|
||||
|
||||
rc.AllowAnonymousAccess = org.AllowAnonymousAccess
|
||||
rc.OrgName = org.Title
|
||||
rc.Administrator = false
|
||||
rc.Editor = false
|
||||
rc.Global = false
|
||||
rc.AppURL = r.Host
|
||||
rc.Subdomain = organization.GetSubdomainFromHost(s, r)
|
||||
rc.SSL = r.TLS != nil
|
||||
|
||||
// get user IP from request
|
||||
i := strings.LastIndex(r.RemoteAddr, ":")
|
||||
if i == -1 {
|
||||
rc.ClientIP = r.RemoteAddr
|
||||
} else {
|
||||
rc.ClientIP = r.RemoteAddr[:i]
|
||||
}
|
||||
|
||||
fip := r.Header.Get("X-Forwarded-For")
|
||||
if len(fip) > 0 {
|
||||
rc.ClientIP = fip
|
||||
}
|
||||
|
||||
// Fetch user permissions for this org
|
||||
if rc.Authenticated {
|
||||
u, err := user.GetSecuredUser(s, org.RefID, rc.UserID)
|
||||
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
rc.Administrator = u.Admin
|
||||
rc.Editor = u.Editor
|
||||
rc.Global = u.Global
|
||||
rc.Fullname = u.Fullname()
|
||||
|
||||
// We send back with every HTTP request/response cycle the latest
|
||||
// user state. This helps client-side applications to detect changes in
|
||||
// user state/privileges.
|
||||
var state struct {
|
||||
Active bool `json:"active"`
|
||||
Admin bool `json:"admin"`
|
||||
Editor bool `json:"editor"`
|
||||
}
|
||||
|
||||
state.Active = u.Active
|
||||
state.Admin = u.Admin
|
||||
state.Editor = u.Editor
|
||||
sb, err := json.Marshal(state)
|
||||
|
||||
w.Header().Add("X-Documize-Status", string(sb))
|
||||
}
|
||||
|
||||
// m.Runtime.Log.Info(fmt.Sprintf("%v", rc))
|
||||
ctx := context.WithValue(r.Context(), domain.DocumizeContextKey, rc)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Middleware moves on if we say 'yes' -- authenticated or allow anon access.
|
||||
authenticated = rc.Authenticated || org.AllowAnonymousAccess
|
||||
}
|
||||
|
||||
if authenticated {
|
||||
next(w, r)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
// Certain assets/URL do not require authentication.
|
||||
// Just stops the log files being clogged up with failed auth errors.
|
||||
func preAuthorizeStaticAssets(rt env.Runtime, r *http.Request) bool {
|
||||
if strings.ToLower(r.URL.Path) == "/" ||
|
||||
strings.ToLower(r.URL.Path) == "/validate" ||
|
||||
strings.ToLower(r.URL.Path) == "/favicon.ico" ||
|
||||
strings.ToLower(r.URL.Path) == "/robots.txt" ||
|
||||
strings.ToLower(r.URL.Path) == "/version" ||
|
||||
strings.HasPrefix(strings.ToLower(r.URL.Path), "/api/public/") ||
|
||||
((rt.Flags.SiteMode == env.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -28,6 +28,8 @@ const (
|
|||
RoutePrefixPrivate = "/api/"
|
||||
// RoutePrefixRoot used for unsecured endpoints at root (e.g. robots.txt)
|
||||
RoutePrefixRoot = "/"
|
||||
// RoutePrefixTesting used for isolated testing of routes with custom middleware
|
||||
RoutePrefixTesting = "/testing/"
|
||||
)
|
||||
|
||||
type routeDef struct {
|
||||
|
|
|
@ -18,7 +18,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/documize/community/core/api"
|
||||
"github.com/documize/community/core/api/endpoint"
|
||||
"github.com/documize/community/core/api/plugins"
|
||||
"github.com/documize/community/core/database"
|
||||
|
@ -39,21 +38,24 @@ func Start(rt env.Runtime, ready chan struct{}) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
rt.Log.Info(fmt.Sprintf("Starting %s version %s", api.Runtime.Product.Title, api.Runtime.Product.Version))
|
||||
rt.Log.Info(fmt.Sprintf("Starting %s version %s", rt.Product.Title, rt.Product.Version))
|
||||
|
||||
// decide which mode to serve up
|
||||
switch api.Runtime.Flags.SiteMode {
|
||||
case web.SiteModeOffline:
|
||||
switch rt.Flags.SiteMode {
|
||||
case env.SiteModeOffline:
|
||||
rt.Log.Info("Serving OFFLINE web server")
|
||||
case web.SiteModeSetup:
|
||||
case env.SiteModeSetup:
|
||||
routing.Add(rt, routing.RoutePrefixPrivate, "setup", []string{"POST", "OPTIONS"}, nil, database.Create)
|
||||
rt.Log.Info("Serving SETUP web server")
|
||||
case web.SiteModeBadDB:
|
||||
case env.SiteModeBadDB:
|
||||
rt.Log.Info("Serving BAD DATABASE web server")
|
||||
default:
|
||||
rt.Log.Info("Starting web server")
|
||||
}
|
||||
|
||||
// define middleware
|
||||
cm := middleware{Runtime: rt}
|
||||
|
||||
// define API endpoints
|
||||
routing.RegisterEndpoints(rt)
|
||||
|
||||
|
@ -62,7 +64,7 @@ func Start(rt env.Runtime, ready chan struct{}) {
|
|||
|
||||
// "/api/public/..."
|
||||
router.PathPrefix(routing.RoutePrefixPublic).Handler(negroni.New(
|
||||
negroni.HandlerFunc(cors),
|
||||
negroni.HandlerFunc(cm.cors),
|
||||
negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixPublic)),
|
||||
))
|
||||
|
||||
|
@ -74,74 +76,46 @@ func Start(rt env.Runtime, ready chan struct{}) {
|
|||
|
||||
// "/..."
|
||||
router.PathPrefix(routing.RoutePrefixRoot).Handler(negroni.New(
|
||||
negroni.HandlerFunc(cors),
|
||||
negroni.HandlerFunc(cm.cors),
|
||||
negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixRoot)),
|
||||
))
|
||||
|
||||
n := negroni.New()
|
||||
n.Use(negroni.NewStatic(web.StaticAssetsFileSystem()))
|
||||
n.Use(negroni.HandlerFunc(cors))
|
||||
n.Use(negroni.HandlerFunc(metrics))
|
||||
n.Use(negroni.HandlerFunc(cm.cors))
|
||||
n.Use(negroni.HandlerFunc(cm.metrics))
|
||||
n.UseHandler(router)
|
||||
|
||||
// start server
|
||||
if !api.Runtime.Flags.SSLEnabled() {
|
||||
rt.Log.Info("Starting non-SSL server on " + api.Runtime.Flags.HTTPPort)
|
||||
n.Run(testHost + ":" + api.Runtime.Flags.HTTPPort)
|
||||
if !rt.Flags.SSLEnabled() {
|
||||
rt.Log.Info("Starting non-SSL server on " + rt.Flags.HTTPPort)
|
||||
n.Run(testHost + ":" + rt.Flags.HTTPPort)
|
||||
} else {
|
||||
if api.Runtime.Flags.ForceHTTPPort2SSL != "" {
|
||||
rt.Log.Info("Starting non-SSL server on " + api.Runtime.Flags.ForceHTTPPort2SSL + " and redirecting to SSL server on " + api.Runtime.Flags.HTTPPort)
|
||||
if rt.Flags.ForceHTTPPort2SSL != "" {
|
||||
rt.Log.Info("Starting non-SSL server on " + rt.Flags.ForceHTTPPort2SSL + " and redirecting to SSL server on " + rt.Flags.HTTPPort)
|
||||
|
||||
go func() {
|
||||
err := http.ListenAndServe(":"+api.Runtime.Flags.ForceHTTPPort2SSL, http.HandlerFunc(
|
||||
err := http.ListenAndServe(":"+rt.Flags.ForceHTTPPort2SSL, http.HandlerFunc(
|
||||
func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Connection", "close")
|
||||
var host = strings.Replace(req.Host, api.Runtime.Flags.ForceHTTPPort2SSL, api.Runtime.Flags.HTTPPort, 1) + req.RequestURI
|
||||
var host = strings.Replace(req.Host, rt.Flags.ForceHTTPPort2SSL, rt.Flags.HTTPPort, 1) + req.RequestURI
|
||||
http.Redirect(w, req, "https://"+host, http.StatusMovedPermanently)
|
||||
}))
|
||||
if err != nil {
|
||||
rt.Log.Error("ListenAndServe on "+api.Runtime.Flags.ForceHTTPPort2SSL, err)
|
||||
rt.Log.Error("ListenAndServe on "+rt.Flags.ForceHTTPPort2SSL, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
rt.Log.Info("Starting SSL server on " + api.Runtime.Flags.HTTPPort + " with " + api.Runtime.Flags.SSLCertFile + " " + api.Runtime.Flags.SSLKeyFile)
|
||||
rt.Log.Info("Starting SSL server on " + rt.Flags.HTTPPort + " with " + rt.Flags.SSLCertFile + " " + rt.Flags.SSLKeyFile)
|
||||
|
||||
// TODO: https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/
|
||||
|
||||
server := &http.Server{Addr: ":" + api.Runtime.Flags.HTTPPort, Handler: n /*, TLSConfig: myTLSConfig*/}
|
||||
server := &http.Server{Addr: ":" + rt.Flags.HTTPPort, Handler: n /*, TLSConfig: myTLSConfig*/}
|
||||
server.SetKeepAlivesEnabled(true)
|
||||
|
||||
if err := server.ListenAndServeTLS(api.Runtime.Flags.SSLCertFile, api.Runtime.Flags.SSLKeyFile); err != nil {
|
||||
rt.Log.Error("ListenAndServeTLS on "+api.Runtime.Flags.HTTPPort, err)
|
||||
if err := server.ListenAndServeTLS(rt.Flags.SSLCertFile, rt.Flags.SSLKeyFile); err != nil {
|
||||
rt.Log.Error("ListenAndServeTLS on "+rt.Flags.HTTPPort, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cors(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS, PATCH")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "host, content-type, accept, authorization, origin, referer, user-agent, cache-control, x-requested-with")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "x-documize-version, x-documize-status")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.Header().Add("X-Documize-Version", api.Runtime.Product.Version)
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
|
||||
w.Write([]byte(""))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
func metrics(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
w.Header().Add("X-Documize-Version", api.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)
|
||||
}
|
||||
|
|
|
@ -17,20 +17,10 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/documize/community/core/api"
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/secrets"
|
||||
)
|
||||
|
||||
const (
|
||||
// SiteModeNormal serves app
|
||||
SiteModeNormal = ""
|
||||
// SiteModeOffline serves offline.html
|
||||
SiteModeOffline = "1"
|
||||
// SiteModeSetup tells Ember to serve setup route
|
||||
SiteModeSetup = "2"
|
||||
// SiteModeBadDB redirects to db-error.html page
|
||||
SiteModeBadDB = "3"
|
||||
)
|
||||
|
||||
// SiteInfo describes set-up information about the site
|
||||
var SiteInfo struct {
|
||||
DBname, DBhash, Issue string
|
||||
|
@ -44,11 +34,11 @@ func init() {
|
|||
func EmberHandler(w http.ResponseWriter, r *http.Request) {
|
||||
filename := "index.html"
|
||||
switch api.Runtime.Flags.SiteMode {
|
||||
case SiteModeOffline:
|
||||
case env.SiteModeOffline:
|
||||
filename = "offline.html"
|
||||
case SiteModeSetup:
|
||||
case env.SiteModeSetup:
|
||||
// NoOp
|
||||
case SiteModeBadDB:
|
||||
case env.SiteModeBadDB:
|
||||
filename = "db-error.html"
|
||||
default:
|
||||
SiteInfo.DBhash = ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue