mirror of
https://github.com/documize/community.git
synced 2025-07-23 15:19:42 +02:00
restructure directories
This commit is contained in:
parent
7e4ed6545b
commit
a2ce777762
159 changed files with 320 additions and 323 deletions
13
core/api/endpoint/TESTDATA.html
Normal file
13
core/api/endpoint/TESTDATA.html
Normal 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>
|
219
core/api/endpoint/attachment_endpoint.go
Normal file
219
core/api/endpoint/attachment_endpoint.go
Normal 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)
|
||||
}
|
431
core/api/endpoint/authentication_endpoint.go
Normal file
431
core/api/endpoint/authentication_endpoint.go
Normal 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
|
||||
}
|
253
core/api/endpoint/conversion_endpoint.go
Normal file
253
core/api/endpoint/conversion_endpoint.go
Normal 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
13
core/api/endpoint/doc.go
Normal 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
|
384
core/api/endpoint/document_endpoint.go
Normal file
384
core/api/endpoint/document_endpoint.go
Normal 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)
|
||||
}
|
110
core/api/endpoint/endpoint_test.go
Normal file
110
core/api/endpoint/endpoint_test.go
Normal 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
180
core/api/endpoint/init.go
Normal 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)
|
||||
}
|
840
core/api/endpoint/label_endpoint.go
Normal file
840
core/api/endpoint/label_endpoint.go
Normal 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
|
||||
}
|
172
core/api/endpoint/meta_endpoint.go
Normal file
172
core/api/endpoint/meta_endpoint.go
Normal 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
|
||||
}
|
68
core/api/endpoint/models/models.go
Normal file
68
core/api/endpoint/models/models.go
Normal 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"`
|
||||
}
|
109
core/api/endpoint/org_endpoint.go
Normal file
109
core/api/endpoint/org_endpoint.go
Normal 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)
|
||||
}
|
647
core/api/endpoint/page_endpoint.go
Normal file
647
core/api/endpoint/page_endpoint.go
Normal 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
276
core/api/endpoint/router.go
Normal 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
|
||||
}
|
178
core/api/endpoint/sections_endpoint.go
Normal file
178
core/api/endpoint/sections_endpoint.go
Normal 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)
|
||||
}
|
557
core/api/endpoint/templates_endpoint.go
Normal file
557
core/api/endpoint/templates_endpoint.go
Normal 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
|
||||
}
|
720
core/api/endpoint/user_endpoint.go
Normal file
720
core/api/endpoint/user_endpoint.go
Normal 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue