1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-23 15:19:42 +02:00

restructure directories

This commit is contained in:
Elliott Stoneham 2016-07-20 15:58:37 +01:00
parent 7e4ed6545b
commit a2ce777762
159 changed files with 320 additions and 323 deletions

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>TESTDATA Title</title>
</head>
<body>
<h1>TESTDATA Heading</h1>
<p>TESTDATA paragraph.</p>
</body>
</html>

View file

@ -0,0 +1,219 @@
// 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 endpoint
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"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/log"
uuid "github.com/nu7hatch/gouuid"
"github.com/gorilla/mux"
_ "github.com/mytrile/mime-ext" // this adds a large number of mime extensions
)
// AttachmentDownload is the end-point that responds to a request for a particular attachment
// by sending the requested file to the client.
func AttachmentDownload(w http.ResponseWriter, r *http.Request) {
method := "AttachmentDownload"
p := request.GetPersister(r)
params := mux.Vars(r)
attachment, err := p.GetAttachmentByJobAndFileID(params["orgID"], params["job"], params["fileID"])
if err == sql.ErrNoRows {
writeNotFoundError(w, method, params["fileID"])
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
typ := mime.TypeByExtension("." + attachment.Extension)
if typ == "" {
typ = "application/octet-stream"
}
w.Header().Set("Content-Type", typ)
w.Header().Set("Content-Disposition", `Attachment; filename="`+attachment.Filename+`" ; `+`filename*="`+attachment.Filename+`"`)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(attachment.Data)))
w.WriteHeader(http.StatusOK)
_, err = w.Write(attachment.Data)
log.IfErr(err)
}
// GetAttachments is an end-point that returns all of the attachments of a particular documentID.
func GetAttachments(w http.ResponseWriter, r *http.Request) {
method := "GetAttachments"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanViewDocument(documentID) {
writeForbiddenError(w)
return
}
a, err := p.GetAttachments(documentID)
if err != nil && err != sql.ErrNoRows {
writeGeneralSQLError(w, method, err)
return
}
json, err := json.Marshal(a)
if err != nil {
writeJSONMarshalError(w, method, "attachments", err)
return
}
writeSuccessBytes(w, json)
}
// DeleteAttachment is an endpoint that deletes a particular document attachment.
func DeleteAttachment(w http.ResponseWriter, r *http.Request) {
method := "DeleteAttachment"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
attachmentID := params["attachmentID"]
if len(documentID) == 0 || len(attachmentID) == 0 {
writeMissingDataError(w, method, "documentID, attachmentID")
return
}
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
_, err = p.DeleteAttachment(attachmentID)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// AddAttachments stores files against a document.
func AddAttachments(w http.ResponseWriter, r *http.Request) {
method := "AddAttachments"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanChangeDocument(documentID) {
w.WriteHeader(http.StatusForbidden)
return
}
filedata, filename, err := r.FormFile("attachment")
if err != nil {
writeMissingDataError(w, method, "attachment")
return
}
b := new(bytes.Buffer)
_, err = io.Copy(b, filedata)
if err != nil {
writeServerError(w, method, err)
return
}
var job = "some-uuid"
newUUID, err := uuid.NewV4()
if err != nil {
writeServerError(w, method, err)
return
}
job = newUUID.String()
var a entity.Attachment
refID := util.UniqueID()
a.RefID = refID
a.DocumentID = documentID
a.Job = job
random := util.GenerateSalt()
a.FileID = random[0:9]
a.Filename = filename.Filename
a.Data = b.Bytes()
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
err = p.AddAttachment(a)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}

View file

@ -0,0 +1,431 @@
// 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 endpoint
import (
"crypto/rand"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/documize/community/core/api/endpoint/models"
"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/section/provider"
"github.com/documize/community/core/web"
"github.com/documize/community/core/environment"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
)
// Authenticate user based up HTTP Authorization header.
// An encrypted authentication token is issued with an expiry date.
func Authenticate(w http.ResponseWriter, r *http.Request) {
method := "Authenticate"
p := request.GetPersister(r)
authHeader := r.Header.Get("Authorization")
// check for http header
if len(authHeader) == 0 {
writeBadRequestError(w, method, "Missing Authorization header")
return
}
// decode what we received
data := strings.Replace(authHeader, "Basic ", "", 1)
decodedBytes, err := utility.DecodeBase64([]byte(data))
if err != nil {
writeBadRequestError(w, method, "Unable to decode authentication token")
return
}
// check that we have domain:email:password (but allow for : in password field!)
decoded := string(decodedBytes)
credentials := strings.SplitN(decoded, ":", 3)
if len(credentials) != 3 {
writeBadRequestError(w, method, "Bad authentication token, expecting domain:email:password")
return
}
domain := strings.TrimSpace(strings.ToLower(credentials[0]))
domain = request.CheckDomain(domain) // TODO optimize by removing this once js allows empty domains
email := strings.TrimSpace(strings.ToLower(credentials[1]))
password := credentials[2]
log.Info("logon attempt for " + domain + " @ " + email)
user, err := p.GetUserByDomain(domain, email)
if err == sql.ErrNoRows {
writeUnauthorizedError(w)
return
}
if err != nil {
writeServerError(w, method, err)
return
}
if !user.Active || len(user.Reset) > 0 || len(user.Password) == 0 {
writeUnauthorizedError(w)
return
}
// Password correct and active user
if email != strings.TrimSpace(strings.ToLower(user.Email)) || !util.MatchPassword(user.Password, password, user.Salt) {
writeUnauthorizedError(w)
return
}
org, err := p.GetOrganizationByDomain(domain)
if err != nil {
writeUnauthorizedError(w)
return
}
// Attach user accounts and work out permissions
attachUserAccounts(p, org.RefID, &user)
if len(user.Accounts) == 0 {
writeUnauthorizedError(w)
return
}
authModel := models.AuthenticationModel{}
authModel.Token = generateJWT(user.RefID, org.RefID, domain)
authModel.User = user
json, err := json.Marshal(authModel)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
writeSuccessBytes(w, json)
}
// Authorize secure API calls by inspecting authentication token.
// request.Context provides caller user information.
// Site meta sent back as HTTP custom headers.
func Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
method := "Authorize"
// Let certain requests pass straight through
authenticated := preAuthorizeStaticAssets(r)
if !authenticated {
token := findJWT(r)
hasToken := len(token) > 1
context, _, tokenErr := decodeJWT(token)
var org = entity.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.
p := request.GetPersister(r)
if len(context.OrgID) == 0 {
org, err = p.GetOrganizationByDomain(request.GetRequestSubdomain(r))
} else {
org, err = p.GetOrganization(context.OrgID)
}
context.Subdomain = org.Domain
// Inability to find org record spells the end of this request.
if err != nil {
writeForbiddenError(w)
return
}
// If we have bad auth token and the domain does not allow anon access
if !org.AllowAnonymousAccess && tokenErr != nil {
writeUnauthorizedError(w)
return
}
domain := request.GetSubdomainFromHost(r)
if org.Domain != domain {
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 hasToken {
if tokenErr != nil {
writeUnauthorizedError(w)
return
}
} else {
// Just grant anon user guest access
context.UserID = "0"
context.OrgID = org.RefID
context.Authenticated = false
context.Guest = true
}
}
// Refresh context and persister
request.SetContext(r, context)
p = request.GetPersister(r)
context.AllowAnonymousAccess = org.AllowAnonymousAccess
context.OrgName = org.Title
context.Administrator = false
context.Editor = false
// Fetch user permissions for this org
if context.Authenticated {
user, err := getSecuredUser(p, org.RefID, context.UserID)
if err != nil {
writeServerError(w, method, err)
return
}
context.Administrator = user.Admin
context.Editor = user.Editor
}
request.SetContext(r, context)
p = request.GetPersister(r)
// Middleware moves on if we say 'yes' -- autheticated or allow anon access.
authenticated = context.Authenticated || org.AllowAnonymousAccess
}
if authenticated {
next(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
// ValidateAuthToken checks the auth token and returns the corresponding user.
func 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 {
log.Error("section validation failure", err)
w.WriteHeader(http.StatusUnauthorized)
}
return
}
method := "ValidateAuthToken"
context, claims, err := decodeJWT(findJWT(r))
if err != nil {
log.Error("token validation", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
request.SetContext(r, context)
p := request.GetPersister(r)
org, err := p.GetOrganization(context.OrgID)
if err != nil {
log.Error("token validation", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
domain := request.GetSubdomainFromHost(r)
if org.Domain != domain || claims["domain"] != domain {
log.Error("token validation", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
user, err := getSecuredUser(p, context.OrgID, context.UserID)
if err != nil {
log.Error("get user error for token validation", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
json, err := json.Marshal(user)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
writeSuccessBytes(w, json)
}
// Certain assets/URL do not require authentication.
// Just stops the log files being clogged up with failed auth errors.
func preAuthorizeStaticAssets(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/") ||
((web.SiteMode == web.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) {
return true
}
return false
}
var jwtKey string
func init() {
environment.GetString(&jwtKey, "salt", false, "the salt string used to encode JWT tokens, if not set a random value will be generated",
func(t *string, n string) bool {
if jwtKey == "" {
b := make([]byte, 17)
_, err := rand.Read(b)
if err != nil {
jwtKey = err.Error()
log.Error("problem using crypto/rand", err)
return false
}
for k, v := range b {
if (v >= 'a' && v <= 'z') || (v >= 'A' && v <= 'Z') || (v >= '0' && v <= '0') {
b[k] = v
} else {
s := fmt.Sprintf("%x", v)
b[k] = s[0]
}
}
jwtKey = string(b)
log.Info("Please set DOCUMIZESALT or use -salt with this value: " + jwtKey)
}
return true
})
}
// Generates JSON Web Token (http://jwt.io)
func generateJWT(user, org, domain string) string {
token := jwt.New(jwt.SigningMethodHS256)
// issuer
token.Claims["iss"] = "Documize"
// subject
token.Claims["sub"] = "webapp"
// expiry
token.Claims["exp"] = time.Now().Add(time.Hour * 168).Unix()
// data
token.Claims["user"] = user
token.Claims["org"] = org
token.Claims["domain"] = domain
tokenString, _ := token.SignedString([]byte(jwtKey))
return tokenString
}
// Check for authorization token.
// We look 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
}
// We take in raw token string and decode it.
func decodeJWT(tokenString string) (c request.Context, claims map[string]interface{}, err error) {
method := "decodeJWT"
// 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(jwtKey), 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 {
log.Error("invalid token", err)
err = fmt.Errorf("bad token")
return
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
log.Error("expired token", err)
err = fmt.Errorf("expired token")
return
} else {
log.Error("invalid token", err)
err = fmt.Errorf("bad token")
return
}
} else {
log.Error("invalid token", err)
err = fmt.Errorf("bad token")
return
}
}
c = request.NewContext()
c.UserID = token.Claims["user"].(string)
c.OrgID = token.Claims["org"].(string)
if len(c.UserID) == 0 || len(c.OrgID) == 0 {
err = fmt.Errorf("%s : unable parse token data", method)
return
}
c.Authenticated = true
c.Guest = false
return c, token.Claims, nil
}

View file

@ -0,0 +1,253 @@
// 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 endpoint
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/documize/community/core/api/endpoint/models"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/store"
"github.com/documize/community/core/api/util"
api "github.com/documize/community/core/convapi"
"github.com/documize/community/core/log"
uuid "github.com/nu7hatch/gouuid"
"github.com/gorilla/mux"
)
func uploadDocument(w http.ResponseWriter, r *http.Request) (string, string, string) {
method := "uploadDocument"
p := request.GetPersister(r)
params := mux.Vars(r)
folderID := params["folderID"]
if !p.CanUploadDocument(folderID) {
writeForbiddenError(w)
return "", "", ""
}
// grab file
filedata, filename, err := r.FormFile("attachment")
if err != nil {
writeMissingDataError(w, method, "attachment")
return "", "", ""
}
b := new(bytes.Buffer)
_, err = io.Copy(b, filedata)
if err != nil {
writeServerError(w, method, err)
return "", "", ""
}
// generate job id
var job = "some-uuid"
newUUID, err := uuid.NewV4()
if err != nil {
writeServerError(w, method, err)
return "", "", ""
}
job = newUUID.String()
err = storageProvider.Upload(job, filename.Filename, b.Bytes())
if err != nil {
writeServerError(w, method, err)
return "", "", ""
}
log.Info(fmt.Sprintf("Org %s (%s) [Uploaded] %s", p.Context.OrgName, p.Context.OrgID, filename.Filename))
return job, folderID, p.Context.OrgID
}
func convertDocument(w http.ResponseWriter, r *http.Request, job, folderID string, conversion api.ConversionJobRequest) {
method := "convertDocument"
p := request.GetPersister(r)
var fileResult *api.DocumentConversionResponse
var filename string
var err error
filename, fileResult, err = storageProvider.Convert(conversion)
if err != nil {
writePayloadError(w, method, err)
return
}
if fileResult.Err != "" {
writeGeneralSQLError(w, method, errors.New(fileResult.Err))
return
}
// NOTE: empty .docx documents trigger this error
if len(fileResult.Pages) == 0 {
writeMissingDataError(w, method, "no pages in document")
return
}
// All the commented-out code below should be in following function call
newDocument, err := processDocument(p, filename, job, folderID, fileResult)
if err != nil {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(newDocument)
if err != nil {
writeJSONMarshalError(w, method, "conversion", err)
return
}
writeSuccessBytes(w, json)
}
// UploadConvertDocument is an endpoint to both upload and convert a document
func UploadConvertDocument(w http.ResponseWriter, r *http.Request) {
job, folderID, orgID := uploadDocument(w, r)
if job == "" {
return // error already handled
}
convertDocument(w, r, job, folderID, api.ConversionJobRequest{
Job: job,
IndexDepth: 4,
OrgID: orgID,
})
}
func processDocument(p request.Persister, filename, job, folderID string, fileResult *api.DocumentConversionResponse) (newDocument entity.Document, err error) {
// Convert into database objects
document := store.ConvertFileResult(filename, fileResult)
document.Job = job
document.OrgID = p.Context.OrgID
document.LabelID = folderID
document.UserID = p.Context.UserID
documentID := util.UniqueID()
document.RefID = documentID
tx, err := request.Db.Beginx()
log.IfErr(err)
p.Context.Transaction = tx
err = p.AddDocument(document)
if err != nil {
log.IfErr(tx.Rollback())
log.Error("Cannot insert new document", err)
return
}
//err = processPage(documentID, fileResult.PageFiles, fileResult.Pages.Children[0], 1, p)
for k, v := range fileResult.Pages {
var page entity.Page
page.OrgID = p.Context.OrgID
page.DocumentID = documentID
page.Level = v.Level
page.Title = v.Title
page.Body = string(v.Body)
page.Sequence = float64(k+1) * 1024.0 // need to start above 0 to allow insertion before the first item
pageID := util.UniqueID()
page.RefID = pageID
meta := entity.PageMeta{}
meta.PageID = pageID
meta.RawBody = page.Body
model := models.PageModel{}
model.Page = page
model.Meta = meta
err = p.AddPage(model)
if err != nil {
log.IfErr(tx.Rollback())
log.Error("Cannot process page newly added document", err)
return
}
}
for _, e := range fileResult.EmbeddedFiles {
//fmt.Println("DEBUG embedded file info", document.OrgId, document.Job, e.Name, len(e.Data), e.ID)
var a entity.Attachment
a.DocumentID = documentID
a.Job = document.Job
a.FileID = e.ID
a.Filename = strings.Replace(e.Name, "embeddings/", "", 1)
a.Data = e.Data
refID := util.UniqueID()
a.RefID = refID
err = p.AddAttachment(a)
if err != nil {
log.IfErr(tx.Rollback())
log.Error("Cannot add attachment for newly added document", err)
return
}
}
log.IfErr(tx.Commit())
newDocument, err = p.GetDocument(documentID)
if err != nil {
log.Error("Cannot fetch newly added document", err)
return
}
// New code from normal conversion code
tx, err = request.Db.Beginx()
if err != nil {
log.Error("Cannot begin a transatcion", err)
return
}
p.Context.Transaction = tx
err = p.UpdateDocument(newDocument) // TODO review - this seems to write-back an unaltered record from that read above, but within that it calls searches.UpdateDocument() to reindex the doc.
if err != nil {
log.IfErr(tx.Rollback())
log.Error("Cannot update an imported document", err)
return
}
log.IfErr(tx.Commit())
// End new code
return
}

13
core/api/endpoint/doc.go Normal file
View file

@ -0,0 +1,13 @@
// 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 endpoint provides API endpoints for Documize.
package endpoint

View file

@ -0,0 +1,384 @@
// 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 endpoint
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/plugins"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/store"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
"github.com/gorilla/mux"
)
// SearchDocuments endpoint takes a list of keywords and returns a list of document references matching those keywords.
func SearchDocuments(w http.ResponseWriter, r *http.Request) {
method := "SearchDocuments"
p := request.GetPersister(r)
query := r.URL.Query()
keywords := query.Get("keywords")
decoded, err := url.QueryUnescape(keywords)
log.IfErr(err)
results, err := p.SearchDocument(decoded)
if err != nil {
writeServerError(w, method, err)
return
}
// Put in slugs for easy UI display of search URL
for key, result := range results {
result.DocumentSlug = utility.MakeSlug(result.DocumentTitle)
result.FolderSlug = utility.MakeSlug(result.LabelName)
results[key] = result
}
if len(results) == 0 {
results = []entity.DocumentSearch{}
}
data, err := json.Marshal(results)
if err != nil {
writeJSONMarshalError(w, method, "search", err)
return
}
writeSuccessBytes(w, data)
}
// GetDocument is an endpoint that returns the document-level information for a given documentID.
func GetDocument(w http.ResponseWriter, r *http.Request) {
method := "GetDocument"
p := request.GetPersister(r)
params := mux.Vars(r)
id := params["documentID"]
if len(id) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
document, err := p.GetDocument(id)
if err == sql.ErrNoRows {
writeNotFoundError(w, method, id)
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
if !p.CanViewDocumentInFolder(document.LabelID) {
writeForbiddenError(w)
return
}
json, err := json.Marshal(document)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, json)
}
// GetDocumentMeta is an endpoint returning the metadata for a document.
func GetDocumentMeta(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentMeta"
p := request.GetPersister(r)
params := mux.Vars(r)
id := params["documentID"]
if len(id) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
meta, err := p.GetDocumentMeta(id)
if err == sql.ErrNoRows {
writeNotFoundError(w, method, id)
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
json, err := json.Marshal(meta)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, json)
}
// GetDocumentsByFolder is an endpoint that returns the documents in a given folder.
func GetDocumentsByFolder(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentsByFolder"
p := request.GetPersister(r)
query := r.URL.Query()
folderID := query.Get("folder")
if len(folderID) == 0 {
writeMissingDataError(w, method, "folder")
return
}
if !p.CanViewFolder(folderID) {
writeForbiddenError(w)
return
}
documents, err := p.GetDocumentsByFolder(folderID)
if err != nil && err != sql.ErrNoRows {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(documents)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, json)
}
// GetDocumentsByTag is an endpoint that returns the documents with a given tag.
func GetDocumentsByTag(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentsByTag"
p := request.GetPersister(r)
query := r.URL.Query()
tag := query.Get("tag")
if len(tag) == 0 {
writeMissingDataError(w, method, "tag")
return
}
documents, err := p.GetDocumentsByTag(tag)
if err != nil && err != sql.ErrNoRows {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(documents)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, json)
}
// DeleteDocument is an endpoint that deletes a document specified by documentID.
func DeleteDocument(w http.ResponseWriter, r *http.Request) {
method := "DeleteDocument"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
_, err = p.DeleteDocument(documentID)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// GetDocumentAsDocx returns a Word document.
func GetDocumentAsDocx(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentAsDocx"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
document, err := p.GetDocument(documentID)
if err == sql.ErrNoRows {
writeNotFoundError(w, method, documentID)
return
}
if err != nil {
writeServerError(w, method, err)
return
}
if !p.CanViewDocumentInFolder(document.LabelID) {
writeForbiddenError(w)
return
}
pages, err := p.GetPages(documentID)
if err != nil {
writeServerError(w, method, err)
return
}
xtn := "html"
actions, err := plugins.Lib.Actions("Export")
if err == nil {
for _, x := range actions {
if x == "docx" { // only actually export a docx if we have the plugin
xtn = x
break
}
}
}
estLen := 0
for _, page := range pages {
estLen += len(page.Title) + len(page.Body)
}
html := make([]byte, 0, estLen*2) // should be far bigger than we need
html = append(html, []byte("<html><head></head><body>")...)
for _, page := range pages {
html = append(html, []byte(fmt.Sprintf("<h%d>", page.Level))...)
html = append(html, utility.EscapeHTMLcomplexCharsByte([]byte(page.Title))...)
html = append(html, []byte(fmt.Sprintf("</h%d>", page.Level))...)
html = append(html, utility.EscapeHTMLcomplexCharsByte([]byte(page.Body))...)
}
html = append(html, []byte("</body></html>")...)
export, err := store.ExportAs(xtn, string(html))
log.Error("store.ExportAs()", err)
w.Header().Set("Content-Disposition", "attachment; filename="+utility.MakeSlug(document.Title)+"."+xtn)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(export.File)))
writeSuccessBytes(w, export.File)
}
// UpdateDocument updates an existing document using the
// format described in NewDocumentModel() encoded as JSON in the request.
func UpdateDocument(w http.ResponseWriter, r *http.Request) {
method := "UpdateDocument"
p := request.GetPersister(r)
if !p.Context.Editor {
w.WriteHeader(http.StatusForbidden)
return
}
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
d := entity.Document{}
err = json.Unmarshal(body, &d)
if err != nil {
writeBadRequestError(w, method, "document")
return
}
d.RefID = documentID
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
err = p.UpdateDocument(d)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}

View file

@ -0,0 +1,110 @@
// 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 endpoint
// TestEndpoint is the entrypoint for all testing unit testing of this package.
// The actual tests are in "github.com/documize/documize-sdk/exttest".
/* The tests require an environment specified by two environment variables:
"DOCUMIZEAPI" e.g. "http://localhost:5002"
"DOCUMIZEAUTH" e.g. "demo1:jim@davidson.com:demo123"
- the user for testing must have admin privilidges and a folder called 'TEST'.
*/
/* NOTE currently excluded from SDK and testing are endpoints requiring e-mail interaction:
InviteToFolder()
inviteNewUserToSharedFolder()
AcceptSharedFolder()
ForgotUserPassword()
ResetUserPassword()
ChangeUserPassword()
*/
/* TODO (Elliott) make tests work on an empty database
import (
"os"
"strings"
"testing"
"time"
"github.com/documize/community/documize/api/plugins"
"github.com/documize/community/core/environment"
"github.com/documize/community/core/log"
"github.com/documize/community/sdk/exttest"
)
func TestMain(m *testing.M) {
environment.Parse("db") // the database environment variables must be set
port = "5002"
testHost = "localhost"
testSetup()
x := m.Run()
testTeardown()
os.Exit(x)
}
func testSetup() {
path, err := os.Getwd()
if err != nil {
log.IfErr(err)
return
}
switch {
case strings.HasSuffix(path, "endpoint"):
err = os.Chdir("../../..") // everything needs to be run from the top level documize directory, as plugin paths are relative
if err != nil {
log.IfErr(err)
return
}
case strings.HasSuffix(path, "api"):
err = os.Chdir("../..") // everything needs to be run from the top level documize directory, as plugin paths are relative
if err != nil {
log.IfErr(err)
return
}
case strings.HasSuffix(path, "documize"):
err = os.Chdir("..") // everything needs to be run from the top level documize directory, as plugin paths are relative
if err != nil {
log.IfErr(err)
return
}
case strings.HasSuffix(path, "community"):
// in the right place
default:
log.Error("wrong directory? "+path, nil)
return
}
ready := make(chan struct{}, 1)
go Serve(ready)
<-ready
time.Sleep(time.Second) // just to let everything settle down
}
func testTeardown() {
log.IfErr(plugins.Lib.KillSubProcs())
}
func TestEndpoint(t *testing.T) {
exttest.APItest(t)
}
func BenchmarkEndpoint(b *testing.B) {
for n := 0; n < b.N; n++ {
err := exttest.APIbenchmark()
if err != nil {
b.Error(err)
b.Fail()
}
}
}
*/

180
core/api/endpoint/init.go Normal file
View file

@ -0,0 +1,180 @@
// 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 endpoint
import (
"fmt"
"net/http"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/store"
"github.com/documize/community/core/log"
)
var storageProvider store.StorageProvider
func init() {
storageProvider = new(store.LocalStorageProvider)
}
//getAppURL returns full HTTP url for the app
func getAppURL(c request.Context, endpoint string) string {
scheme := "http://"
if c.SSL {
scheme = "https://"
}
return fmt.Sprintf("%s%s/%s", scheme, c.AppURL, endpoint)
}
func writePayloadError(w http.ResponseWriter, method string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_, err2 := w.Write([]byte("{Error: 'Bad payload'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Unable to decode HTML request body for method %s", method), err)
}
func writeTransactionError(w http.ResponseWriter, method string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
_, err2 := w.Write([]byte("{Error: 'No transaction'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Unable to get database transaction for method %s", method), err)
}
/*
func WriteAddRecordError(w http.ResponseWriter, method string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
_, err2 := w.Write([]byte("{Error: 'Add error'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Unable to insert new database record for method %s", method), err)
}
func WriteGetRecordError(w http.ResponseWriter, method, entity string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
_, err2 := w.Write([]byte("{Error: 'Get error'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Unable to get %s record for method %s", entity, method), err)
}
func WriteUpdateRecordError(w http.ResponseWriter, method string, id string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
_, err2 := w.Write([]byte("{Error: 'Add error'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Unable to update database record ID %s for method %s", id, method), err)
}
func WriteParameterParsingError(w http.ResponseWriter, method, parameter string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_, err2 := w.Write([]byte("{Error: 'Bad parameter'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Unable to parse API parameter %s for method %s", parameter, method), err)
}
*/
func writeMissingDataError(w http.ResponseWriter, method, parameter string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte("{Error: 'Missing data'}"))
log.IfErr(err)
log.Info(fmt.Sprintf("Missing data %s for method %s", parameter, method))
}
func writeNotFoundError(w http.ResponseWriter, method string, id string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(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))
}
func writeGeneralSQLError(w http.ResponseWriter, method string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_, err2 := w.Write([]byte("{Error: 'SQL error'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("General SQL error for method %s", method), err)
}
func writeJSONMarshalError(w http.ResponseWriter, method, entity string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_, err2 := w.Write([]byte("{Error: 'JSON marshal failed'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Failed to JSON marshal %s for method %s", entity, method), err)
}
func writeServerError(w http.ResponseWriter, method string, err error) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(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)
}
func writeDuplicateError(w http.ResponseWriter, method, entity string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(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))
}
func writeUnauthorizedError(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)
_, err := w.Write([]byte("{Error: 'Unauthorized'}"))
log.IfErr(err)
}
func writeForbiddenError(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
_, err := w.Write([]byte("{Error: 'Forbidden'}"))
log.IfErr(err)
}
func writeBadRequestError(w http.ResponseWriter, method, message string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte("{Error: 'Bad Request'}"))
log.IfErr(err)
log.Info(fmt.Sprintf("Bad Request %s for method %s", message, method))
}
func writeSuccessBytes(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, err := w.Write(data)
log.IfErr(err)
}
func writeSuccessString(w http.ResponseWriter, data string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(data))
log.IfErr(err)
}
func writeSuccessEmptyJSON(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("{}"))
log.IfErr(err)
}

View file

@ -0,0 +1,840 @@
// 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 endpoint
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/documize/community/core/api/endpoint/models"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/mail"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/util"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
)
// AddFolder creates a new folder.
func AddFolder(w http.ResponseWriter, r *http.Request) {
method := "AddFolder"
p := request.GetPersister(r)
if !p.Context.Editor {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var folder = entity.Label{}
err = json.Unmarshal(body, &folder)
if len(folder.Name) == 0 {
writeJSONMarshalError(w, method, "folder", err)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
id := util.UniqueID()
folder.RefID = id
folder.OrgID = p.Context.OrgID
err = addFolder(p, &folder)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
folder, err = p.GetLabel(id)
json, err := json.Marshal(folder)
if err != nil {
writeJSONMarshalError(w, method, "folder", err)
return
}
writeSuccessBytes(w, json)
}
func addFolder(p request.Persister, label *entity.Label) (err error) {
label.Type = entity.FolderTypePrivate
label.UserID = p.Context.UserID
err = p.AddLabel(*label)
if err != nil {
return
}
role := entity.LabelRole{}
role.LabelID = label.RefID
role.OrgID = label.OrgID
role.UserID = p.Context.UserID
role.CanEdit = true
role.CanView = true
refID := util.UniqueID()
role.RefID = refID
err = p.AddLabelRole(role)
return
}
// GetFolder returns the requested folder.
func GetFolder(w http.ResponseWriter, r *http.Request) {
method := "GetFolder"
p := request.GetPersister(r)
params := mux.Vars(r)
id := params["folderID"]
if len(id) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
folder, err := p.GetLabel(id)
if err != nil && err != sql.ErrNoRows {
writeServerError(w, method, err)
return
}
if err == sql.ErrNoRows {
writeNotFoundError(w, method, id)
return
}
json, err := json.Marshal(folder)
if err != nil {
writeJSONMarshalError(w, method, "folder", err)
return
}
writeSuccessBytes(w, json)
}
// GetFolders returns the folders the user can see.
func GetFolders(w http.ResponseWriter, r *http.Request) {
method := "GetFolders"
p := request.GetPersister(r)
folders, err := p.GetLabels()
if err != nil && err != sql.ErrNoRows {
writeServerError(w, method, err)
return
}
if len(folders) == 0 {
folders = []entity.Label{}
}
json, err := json.Marshal(folders)
if err != nil {
writeJSONMarshalError(w, method, "folder", err)
return
}
writeSuccessBytes(w, json)
}
// GetFolderVisibility returns the users that can see the shared folders.
func GetFolderVisibility(w http.ResponseWriter, r *http.Request) {
method := "GetFolderVisibility"
p := request.GetPersister(r)
folders, err := p.GetFolderVisibility()
if err != nil && err != sql.ErrNoRows {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(folders)
if err != nil {
writeJSONMarshalError(w, method, "folder", err)
return
}
writeSuccessBytes(w, json)
}
// UpdateFolder processes request to save folder object to the database
func UpdateFolder(w http.ResponseWriter, r *http.Request) {
method := "UpdateFolder"
p := request.GetPersister(r)
if !p.Context.Editor {
writeForbiddenError(w)
return
}
params := mux.Vars(r)
folderID := params["folderID"]
if len(folderID) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var folder = entity.Label{}
err = json.Unmarshal(body, &folder)
if len(folder.Name) == 0 {
writeJSONMarshalError(w, method, "folder", err)
return
}
folder.RefID = folderID
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
err = p.UpdateLabel(folder)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
json, err := json.Marshal(folder)
if err != nil {
writeJSONMarshalError(w, method, "folder", err)
return
}
writeSuccessBytes(w, json)
}
// RemoveFolder moves documents to another folder before deleting it
func RemoveFolder(w http.ResponseWriter, r *http.Request) {
method := "RemoveFolder"
p := request.GetPersister(r)
if !p.Context.Editor {
writeForbiddenError(w)
return
}
params := mux.Vars(r)
id := params["folderID"]
move := params["moveToId"]
if len(id) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
if len(move) == 0 {
writeMissingDataError(w, method, "moveToId")
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
_, err = p.DeleteLabel(id)
if err != nil {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
err = p.MoveDocumentLabel(id, move)
if err != nil {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
err = p.MoveLabelRoles(id, move)
if err != nil {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessString(w, "{}")
}
// SetFolderPermissions persists specified folder permissions
func SetFolderPermissions(w http.ResponseWriter, r *http.Request) {
method := "SetFolderPermissions"
p := request.GetPersister(r)
params := mux.Vars(r)
id := params["folderID"]
if len(id) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
label, err := p.GetLabel(id)
if err != nil {
writeBadRequestError(w, method, "No such folder")
return
}
if label.UserID != p.Context.UserID {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var model = models.FolderRolesModel{}
err = json.Unmarshal(body, &model)
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
// We compare new permisions to what we had before.
// Why? So we can send out folder invitation emails.
previousRoles, err := p.GetLabelRoles(id)
if err != nil {
writeGeneralSQLError(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 := p.GetUser(p.Context.UserID)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
// Nuke all previous permissions for this folder
_, err = p.DeleteLabelRoles(id)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
me := false
hasEveryoneRole := false
roleCount := 0
url := getAppURL(p.Context, fmt.Sprintf("s/%s/%s", label.RefID, utility.MakeSlug(label.Name)))
for _, role := range model.Roles {
role.OrgID = p.Context.OrgID
role.LabelID = id
// Ensure the folder owner always has access!
if role.UserID == p.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 := util.UniqueID()
role.RefID = roleID
err = p.AddLabelRole(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 entity.User
existingUser, err = p.GetUser(role.UserID)
if err == nil {
go mail.ShareFolderExistingUser(existingUser.Email, inviter.Fullname(), url, label.Name, model.Message)
log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, label.Name, existingUser.Email))
} else {
writeServerError(w, method, err)
}
}
}
}
}
// Do we need to ensure permissions for folder owner when shared?
if !me {
role := entity.LabelRole{}
role.LabelID = id
role.OrgID = p.Context.OrgID
role.UserID = p.Context.UserID
role.CanEdit = true
role.CanView = true
roleID := util.UniqueID()
role.RefID = roleID
err = p.AddLabelRole(role)
log.IfErr(err)
}
// Mark up folder type as either public, private or restricted access.
if hasEveryoneRole {
label.Type = entity.FolderTypePublic
} else {
if roleCount > 1 {
label.Type = entity.FolderTypeRestricted
} else {
label.Type = entity.FolderTypePrivate
}
}
log.Error("p.UpdateLabel()", p.UpdateLabel(label))
log.Error("tx.Commit()", tx.Commit())
writeSuccessEmptyJSON(w)
}
// GetFolderPermissions returns user permissions for the requested folder.
func GetFolderPermissions(w http.ResponseWriter, r *http.Request) {
method := "GetFolderPermissions"
p := request.GetPersister(r)
params := mux.Vars(r)
folderID := params["folderID"]
if len(folderID) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
roles, err := p.GetLabelRoles(folderID)
if err != nil && err != sql.ErrNoRows {
writeGeneralSQLError(w, method, err)
return
}
if len(roles) == 0 {
roles = []entity.LabelRole{}
}
json, err := json.Marshal(roles)
if err != nil {
writeJSONMarshalError(w, method, "folder-permissions", err)
return
}
writeSuccessBytes(w, json)
}
// AcceptSharedFolder records the fact that a user has completed folder onboard process.
func AcceptSharedFolder(w http.ResponseWriter, r *http.Request) {
method := "AcceptSharedFolder"
p := request.GetPersister(r)
params := mux.Vars(r)
folderID := params["folderID"]
if len(folderID) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
org, err := p.GetOrganizationByDomain(p.Context.Subdomain)
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
p.Context.OrgID = org.RefID
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var model = models.AcceptSharedFolderModel{}
err = json.Unmarshal(body, &model)
if err != nil {
writePayloadError(w, method, err)
return
}
if len(model.Serial) == 0 || len(model.Firstname) == 0 || len(model.Lastname) == 0 || len(model.Password) == 0 {
writeJSONMarshalError(w, method, "missing field data", err)
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
user, err := p.GetUserBySerial(model.Serial)
// User has already on-boarded.
if err != nil && err == sql.ErrNoRows {
writeDuplicateError(w, method, "user")
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
user.Firstname = model.Firstname
user.Lastname = model.Lastname
user.Initials = utility.MakeInitials(user.Firstname, user.Lastname)
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
err = p.UpdateUser(user)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
salt := util.GenerateSalt()
log.IfErr(p.UpdateUserPassword(user.RefID, salt, util.GeneratePassword(model.Password, salt)))
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
data, err := json.Marshal(user)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
writeSuccessBytes(w, data)
}
// InviteToFolder sends users folder invitation emails.
func InviteToFolder(w http.ResponseWriter, r *http.Request) {
method := "InviteToFolder"
p := request.GetPersister(r)
params := mux.Vars(r)
id := params["folderID"]
if len(id) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
label, err := p.GetLabel(id)
if err != nil {
writeBadRequestError(w, method, "folder not found")
return
}
if label.UserID != p.Context.UserID {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var model = models.FolderInvitationModel{}
err = json.Unmarshal(body, &model)
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
inviter, err := p.GetUser(p.Context.UserID)
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
for _, email := range model.Recipients {
var user entity.User
user, err = p.GetUserByEmail(email)
if err != nil && err != sql.ErrNoRows {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
if len(user.RefID) > 0 {
// Ensure they have access to this organization
accounts, err2 := p.GetUserAccounts(user.RefID)
if err2 != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err2)
return
}
// we create if they c
hasAccess := false
for _, a := range accounts {
if a.OrgID == p.Context.OrgID {
hasAccess = true
}
}
if !hasAccess {
var a entity.Account
a.UserID = user.RefID
a.OrgID = p.Context.OrgID
a.Admin = false
a.Editor = false
accountID := util.UniqueID()
a.RefID = accountID
err = p.AddAccount(a)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
// Ensure they have folder roles
_, err = p.DeleteUserFolderRoles(label.RefID, user.RefID)
log.IfErr(err)
role := entity.LabelRole{}
role.LabelID = label.RefID
role.OrgID = p.Context.OrgID
role.UserID = user.RefID
role.CanEdit = false
role.CanView = true
roleID := util.UniqueID()
role.RefID = roleID
err = p.AddLabelRole(role)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
url := getAppURL(p.Context, fmt.Sprintf("s/%s/%s", label.RefID, utility.MakeSlug(label.Name)))
go mail.ShareFolderExistingUser(email, inviter.Fullname(), url, label.Name, model.Message)
log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, label.Name, email))
} else {
// On-board new user
if strings.Contains(email, "@") {
url := getAppURL(p.Context, fmt.Sprintf("auth/share/%s/%s", label.RefID, utility.MakeSlug(label.Name)))
err = inviteNewUserToSharedFolder(p, email, inviter, url, label, model.Message)
if err != nil {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
log.Info(fmt.Sprintf("%s is sharing space %s with new user %s", inviter.Email, label.Name, email))
}
}
}
// We ensure that the folder is marked as restricted as a minimum!
if len(model.Recipients) > 0 && label.Type == entity.FolderTypePrivate {
label.Type = entity.FolderTypeRestricted
err = p.UpdateLabel(label)
if err != nil {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
}
log.IfErr(tx.Commit())
_, err = w.Write([]byte("{}"))
log.IfErr(err)
}
// 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 inviteNewUserToSharedFolder(p request.Persister, email string, invitedBy entity.User,
baseURL string, label entity.Label, invitationMessage string) (err error) {
var user = entity.User{}
user.Email = email
user.Firstname = email
user.Lastname = ""
user.Salt = util.GenerateSalt()
requestedPassword := util.GenerateRandomPassword()
user.Password = util.GeneratePassword(requestedPassword, user.Salt)
userID := util.UniqueID()
user.RefID = userID
err = p.AddUser(user)
if err != nil {
return
}
// Let's give this user access to the organization
var a entity.Account
a.UserID = userID
a.OrgID = p.Context.OrgID
a.Admin = false
a.Editor = false
accountID := util.UniqueID()
a.RefID = accountID
err = p.AddAccount(a)
if err != nil {
return
}
role := entity.LabelRole{}
role.LabelID = label.RefID
role.OrgID = p.Context.OrgID
role.UserID = userID
role.CanEdit = false
role.CanView = true
roleID := util.UniqueID()
role.RefID = roleID
err = p.AddLabelRole(role)
if err != nil {
return
}
url := fmt.Sprintf("%s/%s", baseURL, user.Salt)
go mail.ShareFolderNewUser(user.Email, invitedBy.Fullname(), url, label.Name, invitationMessage)
return
}

View file

@ -0,0 +1,172 @@
// 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 endpoint
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"text/template"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
)
// GetMeta provides org meta data based upon request domain (e.g. acme.documize.com).
func GetMeta(w http.ResponseWriter, r *http.Request) {
method := "GetMeta"
p := request.GetPersister(r)
data := entity.SiteMeta{}
data.URL = request.GetSubdomainFromHost(r)
org, err := p.GetOrganizationByDomain(data.URL)
if err != nil {
log.Info(fmt.Sprintf("%s URL not found", data.URL))
writeForbiddenError(w)
return
}
data.OrgID = org.RefID
data.Title = org.Title
data.Message = org.Message
data.AllowAnonymousAccess = org.AllowAnonymousAccess
data.Version = AppVersion
json, err := json.Marshal(data)
if err != nil {
writeJSONMarshalError(w, method, "meta", err)
return
}
writeSuccessBytes(w, json)
}
// GetRobots returns robots.txt depending on site configuration.
// Did we allow anonymouse access?
func GetRobots(w http.ResponseWriter, r *http.Request) {
method := "GetRobots"
p := request.GetPersister(r)
domain := request.GetSubdomainFromHost(r)
org, err := p.GetOrganizationByDomain(domain)
// default is to deny
robots :=
`User-agent: *
Disallow: /
`
if err != nil {
log.Error(fmt.Sprintf("%s failed to get Organization for domain %s", method, domain), err)
}
// Anonymous access would mean we allow bots to crawl.
if org.AllowAnonymousAccess {
sitemap := getAppURL(p.Context, "sitemap.xml")
robots = fmt.Sprintf(
`User-agent: *
Disallow: /settings/
Disallow: /settings/*
Disallow: /profile/
Disallow: /profile/*
Disallow: /auth/login/
Disallow: /auth/login/
Disallow: /auth/logout/
Disallow: /auth/logout/*
Disallow: /auth/reset/*
Disallow: /auth/reset/*
Disallow: /auth/sso/
Disallow: /auth/sso/*
Disallow: /share
Disallow: /share/*
Sitemap: %s`, sitemap)
}
writeSuccessBytes(w, []byte(robots))
}
// GetSitemap returns URLs that can be indexed.
// We only include public folders and documents (e.g. can be seen by everyone).
func GetSitemap(w http.ResponseWriter, r *http.Request) {
method := "GetSitemap"
p := request.GetPersister(r)
domain := request.GetSubdomainFromHost(r)
org, err := p.GetOrganizationByDomain(domain)
if err != nil {
log.Error(fmt.Sprintf("%s failed to get Organization for domain %s", method, domain), err)
}
sitemap :=
`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
{{range .}}<url>
<loc>{{ .URL }}</loc>
<lastmod>{{ .Date }}</lastmod>
</url>{{end}}
</urlset>`
var items []sitemapItem
// Anonymous access means we announce folders/documents shared with 'Everyone'.
if org.AllowAnonymousAccess {
// Grab shared folders
folders, err := p.GetPublicFolders(org.RefID)
if err != nil {
log.Error(fmt.Sprintf("%s failed to get folders for domain %s", method, domain), err)
}
for _, folder := range folders {
var item sitemapItem
item.URL = getAppURL(p.Context, fmt.Sprintf("s/%s/%s", folder.RefID, utility.MakeSlug(folder.Name)))
item.Date = folder.Revised.Format("2006-01-02T15:04:05.999999-07:00")
items = append(items, item)
}
// Grab documents from shared folders
documents, err := p.GetPublicDocuments(org.RefID)
if err != nil {
log.Error(fmt.Sprintf("%s failed to get documents for domain %s", method, domain), err)
}
for _, document := range documents {
var item sitemapItem
item.URL = getAppURL(p.Context, fmt.Sprintf("s/%s/%s/d/%s/%s",
document.FolderID, utility.MakeSlug(document.Folder), document.DocumentID, utility.MakeSlug(document.Document)))
item.Date = document.Revised.Format("2006-01-02T15:04:05.999999-07:00")
items = append(items, item)
}
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("tmp").Parse(sitemap))
log.IfErr(t.Execute(buffer, &items))
writeSuccessBytes(w, buffer.Bytes())
}
// sitemapItem provides a means to teleport somewhere else for free.
// What did you think it did?
type sitemapItem struct {
URL string
Date string
}

View file

@ -0,0 +1,68 @@
// 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 models describes the communication format between JS snd Go.
// Models are not persisted entities - they are object models that are marshalled between the
// backend and the consumer (UI).
package models
import (
"github.com/documize/community/core/api/entity"
)
// PageSequenceRequestModel details a page ID and its sequence within the document.
type PageSequenceRequestModel struct {
PageID string `json:"pageId"`
Sequence float64 `json:"sequence"`
}
// PageLevelRequestModel details a page ID and level.
type PageLevelRequestModel struct {
PageID string `json:"pageId"`
Level int `json:"level"`
}
// AuthenticationModel details authentication token and user details.
type AuthenticationModel struct {
Token string `json:"token"`
User entity.User `json:"user"`
}
// DocumentUploadModel details the job ID of an uploaded document.
type DocumentUploadModel struct {
JobID string `json:"jobId"`
}
// FolderInvitationModel details which users have been invited to a folder.
type FolderInvitationModel struct {
Message string
Recipients []string
}
// FolderRolesModel details which users have what permissions on a given folder.
type FolderRolesModel struct {
Message string
Roles []entity.LabelRole
}
// AcceptSharedFolderModel is used to setup a user who has accepted a shared folder.
type AcceptSharedFolderModel struct {
Serial string `json:"serial"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Password string `json:"password"`
}
// PageModel contains the page and associated meta.
type PageModel struct {
Page entity.Page `json:"page"`
Meta entity.PageMeta `json:"meta"`
}

View file

@ -0,0 +1,109 @@
// 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 endpoint
import (
// "bytes"
"database/sql"
"encoding/json"
"io/ioutil"
"net/http"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
"github.com/gorilla/mux"
)
// GetOrganization returns the requested organization.
func GetOrganization(w http.ResponseWriter, r *http.Request) {
method := "GetOrganization"
p := request.GetPersister(r)
params := mux.Vars(r)
orgID := params["orgID"]
if orgID != p.Context.OrgID {
writeForbiddenError(w)
return
}
org, err := p.GetOrganization(p.Context.OrgID)
if err != nil && err != sql.ErrNoRows {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(org)
if err != nil {
writeJSONMarshalError(w, method, "organization", err)
return
}
writeSuccessBytes(w, json)
}
// UpdateOrganization saves organization amends.
func UpdateOrganization(w http.ResponseWriter, r *http.Request) {
method := "UpdateOrganization"
p := request.GetPersister(r)
if !p.Context.Administrator {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var org = entity.Organization{}
err = json.Unmarshal(body, &org)
org.RefID = p.Context.OrgID
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
err = p.UpdateOrganization(org)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
json, err := json.Marshal(org)
if err != nil {
writeJSONMarshalError(w, method, "organization", err)
return
}
writeSuccessBytes(w, json)
}

View file

@ -0,0 +1,647 @@
// 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 endpoint
import (
"database/sql"
"encoding/json"
// "html/template"
"io/ioutil"
"net/http"
"strconv"
"github.com/documize/community/core/api/endpoint/models"
"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/section/provider"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
"github.com/gorilla/mux"
)
// AddDocumentPage inserts new section into document.
func AddDocumentPage(w http.ResponseWriter, r *http.Request) {
method := "AddDocumentPage"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writeBadRequestError(w, method, "Bad payload")
return
}
model := new(models.PageModel)
err = json.Unmarshal(body, &model)
if err != nil {
writePayloadError(w, method, err)
return
}
if model.Page.DocumentID != documentID {
writeBadRequestError(w, method, "documentID mismatch")
return
}
if model.Meta.DocumentID != documentID {
writeBadRequestError(w, method, "documentID mismatch")
return
}
pageID := util.UniqueID()
model.Page.RefID = pageID
model.Meta.PageID = pageID
model.Page.SetDefaults()
model.Meta.SetDefaults()
model.Meta.OrgID = p.Context.OrgID
model.Meta.UserID = p.Context.UserID
// page.Title = template.HTMLEscapeString(page.Title)
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
output, ok := provider.Render(model.Page.ContentType,
provider.NewContext(model.Meta.OrgID, model.Meta.UserID), model.Meta.Config, model.Meta.RawBody)
if !ok {
log.ErrorString("provider.Render could not find: " + model.Page.ContentType)
}
model.Page.Body = output
err = p.AddPage(*model)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
newPage, err := p.GetPage(pageID)
json, err := json.Marshal(newPage)
if err != nil {
writeJSONMarshalError(w, method, "page", err)
return
}
writeSuccessBytes(w, json)
}
// GetDocumentPage gets specified page for document.
func GetDocumentPage(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentPage"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
pageID := params["pageID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if len(pageID) == 0 {
writeMissingDataError(w, method, "pageID")
return
}
if !p.CanViewDocument(documentID) {
writeForbiddenError(w)
return
}
page, err := p.GetPage(pageID)
if err == sql.ErrNoRows {
writeNotFoundError(w, method, documentID)
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
if page.DocumentID != documentID {
writeBadRequestError(w, method, "documentID mismatch")
return
}
json, err := json.Marshal(page)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, json)
}
// GetDocumentPages gets all pages for document.
func GetDocumentPages(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentPages"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanViewDocument(documentID) {
writeForbiddenError(w)
return
}
query := r.URL.Query()
content := query.Get("content")
var pages []entity.Page
var err error
if len(content) > 0 {
pages, err = p.GetPagesWithoutContent(documentID)
} else {
pages, err = p.GetPages(documentID)
}
if len(pages) == 0 {
pages = []entity.Page{}
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
json, err := json.Marshal(pages)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, json)
}
// GetDocumentPagesBatch gets specified pages for document.
func GetDocumentPagesBatch(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentPagesBatch"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanViewDocument(documentID) {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
requestedPages := string(body)
pages, err := p.GetPagesWhereIn(documentID, requestedPages)
if err == sql.ErrNoRows {
writeNotFoundError(w, method, documentID)
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
json, err := json.Marshal(pages)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, json)
}
// DeleteDocumentPage deletes a page.
func DeleteDocumentPage(w http.ResponseWriter, r *http.Request) {
method := "DeleteDocumentPage"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
pageID := params["pageID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "pageID")
return
}
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
_, err = p.DeletePage(documentID, pageID)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// DeleteDocumentPages batch deletes pages.
func DeleteDocumentPages(w http.ResponseWriter, r *http.Request) {
method := "DeleteDocumentPages"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writeBadRequestError(w, method, "Bad body")
return
}
model := new([]models.PageLevelRequestModel)
err = json.Unmarshal(body, &model)
if err != nil {
writePayloadError(w, method, err)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
for _, page := range *model {
_, err = p.DeletePage(documentID, page.PageID)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// UpdateDocumentPage will persist changed page and note the fact
// that this is a new revision. If the page is the first in a document
// then the corresponding document title will also be changed.
func UpdateDocumentPage(w http.ResponseWriter, r *http.Request) {
method := "UpdateDocumentPage"
p := request.GetPersister(r)
if !p.Context.Editor {
writeForbiddenError(w)
return
}
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
pageID := params["pageID"]
if len(pageID) == 0 {
writeMissingDataError(w, method, "pageID")
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writeBadRequestError(w, method, "Bad request body")
return
}
model := new(models.PageModel)
err = json.Unmarshal(body, &model)
if err != nil {
writePayloadError(w, method, err)
return
}
if model.Page.RefID != pageID || model.Page.DocumentID != documentID {
writeBadRequestError(w, method, "id mismatch")
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
model.Page.SetDefaults()
model.Meta.SetDefaults()
oldPageMeta, err := p.GetPageMeta(pageID)
if err != nil {
log.Error("unable to fetch old pagemeta record", err)
writeBadRequestError(w, method, err.Error())
return
}
output, ok := provider.Render(model.Page.ContentType, provider.NewContext(model.Meta.OrgID, oldPageMeta.UserID), model.Meta.Config, model.Meta.RawBody)
if !ok {
log.ErrorString("provider.Render could not find: " + model.Page.ContentType)
}
model.Page.Body = output
p.Context.Transaction = tx
var skipRevision bool
skipRevision, err = strconv.ParseBool(r.URL.Query().Get("r"))
refID := util.UniqueID()
err = p.UpdatePage(model.Page, refID, p.Context.UserID, skipRevision)
if err != nil {
writeGeneralSQLError(w, method, err)
log.IfErr(tx.Rollback())
return
}
err = p.UpdatePageMeta(model.Meta, true) // change the UserID to the current one
log.IfErr(tx.Commit())
updatedPage, err := p.GetPage(pageID)
json, err := json.Marshal(updatedPage)
if err != nil {
writeJSONMarshalError(w, method, "page", err)
return
}
writeSuccessBytes(w, json)
}
// ChangeDocumentPageSequence will swap page sequence for a given number of pages.
func ChangeDocumentPageSequence(w http.ResponseWriter, r *http.Request) {
method := "ChangeDocumentPageSequence"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
model := new([]models.PageSequenceRequestModel)
err = json.Unmarshal(body, &model)
if err != nil {
writeBadRequestError(w, method, "bad payload")
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
for _, page := range *model {
err = p.UpdatePageSequence(documentID, page.PageID, page.Sequence)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// ChangeDocumentPageLevel handles page indent/outdent changes.
func ChangeDocumentPageLevel(w http.ResponseWriter, r *http.Request) {
method := "ChangeDocumentPageLevel"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanChangeDocument(documentID) {
w.WriteHeader(http.StatusForbidden)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
model := new([]models.PageLevelRequestModel)
err = json.Unmarshal(body, &model)
if err != nil {
writeBadRequestError(w, method, "bad payload")
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
for _, page := range *model {
err = p.UpdatePageLevel(documentID, page.PageID, page.Level)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// GetDocumentPageMeta gets page meta data for specified document page.
func GetDocumentPageMeta(w http.ResponseWriter, r *http.Request) {
method := "GetDocumentPageMeta"
p := request.GetPersister(r)
params := mux.Vars(r)
documentID := params["documentID"]
pageID := params["pageID"]
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if len(pageID) == 0 {
writeMissingDataError(w, method, "pageID")
return
}
if !p.CanViewDocument(documentID) {
writeForbiddenError(w)
return
}
meta, err := p.GetPageMeta(pageID)
if err == sql.ErrNoRows {
writeNotFoundError(w, method, pageID)
return
}
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
if meta.DocumentID != documentID {
writeBadRequestError(w, method, "documentID mismatch")
return
}
json, err := json.Marshal(meta)
if err != nil {
writeJSONMarshalError(w, method, "pagemeta", err)
return
}
writeSuccessBytes(w, json)
}

276
core/api/endpoint/router.go Normal file
View file

@ -0,0 +1,276 @@
// 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 endpoint
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/codegangsta/negroni"
"github.com/documize/community/core/api/plugins"
"github.com/documize/community/core/database"
"github.com/documize/community/core/web"
"github.com/documize/community/core/environment"
"github.com/documize/community/core/log"
"github.com/gorilla/mux"
)
const (
// AppVersion does what it says
// Note: versioning scheme is not http://semver.org
AppVersion = "0.15.0"
)
var port, certFile, keyFile, forcePort2SSL string
func init() {
environment.GetString(&certFile, "cert", false, "the cert.pem file used for https", nil)
environment.GetString(&keyFile, "key", false, "the key.pem file used for https", nil)
environment.GetString(&port, "port", false, "http/https port number", nil)
environment.GetString(&forcePort2SSL, "forcesslport", false, "redirect given http port number to TLS", nil)
}
var testHost string // used during automated testing
// Serve the Documize endpoint.
func Serve(ready chan struct{}) {
err := plugins.LibSetup()
if err != nil {
log.Error("Terminating before running - invalid plugin.json", err)
os.Exit(1)
}
log.Info(fmt.Sprintf("Documize version %s", AppVersion))
router := mux.NewRouter()
router.PathPrefix("/api/public/").Handler(negroni.New(
negroni.HandlerFunc(cors),
negroni.Wrap(buildUnsecureRoutes()),
))
router.PathPrefix("/api").Handler(negroni.New(
negroni.HandlerFunc(Authorize),
negroni.Wrap(buildSecureRoutes()),
))
router.PathPrefix("/").Handler(negroni.New(
negroni.HandlerFunc(cors),
negroni.Wrap(AppRouter()),
))
n := negroni.New()
n.Use(negroni.NewStatic(web.StaticAssetsFileSystem()))
n.Use(negroni.HandlerFunc(cors))
n.Use(negroni.HandlerFunc(metrics))
n.UseHandler(router)
ready <- struct{}{}
if certFile == "" && keyFile == "" {
if port == "" {
port = "80"
}
log.Info("Starting non-SSL server on " + port)
n.Run(testHost + ":" + port)
} else {
if port == "" {
port = "443"
}
if forcePort2SSL != "" {
log.Info("Starting non-SSL server on " + forcePort2SSL + " and redirecting to SSL server on " + port)
go func() {
err := http.ListenAndServe(":"+forcePort2SSL, http.HandlerFunc(
func(w http.ResponseWriter, req *http.Request) {
var host = strings.Replace(req.Host, forcePort2SSL, port, 1) + req.RequestURI
http.Redirect(w, req, "https://"+host, http.StatusMovedPermanently)
}))
if err != nil {
log.Error("ListenAndServe on "+forcePort2SSL, err)
}
}()
}
log.Info("Starting SSL server on " + port + " with " + certFile + " " + keyFile)
server := &http.Server{Addr: ":" + port, Handler: n}
server.SetKeepAlivesEnabled(true)
if err := server.ListenAndServeTLS(certFile, keyFile); err != nil {
log.Error("ListenAndServeTLS on "+port, err)
}
}
}
func buildUnsecureRoutes() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/api/public/meta", GetMeta).Methods("GET", "OPTIONS")
router.HandleFunc("/api/public/authenticate", Authenticate).Methods("POST", "OPTIONS")
router.HandleFunc("/api/public/validate", ValidateAuthToken).Methods("GET", "OPTIONS")
router.HandleFunc("/api/public/forgot", ForgotUserPassword).Methods("POST", "OPTIONS")
router.HandleFunc("/api/public/reset/{token}", ResetUserPassword).Methods("POST", "OPTIONS")
router.HandleFunc("/api/public/share/{folderID}", AcceptSharedFolder).Methods("POST", "OPTIONS")
router.HandleFunc("/api/public/attachments/{orgID}/{job}/{fileID}", AttachmentDownload).Methods("GET", "OPTIONS")
router.HandleFunc("/api/public/version", version).Methods("GET", "OPTIONS")
return router
}
func buildSecureRoutes() *mux.Router {
router := mux.NewRouter()
if web.SiteMode == web.SiteModeSetup {
router.HandleFunc("/api/setup", database.Create).Methods("POST", "OPTIONS")
}
// Import & Convert Document
router.HandleFunc("/api/import/folder/{folderID}", UploadConvertDocument).Methods("POST", "OPTIONS")
// Document
router.HandleFunc("/api/documents/{documentID}/export", GetDocumentAsDocx).Methods("GET", "OPTIONS")
router.HandleFunc("/api/documents", GetDocumentsByTag).Methods("GET", "OPTIONS").Queries("filter", "tag")
router.HandleFunc("/api/documents", GetDocumentsByFolder).Methods("GET", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}", GetDocument).Methods("GET", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}", UpdateDocument).Methods("PUT", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}", DeleteDocument).Methods("DELETE", "OPTIONS")
// Document Meta
router.HandleFunc("/api/documents/{documentID}/meta", GetDocumentMeta).Methods("GET", "OPTIONS")
// Document Page
router.HandleFunc("/api/documents/{documentID}/pages/level", ChangeDocumentPageLevel).Methods("POST", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages/sequence", ChangeDocumentPageSequence).Methods("POST", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages/batch", GetDocumentPagesBatch).Methods("POST", "OPTIONS")
// router.HandleFunc("/api/documents/{documentID}/pages/{pageID}/revisions", GetDocumentPageRevisions).Methods("GET", "OPTIONS")
// router.HandleFunc("/api/documents/{documentID}/pages/{pageID}/revisions/{revisionID}", GetDocumentPageDiff).Methods("GET", "OPTIONS")
// router.HandleFunc("/api/documents/{documentID}/pages/{pageID}/revisions/{revisionID}", RollbackDocumentPage).Methods("POST", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages", GetDocumentPages).Methods("GET", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages/{pageID}", UpdateDocumentPage).Methods("PUT", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages/{pageID}", DeleteDocumentPage).Methods("DELETE", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages/{pageID}", DeleteDocumentPages).Methods("POST", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages/{pageID}", GetDocumentPage).Methods("GET", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/pages", AddDocumentPage).Methods("POST", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/attachments", GetAttachments).Methods("GET", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/attachments/{attachmentID}", DeleteAttachment).Methods("DELETE", "OPTIONS")
router.HandleFunc("/api/documents/{documentID}/attachments", AddAttachments).Methods("POST", "OPTIONS")
// Document Meta
router.HandleFunc("/api/documents/{documentID}/pages/{pageID}/meta", GetDocumentPageMeta).Methods("GET", "OPTIONS")
// Organization
router.HandleFunc("/api/organizations/{orgID}", GetOrganization).Methods("GET", "OPTIONS")
router.HandleFunc("/api/organizations/{orgID}", UpdateOrganization).Methods("PUT", "OPTIONS")
// Folder
router.HandleFunc("/api/folders/{folderID}/move/{moveToId}", RemoveFolder).Methods("DELETE", "OPTIONS")
router.HandleFunc("/api/folders/{folderID}/permissions", SetFolderPermissions).Methods("PUT", "OPTIONS")
router.HandleFunc("/api/folders/{folderID}/permissions", GetFolderPermissions).Methods("GET", "OPTIONS")
router.HandleFunc("/api/folders/{folderID}/invitation", InviteToFolder).Methods("POST", "OPTIONS")
router.HandleFunc("/api/folders", GetFolderVisibility).Methods("GET", "OPTIONS").Queries("filter", "viewers")
router.HandleFunc("/api/folders", AddFolder).Methods("POST", "OPTIONS")
router.HandleFunc("/api/folders", GetFolders).Methods("GET", "OPTIONS")
router.HandleFunc("/api/folders/{folderID}", GetFolder).Methods("GET", "OPTIONS")
router.HandleFunc("/api/folders/{folderID}", UpdateFolder).Methods("PUT", "OPTIONS")
// Users
router.HandleFunc("/api/users/{userID}/password", ChangeUserPassword).Methods("POST", "OPTIONS")
router.HandleFunc("/api/users/{userID}/permissions", GetUserFolderPermissions).Methods("GET", "OPTIONS")
router.HandleFunc("/api/users", AddUser).Methods("POST", "OPTIONS")
router.HandleFunc("/api/users/folder/{folderID}", GetFolderUsers).Methods("GET", "OPTIONS")
router.HandleFunc("/api/users", GetOrganizationUsers).Methods("GET", "OPTIONS")
router.HandleFunc("/api/users/{userID}", GetUser).Methods("GET", "OPTIONS")
router.HandleFunc("/api/users/{userID}", UpdateUser).Methods("PUT", "OPTIONS")
router.HandleFunc("/api/users/{userID}", DeleteUser).Methods("DELETE", "OPTIONS")
// Search
router.HandleFunc("/api/search", SearchDocuments).Methods("GET", "OPTIONS")
// Templates
router.HandleFunc("/api/templates", SaveAsTemplate).Methods("POST", "OPTIONS")
router.HandleFunc("/api/templates", GetSavedTemplates).Methods("GET", "OPTIONS")
router.HandleFunc("/api/templates/stock", GetStockTemplates).Methods("GET", "OPTIONS")
router.HandleFunc("/api/templates/{templateID}/folder/{folderID}", StartDocumentFromStockTemplate).Methods("POST", "OPTIONS").Queries("type", "stock")
router.HandleFunc("/api/templates/{templateID}/folder/{folderID}", StartDocumentFromSavedTemplate).Methods("POST", "OPTIONS").Queries("type", "saved")
// Sections
router.HandleFunc("/api/sections", GetSections).Methods("GET", "OPTIONS")
router.HandleFunc("/api/sections", RunSectionCommand).Methods("POST", "OPTIONS")
router.HandleFunc("/api/sections/refresh", RefreshSections).Methods("GET", "OPTIONS")
return router
}
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")
if r.Method == "OPTIONS" {
if _, err := w.Write([]byte("")); err != nil {
log.Error("cors", err)
}
return
}
next(w, r)
}
func metrics(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Add("X-Documize-Version", AppVersion)
w.Header().Add("Cache-Control", "no-cache")
// Prevent page from being displayed in an iframe
w.Header().Add("X-Frame-Options", "DENY")
// Force SSL delivery
// if certFile != "" && keyFile != "" {
// w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
// }
next(w, r)
}
func version(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte(AppVersion)); err != nil {
log.Error("versionHandler", err)
}
}
// AppRouter configures single page app handler.
func AppRouter() *mux.Router {
router := mux.NewRouter()
switch web.SiteMode {
case web.SiteModeOffline:
log.Info("Serving OFFLINE web app")
case web.SiteModeSetup:
log.Info("Serving SETUP web app")
case web.SiteModeBadDB:
log.Info("Serving BAD DATABASE web app")
default:
log.Info("Starting web app")
}
router.HandleFunc("/robots.txt", GetRobots).Methods("GET", "OPTIONS")
router.HandleFunc("/sitemap.xml", GetSitemap).Methods("GET", "OPTIONS")
router.HandleFunc("/{rest:.*}", web.EmberHandler)
return router
}

View file

@ -0,0 +1,178 @@
// 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 endpoint
import (
"encoding/json"
"net/http"
"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/section/provider"
"github.com/documize/community/core/log"
)
// GetSections returns available smart sections.
func GetSections(w http.ResponseWriter, r *http.Request) {
method := "GetSections"
json, err := json.Marshal(provider.GetSectionMeta())
if err != nil {
writeJSONMarshalError(w, method, "section", err)
return
}
writeSuccessBytes(w, json)
}
// RunSectionCommand passes UI request to section handler.
func RunSectionCommand(w http.ResponseWriter, r *http.Request) {
method := "WebCommand"
p := request.GetPersister(r)
query := r.URL.Query()
documentID := query.Get("documentID")
sectionName := query.Get("section")
// Missing value checks
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if len(sectionName) == 0 {
writeMissingDataError(w, method, "section")
return
}
// Note that targetMethod query item can be empty --
// it's up to the section handler to parse if required.
// Permission checks
if !p.CanChangeDocument(documentID) {
writeForbiddenError(w)
return
}
if !p.Context.Editor {
writeForbiddenError(w)
return
}
if !provider.Command(sectionName, provider.NewContext(p.Context.OrgID, p.Context.UserID), w, r) {
log.ErrorString("Unable to run provider.Command() for: " + sectionName)
writeNotFoundError(w, "RunSectionCommand", sectionName)
}
}
// RefreshSections updates document sections where the data
// is externally sourced.
func RefreshSections(w http.ResponseWriter, r *http.Request) {
method := "RefreshSections"
p := request.GetPersister(r)
query := r.URL.Query()
documentID := query.Get("documentID")
if len(documentID) == 0 {
writeMissingDataError(w, method, "documentID")
return
}
if !p.CanViewDocument(documentID) {
writeForbiddenError(w)
return
}
// Return payload
var pages []entity.Page
// Let's see what sections are reliant on external sources
meta, err := p.GetDocumentPageMeta(documentID, true)
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
for _, pm := range meta {
// Grab the page because we need content type and
page, err2 := p.GetPage(pm.PageID)
if err2 != nil {
writeGeneralSQLError(w, method, err2)
log.IfErr(tx.Rollback())
return
}
pcontext := provider.NewContext(pm.OrgID, pm.UserID)
// Ask for data refresh
data, ok := provider.Refresh(page.ContentType, pcontext, pm.Config, pm.RawBody)
if !ok {
log.ErrorString("provider.Refresh could not find: " + page.ContentType)
}
// Render again
body, ok := provider.Render(page.ContentType, pcontext, pm.Config, data)
if !ok {
log.ErrorString("provider.Render could not find: " + page.ContentType)
}
// Compare to stored render
if body != page.Body {
// Persist latest data
page.Body = body
pages = append(pages, page)
refID := util.UniqueID()
err = p.UpdatePage(page, refID, p.Context.UserID, false)
if err != nil {
writeGeneralSQLError(w, method, err)
log.IfErr(tx.Rollback())
return
}
err = p.UpdatePageMeta(pm, false) // do not change the UserID on this PageMeta
if err != nil {
writeGeneralSQLError(w, method, err)
log.IfErr(tx.Rollback())
return
}
}
}
log.IfErr(tx.Commit())
json, err := json.Marshal(pages)
if err != nil {
writeJSONMarshalError(w, method, "pages", err)
return
}
writeSuccessBytes(w, json)
}

View file

@ -0,0 +1,557 @@
// 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 endpoint
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"database/sql"
"github.com/gorilla/mux"
"github.com/documize/community/core/api/convert"
"github.com/documize/community/core/api/endpoint/models"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/util"
api "github.com/documize/community/core/convapi"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
uuid "github.com/nu7hatch/gouuid"
)
// SaveAsTemplate saves existing document as a template.
func SaveAsTemplate(w http.ResponseWriter, r *http.Request) {
method := "SaveAsTemplate"
p := request.GetPersister(r)
var model struct {
DocumentID string
Name string
Excerpt string
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writeBadRequestError(w, method, "Bad payload")
return
}
err = json.Unmarshal(body, &model)
if err != nil {
writePayloadError(w, method, err)
return
}
if !p.CanChangeDocument(model.DocumentID) {
writeForbiddenError(w)
return
}
// DB transaction
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
// Duplicate document
doc, err := p.GetDocument(model.DocumentID)
if err != nil {
writeServerError(w, method, err)
return
}
docID := util.UniqueID()
doc.Template = true
doc.Title = model.Name
doc.Excerpt = model.Excerpt
doc.RefID = docID
doc.ID = 0
doc.Template = true
// Duplicate pages and associated meta
pages, err := p.GetPages(model.DocumentID)
var pageModel []models.PageModel
if err != nil {
writeServerError(w, method, err)
return
}
for _, page := range pages {
page.DocumentID = docID
page.ID = 0
meta, err2 := p.GetPageMeta(page.RefID)
if err2 != nil {
writeServerError(w, method, err2)
return
}
pageID := util.UniqueID()
page.RefID = pageID
meta.PageID = pageID
meta.DocumentID = docID
m := models.PageModel{}
m.Page = page
m.Meta = meta
pageModel = append(pageModel, m)
}
// Duplicate attachments
attachments, err := p.GetAttachments(model.DocumentID)
for i, a := range attachments {
a.DocumentID = docID
a.RefID = util.UniqueID()
a.ID = 0
attachments[i] = a
}
// Now create the template: document, attachments, pages and their meta
err = p.AddDocument(doc)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
for _, a := range attachments {
err = p.AddAttachment(a)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
for _, m := range pageModel {
err = p.AddPage(m)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
// Commit and return new document template
log.IfErr(tx.Commit())
doc, err = p.GetDocument(docID)
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
d, err := json.Marshal(doc)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, d)
}
// GetSavedTemplates returns all templates saved by the user
func GetSavedTemplates(w http.ResponseWriter, r *http.Request) {
method := "GetSavedTemplates"
p := request.GetPersister(r)
documents, err := p.GetDocumentTemplates()
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
templates := []entity.Template{}
for _, d := range documents {
var template = entity.Template{}
template.ID = d.RefID
template.Title = d.Title
template.Description = d.Excerpt
template.Author = ""
template.Dated = d.Created
template.Type = entity.TemplateTypePrivate
templates = append(templates, template)
}
data, err := json.Marshal(templates)
if err != nil {
writeJSONMarshalError(w, method, "template", err)
return
}
writeSuccessBytes(w, data)
}
// GetStockTemplates returns available templates from the public Documize repository.
func GetStockTemplates(w http.ResponseWriter, r *http.Request) {
method := "GetStockTemplates"
var templates = fetchStockTemplates()
json, err := json.Marshal(templates)
if err != nil {
writeJSONMarshalError(w, method, "template", err)
return
}
writeSuccessBytes(w, json)
}
// StartDocumentFromStockTemplate creates new document using one of the stock templates
func StartDocumentFromStockTemplate(w http.ResponseWriter, r *http.Request) {
method := "StartDocumentFromStockTemplate"
p := request.GetPersister(r)
if !p.Context.Editor {
writeForbiddenError(w)
return
}
params := mux.Vars(r)
folderID := params["folderID"]
if len(folderID) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
templateID := params["templateID"]
if len(templateID) == 0 {
writeMissingDataError(w, method, "templateID")
return
}
filename, template, err := fetchStockTemplate(templateID)
if err != nil {
writeServerError(w, method, err)
return
}
if len(template) == 0 {
writeBadRequestError(w, method, "No data found in template")
}
fileRequest := api.DocumentConversionRequest{}
fileRequest.Filedata = template
fileRequest.Filename = fmt.Sprintf("%s.docx", filename)
fileRequest.PageBreakLevel = 4
//fileRequest.Job = templateID
//fileRequest.OrgID = p.Context.OrgID
// fileResult, err := store.RunConversion(fileRequest)
//fileResultI, err := plugins.Lib.Run(nil, "Convert", "docx", fileRequest)
fileResult, err := convert.Convert(nil, "docx", &fileRequest)
if err != nil {
writeServerError(w, method, err)
return
}
model, err := processDocument(p, fileRequest.Filename, templateID, folderID, fileResult)
if err != nil {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(model)
if err != nil {
writeJSONMarshalError(w, method, "stockTemplate", err)
return
}
writeSuccessBytes(w, json)
}
// StartDocumentFromSavedTemplate creates new document using a saved document as a template.
// If template ID is ZERO then we provide an Empty Document as the new document.
func StartDocumentFromSavedTemplate(w http.ResponseWriter, r *http.Request) {
method := "StartDocumentFromSavedTemplate"
p := request.GetPersister(r)
params := mux.Vars(r)
folderID := params["folderID"]
if len(folderID) == 0 {
writeMissingDataError(w, method, "folderID")
return
}
templateID := params["templateID"]
// We are OK with zero valued template ID because it signals 'give me empty document'
if len(templateID) == 0 {
writeMissingDataError(w, method, "templateID")
return
}
// Define an empty document just in case user wanted one.
var err error
var d = entity.Document{}
d.Title = "New Document"
d.Location = fmt.Sprintf("template-%s", templateID)
d.Excerpt = "A new document"
d.Slug = utility.MakeSlug(d.Title)
d.Tags = ""
d.LabelID = folderID
documentID := util.UniqueID()
d.RefID = documentID
var pages = []entity.Page{}
//var pages = make([]entity.Page, 1, 1)
//pages[0] = entity.Page{}
//pages[0].Title = "Heading"
//pages[0].Body = "<p>Some content here.</p>"
//pages[0].Level = 1
//pages[0].Sequence = 1
var attachments = []entity.Attachment{}
// Fetch document and associated pages, attachments if we have template ID
if templateID != "0" {
d, err = p.GetDocument(templateID)
if err == sql.ErrNoRows {
api.WriteError(w, errors.New("NotFound"))
return
}
if err != nil {
api.WriteError(w, err)
return
}
pages, err = p.GetPages(templateID)
attachments, err = p.GetAttachmentsWithData(templateID)
}
// create new document
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
// Prepare new document
documentID = util.UniqueID()
d.RefID = documentID
d.Template = false
d.LabelID = folderID
d.UserID = p.Context.UserID
err = p.AddDocument(d)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
for _, page := range pages {
page.DocumentID = documentID
pageID := util.UniqueID()
page.RefID = pageID
meta := entity.PageMeta{}
meta.PageID = pageID
meta.RawBody = page.Body
model := models.PageModel{}
model.Page = page
model.Meta = meta
err = p.AddPage(model)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
newUUID, err := uuid.NewV4()
if err != nil {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
for _, a := range attachments {
a.DocumentID = documentID
a.Job = newUUID.String()
random := util.GenerateSalt()
a.FileID = random[0:9]
attachmentID := util.UniqueID()
a.RefID = attachmentID
err = p.AddAttachment(a)
if err != nil {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
}
log.IfErr(tx.Commit())
newDocument, err := p.GetDocument(documentID)
if err != nil {
writeServerError(w, method, err)
return
}
data, err := json.Marshal(newDocument)
if err != nil {
writeJSONMarshalError(w, method, "document", err)
return
}
writeSuccessBytes(w, data)
}
type templateConfig struct {
ID string `json:"id"`
Description string `json:"description"`
Author string `json:"author"`
Title string `json:"title"`
}
func fetchStockTemplates() (templates []entity.Template) {
path := "./templates"
templates = make([]entity.Template, 0)
folders, err := ioutil.ReadDir(path)
log.IfErr(err)
for _, folder := range folders {
if folder.IsDir() {
files, err := ioutil.ReadDir(path + "/" + folder.Name())
log.IfErr(err)
for _, file := range files {
if !file.IsDir() && file.Name() == "template.json" {
data, err := ioutil.ReadFile(path + "/" + folder.Name() + "/" + file.Name())
if err != nil {
log.Error("error reading template.json", err)
} else {
var config = templateConfig{}
err = json.Unmarshal(data, &config)
if err != nil {
log.Error("error parsing template.json", err)
} else {
var template = entity.Template{}
template.ID = config.ID
template.Title = config.Title
template.Description = config.Description
template.Author = config.Author
template.Type = entity.TemplateTypePublic
templates = append(templates, template)
}
}
}
}
}
}
return
}
func fetchStockTemplate(ID string) (filename string, template []byte, err error) {
path := "./templates"
folders, err := ioutil.ReadDir(path)
if err != nil {
log.Error("error reading template directory", err)
return "", nil, err
}
for _, folder := range folders {
if folder.IsDir() {
files, err := ioutil.ReadDir(path + "/" + folder.Name())
if err != nil {
log.Error("error reading template sub-dir", err)
return "", nil, err
}
for _, file := range files {
if !file.IsDir() && file.Name() == "template.json" {
data, err := ioutil.ReadFile(path + "/" + folder.Name() + "/template.json")
if err != nil {
log.Error("error reading template.json", err)
return "", nil, err
}
var config = templateConfig{}
err = json.Unmarshal(data, &config)
if err != nil {
log.Error("error parsing template.json", err)
return "", nil, err
}
if config.ID == ID {
template, err = ioutil.ReadFile(path + "/" + folder.Name() + "/template.docx")
return folder.Name(), template, err
}
}
}
}
}
return
}

View file

@ -0,0 +1,720 @@
// 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 endpoint
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/mail"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/util"
api "github.com/documize/community/core/convapi"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
"github.com/gorilla/mux"
)
// AddUser is the endpoint that enables an administrator to add a new user for their orgaisation.
func AddUser(w http.ResponseWriter, r *http.Request) {
method := "AddUser"
p := request.GetPersister(r)
if !p.Context.Administrator {
writeForbiddenError(w)
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
userModel := entity.User{}
err = json.Unmarshal(body, &userModel)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
// data validation
userModel.Email = strings.ToLower(strings.TrimSpace(userModel.Email))
userModel.Firstname = strings.TrimSpace(userModel.Firstname)
userModel.Lastname = strings.TrimSpace(userModel.Lastname)
userModel.Password = strings.TrimSpace(userModel.Password)
if len(userModel.Email) == 0 {
writeBadRequestError(w, method, "Missing email")
return
}
if len(userModel.Firstname) == 0 {
writeBadRequestError(w, method, "Missing firstname")
return
}
if len(userModel.Lastname) == 0 {
writeBadRequestError(w, method, "Missing lastname")
return
}
userModel.Initials = utility.MakeInitials(userModel.Firstname, userModel.Lastname)
// generate secrets
requestedPassword := util.GenerateRandomPassword()
userModel.Salt = util.GenerateSalt()
userModel.Password = util.GeneratePassword(requestedPassword, userModel.Salt)
// only create account if not dupe
addUser := true
addAccount := true
var userID string
userDupe, err := p.GetUserByEmail(userModel.Email)
if err != nil && err != sql.ErrNoRows {
writeGeneralSQLError(w, method, err)
return
}
if userModel.Email == userDupe.Email {
addUser = false
userID = userDupe.RefID
log.Info("Dupe user found, will not add")
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
if addUser {
userID = util.UniqueID()
userModel.RefID = userID
err = p.AddUser(userModel)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.Info("Adding user")
} else {
attachUserAccounts(p, p.Context.OrgID, &userDupe)
for _, a := range userDupe.Accounts {
if a.OrgID == p.Context.OrgID {
addAccount = false
log.Info("Dupe account found, will not add")
break
}
}
}
// set up user account for the org
if addAccount {
var a entity.Account
a.UserID = userID
a.OrgID = p.Context.OrgID
a.Editor = true
a.Admin = false
accountID := util.UniqueID()
a.RefID = accountID
err = p.AddAccount(a)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
log.IfErr(tx.Commit())
// If we did not add user or give them access (account) then we error back
if !addUser && !addAccount {
writeDuplicateError(w, method, "user")
return
}
// Invite new user
inviter, err := p.GetUser(p.Context.UserID)
log.IfErr(err)
// Prepare invitation email (that contains SSO link)
if addUser && addAccount {
size := len(requestedPassword)
auth := fmt.Sprintf("%s:%s:%s", p.Context.AppURL, userModel.Email, requestedPassword[:size])
encrypted := utility.EncodeBase64([]byte(auth))
url := fmt.Sprintf("%s/%s", getAppURL(p.Context, "auth/sso"), url.QueryEscape(string(encrypted)))
go mail.InviteNewUser(userModel.Email, inviter.Fullname(), url, userModel.Email, requestedPassword)
log.Info(fmt.Sprintf("%s invited by %s on %s", userModel.Email, inviter.Email, p.Context.AppURL))
} else {
go mail.InviteExistingUser(userModel.Email, inviter.Fullname(), getAppURL(p.Context, ""))
log.Info(fmt.Sprintf("%s is giving access to an existing user %s", inviter.Email, userModel.Email))
}
// Send back new user record
userModel, err = getSecuredUser(p, p.Context.OrgID, userID)
json, err := json.Marshal(userModel)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
writeSuccessBytes(w, json)
}
// GetOrganizationUsers is the endpoint that allows administrators to view the users in their organisation.
func GetOrganizationUsers(w http.ResponseWriter, r *http.Request) {
method := "GetUsersForOrganization"
p := request.GetPersister(r)
if !p.Context.Editor && !p.Context.Administrator {
writeForbiddenError(w)
return
}
users, err := p.GetUsersForOrganization()
if err != nil && err != sql.ErrNoRows {
writeServerError(w, method, err)
return
}
for i := range users {
attachUserAccounts(p, p.Context.OrgID, &users[i])
}
json, err := json.Marshal(users)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
writeSuccessBytes(w, json)
}
// GetFolderUsers returns every user within a given space
func GetFolderUsers(w http.ResponseWriter, r *http.Request) {
method := "GetUsersForSpace"
p := request.GetPersister(r)
var users []entity.User
var err error
params := mux.Vars(r)
folderID := params["folderID"]
if len(folderID) == 0 {
writeBadRequestError(w, method, "missing folderID")
return
}
// check to see folder type as it determines user selection criteria
folder, err := p.GetLabel(folderID)
if err != nil && err != sql.ErrNoRows {
log.Error(fmt.Sprintf("%s: cannot fetch space %s", method, folderID), err)
writeUsers(w, nil)
return
}
switch folder.Type {
case entity.FolderTypePublic:
// return all users for team
users, err = p.GetUsersForOrganization()
break
case entity.FolderTypePrivate:
// just me
var user entity.User
user, err = p.GetUser(p.Context.UserID)
users = append(users, user)
break
case entity.FolderTypeRestricted:
users, err = p.GetFolderUsers(folderID)
break
}
if err != nil && err != sql.ErrNoRows {
log.Error(fmt.Sprintf("%s: cannot fetch users for space %s", method, folderID), err)
writeUsers(w, nil)
return
}
writeUsers(w, users)
}
// GetUser returns user specified by Id
func GetUser(w http.ResponseWriter, r *http.Request) {
method := "GetUser"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
if len(userID) == 0 {
writeMissingDataError(w, method, "userId")
return
}
if userID != p.Context.UserID {
writeBadRequestError(w, method, "User Id mismatch")
return
}
user, err := getSecuredUser(p, p.Context.OrgID, userID)
if err == sql.ErrNoRows {
writeNotFoundError(w, method, userID)
return
}
if err != nil {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(user)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
writeSuccessBytes(w, json)
}
// DeleteUser is the endpoint to delete a user specified by userID, the caller must be an Administrator.
func DeleteUser(w http.ResponseWriter, r *http.Request) {
method := "DeleteUser"
p := request.GetPersister(r)
if !p.Context.Administrator {
writeForbiddenError(w)
return
}
params := mux.Vars(r)
userID := params["userID"]
if len(userID) == 0 {
writeMissingDataError(w, method, "userID")
return
}
if userID == p.Context.UserID {
writeBadRequestError(w, method, "cannot delete self")
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
err = p.DeactiveUser(userID)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
err = p.ChangeLabelOwner(userID, p.Context.UserID)
log.IfErr(err)
log.IfErr(tx.Commit())
writeSuccessString(w, "{}")
}
// UpdateUser is the endpoint to update user information for the given userID.
// Note that unless they have admin privildges, a user can only update their own information.
// Also, only admins can update user roles in organisations.
func UpdateUser(w http.ResponseWriter, r *http.Request) {
method := "UpdateUser"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
if len(userID) == 0 {
writeBadRequestError(w, method, "user id must be numeric")
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
user := entity.User{}
err = json.Unmarshal(body, &user)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
// can only update your own account unless you are an admin
if p.Context.UserID != userID && !p.Context.Administrator {
writeForbiddenError(w)
return
}
// can only update your own account unless you are an admin
if len(user.Email) == 0 {
writeBadRequestError(w, method, "missing email")
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
user.RefID = userID
user.Initials = utility.MakeInitials(user.Firstname, user.Lastname)
err = p.UpdateUser(user)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
// Now we update user roles for this organization.
// That means we have to first find their account record
// for this organization.
account, err := p.GetUserAccount(userID)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
account.Editor = user.Editor
account.Admin = user.Admin
err = p.UpdateAccount(account)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
json, err := json.Marshal(user)
if err != nil {
writeJSONMarshalError(w, method, "user", err)
return
}
writeSuccessBytes(w, json)
}
// ChangeUserPassword accepts password change from within the app.
func ChangeUserPassword(w http.ResponseWriter, r *http.Request) {
method := "ChangeUserPassword"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
if len(userID) == 0 {
writeMissingDataError(w, method, "user id")
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
newPassword := string(body)
// can only update your own account unless you are an admin
if userID != p.Context.UserID && !p.Context.Administrator {
writeForbiddenError(w)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
user, err := p.GetUser(userID)
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
user.Salt = util.GenerateSalt()
err = p.UpdateUserPassword(userID, user.Salt, util.GeneratePassword(newPassword, user.Salt))
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w)
}
// GetUserFolderPermissions returns folder permission for authenticated user.
func GetUserFolderPermissions(w http.ResponseWriter, r *http.Request) {
method := "ChangeUserPassword"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
if userID != p.Context.UserID {
writeUnauthorizedError(w)
return
}
roles, err := p.GetUserLabelRoles()
if err == sql.ErrNoRows {
err = nil
roles = []entity.LabelRole{}
}
if err != nil {
writeServerError(w, method, err)
return
}
json, err := json.Marshal(roles)
if err != nil {
writeJSONMarshalError(w, method, "roles", err)
return
}
writeSuccessBytes(w, json)
}
// ForgotUserPassword initiates the change password procedure.
// Generates a reset token and sends email to the user.
// User has to click link in email and then provide a new password.
func ForgotUserPassword(w http.ResponseWriter, r *http.Request) {
method := "ForgotUserPassword"
p := request.GetPersister(r)
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writeBadRequestError(w, method, "cannot ready payload")
return
}
user := new(entity.User)
err = json.Unmarshal(body, &user)
if err != nil {
writePayloadError(w, method, err)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
token := util.GenerateSalt()
err = p.ForgotUserPassword(user.Email, token)
if err != nil && err != sql.ErrNoRows {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
if err == sql.ErrNoRows {
writeServerError(w, method, fmt.Errorf("User %s not found for password reset process", user.Email))
return
}
log.IfErr(tx.Commit())
appURL := getAppURL(p.Context, fmt.Sprintf("auth/reset/%s", token))
fmt.Println(appURL)
go mail.PasswordReset(user.Email, appURL)
writeSuccessEmptyJSON(w)
}
// ResetUserPassword stores the newly chosen password for the user.
func ResetUserPassword(w http.ResponseWriter, r *http.Request) {
api.SetJSONResponse(w)
p := request.GetPersister(r)
params := mux.Vars(r)
token := params["token"]
if len(token) == 0 {
log.ErrorString("ResetUserPassword - missing password reset token")
api.WriteErrorBadRequest(w, "missing password reset token")
return
}
defer utility.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
api.WriteErrorBadRequest(w, "Bad payload")
log.Error("ResetUserPassword - failed to read body", err)
return
}
newPassword := string(body)
tx, err := request.Db.Beginx()
if err != nil {
api.WriteError(w, err)
log.Error("ResetUserPassword - failed to get DB transaction", err)
return
}
p.Context.Transaction = tx
user, err := p.GetUserByToken(token)
if err != nil {
api.WriteError(w, err)
log.Error("ResetUserPassword - unable to retrieve user", err)
return
}
user.Salt = util.GenerateSalt()
err = p.UpdateUserPassword(user.RefID, user.Salt, util.GeneratePassword(newPassword, user.Salt))
if err != nil {
log.IfErr(tx.Rollback())
api.WriteError(w, err)
log.Error("ResetUserPassword - failed to change password", err)
return
}
log.IfErr(tx.Commit())
_, err = w.Write([]byte("{}"))
log.IfErr(err)
}
// Get user object contain associated accounts but credentials are wiped.
func getSecuredUser(p request.Persister, orgID, user string) (u entity.User, err error) {
u, err = p.GetUser(user)
attachUserAccounts(p, orgID, &u)
return
}
func attachUserAccounts(p request.Persister, orgID string, user *entity.User) {
user.ProtectSecrets()
a, err := p.GetUserAccounts(user.RefID)
if err != nil {
log.Error("Unable to fetch user accounts", err)
return
}
user.Accounts = a
user.Editor = false
user.Admin = false
for _, account := range user.Accounts {
if account.OrgID == orgID {
user.Admin = account.Admin
user.Editor = account.Editor
break
}
}
}
func writeUsers(w http.ResponseWriter, u []entity.User) {
if u == nil {
u = []entity.User{}
}
j, err := json.Marshal(u)
if err != nil {
log.Error("unable to writeUsers", err)
writeServerError(w, "unabe to writeUsers", err)
return
}
writeSuccessBytes(w, j)
}