mirror of
https://github.com/documize/community.git
synced 2025-07-19 21:29:42 +02:00
725 lines
16 KiB
Go
725 lines
16 KiB
Go
// 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) {
|
|
if IsInvalidLicense() {
|
|
util.WriteBadLicense(w)
|
|
return
|
|
}
|
|
|
|
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", p.Context.GetAppURL("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(), p.Context.GetAppURL(""))
|
|
|
|
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
|
|
account.Active = user.Active
|
|
|
|
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 := p.Context.GetAppURL(fmt.Sprintf("auth/reset/%s", token))
|
|
|
|
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
|
|
user.Active = false
|
|
|
|
for _, account := range user.Accounts {
|
|
if account.OrgID == orgID {
|
|
user.Admin = account.Admin
|
|
user.Editor = account.Editor
|
|
user.Active = account.Active
|
|
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)
|
|
}
|