mirror of
https://github.com/documize/community.git
synced 2025-07-24 07:39:43 +02:00
commit
1c4a4424e5
114 changed files with 6324 additions and 2192 deletions
|
@ -1,4 +1,3 @@
|
|||
gui/public/tinymce/**
|
||||
gui/public/tinymce/
|
||||
gui/public/tinymce
|
||||
|
||||
gui/public/tinymce
|
8
Gopkg.lock
generated
8
Gopkg.lock
generated
|
@ -149,9 +149,15 @@
|
|||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v3"
|
||||
name = "gopkg.in/alexcesaro/quotedprintable.v3"
|
||||
packages = ["."]
|
||||
revision = "2caba252f4dc53eaf6b553000885530023f54623"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "98ebdf85207168f55b51629c6c21eb1459881fa58c280503054994887cbde045"
|
||||
inputs-digest = "56eea54cf0b9e18b3e456199ee01aedeeb07b55073ca3d10345e0c178689aee2"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -52,7 +52,7 @@ Space view.
|
|||
|
||||
## Latest version
|
||||
|
||||
Community edition: v1.57.3
|
||||
Community edition: v1.58.0
|
||||
|
||||
## OS support
|
||||
|
||||
|
|
|
@ -165,7 +165,6 @@ func setupAccount(rt *env.Runtime, completion onboardRequest, serial string) (er
|
|||
labelID := uniqueid.Generate()
|
||||
sql = fmt.Sprintf("insert into label (refid, orgid, label, type, userid) values (\"%s\", \"%s\", \"My Project\", 2, \"%s\")", labelID, orgID, userID)
|
||||
_, err = runSQL(rt, sql)
|
||||
|
||||
if err != nil {
|
||||
rt.Log.Error("insert into label failed", err)
|
||||
}
|
||||
|
@ -180,6 +179,45 @@ func setupAccount(rt *env.Runtime, completion onboardRequest, serial string) (er
|
|||
}
|
||||
}
|
||||
|
||||
// Create some user groups
|
||||
groupDevID := uniqueid.Generate()
|
||||
sql = fmt.Sprintf("INSERT INTO role (refid, orgid, role, purpose) VALUES (\"%s\", \"%s\", \"Technology\", \"On-site and remote development teams\")", groupDevID, orgID)
|
||||
_, err = runSQL(rt, sql)
|
||||
if err != nil {
|
||||
rt.Log.Error("insert into role failed", err)
|
||||
}
|
||||
|
||||
groupProjectID := uniqueid.Generate()
|
||||
sql = fmt.Sprintf("INSERT INTO role (refid, orgid, role, purpose) VALUES (\"%s\", \"%s\", \"Project Management\", \"HQ project management\")", groupProjectID, orgID)
|
||||
_, err = runSQL(rt, sql)
|
||||
if err != nil {
|
||||
rt.Log.Error("insert into role failed", err)
|
||||
}
|
||||
|
||||
groupBackofficeID := uniqueid.Generate()
|
||||
sql = fmt.Sprintf("INSERT INTO role (refid, orgid, role, purpose) VALUES (\"%s\", \"%s\", \"Back Office\", \"Non-IT and PMO personnel\")", groupBackofficeID, orgID)
|
||||
_, err = runSQL(rt, sql)
|
||||
if err != nil {
|
||||
rt.Log.Error("insert into role failed", err)
|
||||
}
|
||||
|
||||
// Join some groups
|
||||
sql = fmt.Sprintf("INSERT INTO rolemember (orgid, roleid, userid) VALUES (\"%s\", \"%s\", \"%s\")", orgID, groupDevID, userID)
|
||||
_, err = runSQL(rt, sql)
|
||||
if err != nil {
|
||||
rt.Log.Error("insert into rolemember failed", err)
|
||||
}
|
||||
sql = fmt.Sprintf("INSERT INTO rolemember (orgid, roleid, userid) VALUES (\"%s\", \"%s\", \"%s\")", orgID, groupProjectID, userID)
|
||||
_, err = runSQL(rt, sql)
|
||||
if err != nil {
|
||||
rt.Log.Error("insert into rolemember failed", err)
|
||||
}
|
||||
sql = fmt.Sprintf("INSERT INTO rolemember (orgid, roleid, userid) VALUES (\"%s\", \"%s\", \"%s\")", orgID, groupBackofficeID, userID)
|
||||
_, err = runSQL(rt, sql)
|
||||
if err != nil {
|
||||
rt.Log.Error("insert into rolemember failed", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
9
core/database/scripts/autobuild/db_00018.sql
Normal file
9
core/database/scripts/autobuild/db_00018.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
/* community edition */
|
||||
|
||||
-- role and role membership
|
||||
ALTER TABLE role ADD COLUMN `purpose` VARCHAR(100) DEFAULT '' NOT NULL AFTER `role`;
|
||||
ALTER TABLE role ADD COLUMN `revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP AFTER `created`;
|
||||
CREATE INDEX idx_role_1 ON role(orgid);
|
||||
|
||||
-- deprecations
|
||||
DROP TABLE IF EXISTS `labelrole`;
|
65
core/mail/CHANGELOG.md
Normal file
65
core/mail/CHANGELOG.md
Normal file
|
@ -0,0 +1,65 @@
|
|||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [2.2.0] - 2018-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- #20: Adds `Message.SetBoundary` to allow specifying a custom MIME boundary.
|
||||
- #22: Adds `Message.SetBodyWriter` to make it easy to use text/template and
|
||||
html/template for message bodies. Contributed by Quantcast.
|
||||
- #25: Adds `Dialer.StartTLSPolicy` so that `MandatoryStartTLS` can be required,
|
||||
or `NoStartTLS` can disable it. Contributed by Quantcast.
|
||||
|
||||
## [2.1.0] - 2017-12-14
|
||||
|
||||
### Added
|
||||
|
||||
- go-gomail#40: Adds `Dialer.LocalName` field to allow specifying the hostname
|
||||
sent with SMTP's HELO command.
|
||||
- go-gomail#47: `Message.SetBody`, `Message.AddAlternative`, and
|
||||
`Message.AddAlternativeWriter` allow specifying the encoding of message parts.
|
||||
- `Dialer.Dial`'s returned `SendCloser` automatically redials after a timeout.
|
||||
- go-gomail#55, go-gomail#56: Adds `Rename` to allow specifying filename
|
||||
of an attachment.
|
||||
- go-gomail#100: Exports `NetDialTimeout` to allow setting a custom dialer.
|
||||
- go-gomail#70: Adds `Dialer.Timeout` field to allow specifying a timeout for
|
||||
dials, reads, and writes.
|
||||
|
||||
### Changed
|
||||
|
||||
- go-gomail#52: `Dialer.Dial` automatically uses CRAM-MD5 when available.
|
||||
- `Dialer.Dial` specifies a default timeout of 10 seconds.
|
||||
- Gomail is forked from <https://github.com/go-gomail/gomail/> to
|
||||
<https://github.com/go-mail/mail/>.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- go-gomail#52: `NewPlainDialer` is deprecated in favor of `NewDialer`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- go-gomail#41, go-gomail#42: Fixes a panic when a `Message` contains a
|
||||
nil header.
|
||||
- go-gomail#44: Fixes `AddAlternativeWriter` replacing the message body instead
|
||||
of adding a body part.
|
||||
- go-gomail#53: Folds long header lines for RFC 2047 compliance.
|
||||
- go-gomail#54: Fixes `Message.FormatAddress` when name is blank.
|
||||
|
||||
## [2.0.0] - 2015-09-02
|
||||
|
||||
- Mailer has been removed. It has been replaced by Dialer and Sender.
|
||||
- `File` type and the `CreateFile` and `OpenFile` functions have been removed.
|
||||
- `Message.Attach` and `Message.Embed` have a new signature.
|
||||
- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter`
|
||||
instead.
|
||||
- `Message.Export` has been removed. `Message.WriteTo` can be used instead.
|
||||
- `Message.DelHeader` has been removed.
|
||||
- The `Bcc` header field is no longer sent. It is far more simpler and
|
||||
efficient: the same message is sent to all recipients instead of sending a
|
||||
different email to each Bcc address.
|
||||
- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN
|
||||
authentication mechanism when needed.
|
||||
- Go 1.2 is now required instead of Go 1.3. No external dependency are used when
|
||||
using Go 1.5.
|
20
core/mail/CONTRIBUTING.md
Normal file
20
core/mail/CONTRIBUTING.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
Thank you for contributing to Gomail! Here are a few guidelines:
|
||||
|
||||
## Bugs
|
||||
|
||||
If you think you found a bug, create an issue and supply the minimum amount
|
||||
of code triggering the bug so it can be reproduced.
|
||||
|
||||
|
||||
## Fixing a bug
|
||||
|
||||
If you want to fix a bug, you can send a pull request. It should contains a
|
||||
new test or update an existing one to cover that bug.
|
||||
|
||||
|
||||
## New feature proposal
|
||||
|
||||
If you think Gomail lacks a feature, you can open an issue or send a pull
|
||||
request. I want to keep Gomail code and API as simple as possible so please
|
||||
describe your needs so we can discuss whether this feature should be added to
|
||||
Gomail or not.
|
20
core/mail/LICENSE
Normal file
20
core/mail/LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alexandre Cesaro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
129
core/mail/README.md
Normal file
129
core/mail/README.md
Normal file
|
@ -0,0 +1,129 @@
|
|||
# Gomail
|
||||
[](https://travis-ci.org/go-mail/mail) [](http://gocover.io/github.com/go-mail/mail) [](https://godoc.org/github.com/go-mail/mail)
|
||||
|
||||
This is an actively maintained fork of [Gomail][1] and includes fixes and
|
||||
improvements for a number of outstanding issues. The current progress is
|
||||
as follows:
|
||||
|
||||
- [x] Timeouts and retries can be specified outside of the 10 second default.
|
||||
- [x] Proxying is supported through specifying a custom [NetDialTimeout][2].
|
||||
- [ ] Filenames are properly encoded for non-ASCII characters.
|
||||
- [ ] Email addresses are properly encoded for non-ASCII characters.
|
||||
- [ ] Embedded files and attachments are tested for their existence.
|
||||
- [ ] An `io.Reader` can be supplied when embedding and attaching files.
|
||||
|
||||
See [Transitioning Existing Codebases][3] for more information on switching.
|
||||
|
||||
[1]: https://github.com/go-gomail/gomail
|
||||
[2]: https://godoc.org/gopkg.in/mail.v2#NetDialTimeout
|
||||
[3]: #transitioning-existing-codebases
|
||||
|
||||
## Introduction
|
||||
|
||||
Gomail is a simple and efficient package to send emails. It is well tested and
|
||||
documented.
|
||||
|
||||
Gomail can only send emails using an SMTP server. But the API is flexible and it
|
||||
is easy to implement other methods for sending emails using a local Postfix, an
|
||||
API, etc.
|
||||
|
||||
It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Gomail supports:
|
||||
- Attachments
|
||||
- Embedded images
|
||||
- HTML and text templates
|
||||
- Automatic encoding of special characters
|
||||
- SSL and TLS
|
||||
- Sending multiple emails with the same SMTP connection
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
https://godoc.org/github.com/go-mail/mail
|
||||
|
||||
|
||||
## Download
|
||||
|
||||
If you're already using a dependency manager, like [dep][dep], use the following
|
||||
import path:
|
||||
|
||||
```
|
||||
github.com/go-mail/mail
|
||||
```
|
||||
|
||||
If you *aren't* using vendoring, `go get` the [Gopkg.in](http://gopkg.in)
|
||||
import path:
|
||||
|
||||
```
|
||||
gopkg.in/mail.v2
|
||||
```
|
||||
|
||||
[dep]: https://github.com/golang/dep#readme
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples in the documentation](https://godoc.org/github.com/go-mail/mail#example-package).
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### x509: certificate signed by unknown authority
|
||||
|
||||
If you get this error it means the certificate used by the SMTP server is not
|
||||
considered valid by the client running Gomail. As a quick workaround you can
|
||||
bypass the verification of the server's certificate chain and host name by using
|
||||
`SetTLSConfig`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
d := mail.NewDialer("smtp.example.com", 587, "user", "123456")
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
// Send emails using d.
|
||||
}
|
||||
```
|
||||
|
||||
Note, however, that this is insecure and should not be used in production.
|
||||
|
||||
### Transitioning Existing Codebases
|
||||
|
||||
If you're already using the original Gomail, switching is as easy as updating
|
||||
the import line to:
|
||||
|
||||
```
|
||||
import gomail "gopkg.in/mail.v2"
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for
|
||||
more info.
|
||||
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
|
||||
## Support & Contact
|
||||
|
||||
You can ask questions on the [Gomail
|
||||
thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion)
|
||||
in the Go mailing-list.
|
49
core/mail/auth.go
Normal file
49
core/mail/auth.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism.
|
||||
type loginAuth struct {
|
||||
username string
|
||||
password string
|
||||
host string
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
if !server.TLS {
|
||||
advertised := false
|
||||
for _, mechanism := range server.Auth {
|
||||
if mechanism == "LOGIN" {
|
||||
advertised = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !advertised {
|
||||
return "", nil, errors.New("gomail: unencrypted connection")
|
||||
}
|
||||
}
|
||||
if server.Name != a.host {
|
||||
return "", nil, errors.New("gomail: wrong host name")
|
||||
}
|
||||
return "LOGIN", nil, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if !more {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.Equal(fromServer, []byte("Username:")):
|
||||
return []byte(a.username), nil
|
||||
case bytes.Equal(fromServer, []byte("Password:")):
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer)
|
||||
}
|
||||
}
|
6
core/mail/doc.go
Normal file
6
core/mail/doc.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Package gomail provides a simple interface to compose emails and to mail them
|
||||
// efficiently.
|
||||
//
|
||||
// More info on Github: https://github.com/go-mail/mail
|
||||
//
|
||||
package mail
|
334
core/mail/message.go
Normal file
334
core/mail/message.go
Normal file
|
@ -0,0 +1,334 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message represents an email.
|
||||
type Message struct {
|
||||
header header
|
||||
parts []*part
|
||||
attachments []*file
|
||||
embedded []*file
|
||||
charset string
|
||||
encoding Encoding
|
||||
hEncoder mimeEncoder
|
||||
buf bytes.Buffer
|
||||
boundary string
|
||||
}
|
||||
|
||||
type header map[string][]string
|
||||
|
||||
type part struct {
|
||||
contentType string
|
||||
copier func(io.Writer) error
|
||||
encoding Encoding
|
||||
}
|
||||
|
||||
// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
|
||||
// by default.
|
||||
func NewMessage(settings ...MessageSetting) *Message {
|
||||
m := &Message{
|
||||
header: make(header),
|
||||
charset: "UTF-8",
|
||||
encoding: QuotedPrintable,
|
||||
}
|
||||
|
||||
m.applySettings(settings)
|
||||
|
||||
if m.encoding == Base64 {
|
||||
m.hEncoder = bEncoding
|
||||
} else {
|
||||
m.hEncoder = qEncoding
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Reset resets the message so it can be reused. The message keeps its previous
|
||||
// settings so it is in the same state that after a call to NewMessage.
|
||||
func (m *Message) Reset() {
|
||||
for k := range m.header {
|
||||
delete(m.header, k)
|
||||
}
|
||||
m.parts = nil
|
||||
m.attachments = nil
|
||||
m.embedded = nil
|
||||
}
|
||||
|
||||
func (m *Message) applySettings(settings []MessageSetting) {
|
||||
for _, s := range settings {
|
||||
s(m)
|
||||
}
|
||||
}
|
||||
|
||||
// A MessageSetting can be used as an argument in NewMessage to configure an
|
||||
// email.
|
||||
type MessageSetting func(m *Message)
|
||||
|
||||
// SetCharset is a message setting to set the charset of the email.
|
||||
func SetCharset(charset string) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.charset = charset
|
||||
}
|
||||
}
|
||||
|
||||
// SetEncoding is a message setting to set the encoding of the email.
|
||||
func SetEncoding(enc Encoding) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.encoding = enc
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding represents a MIME encoding scheme like quoted-printable or base64.
|
||||
type Encoding string
|
||||
|
||||
const (
|
||||
// QuotedPrintable represents the quoted-printable encoding as defined in
|
||||
// RFC 2045.
|
||||
QuotedPrintable Encoding = "quoted-printable"
|
||||
// Base64 represents the base64 encoding as defined in RFC 2045.
|
||||
Base64 Encoding = "base64"
|
||||
// Unencoded can be used to avoid encoding the body of an email. The headers
|
||||
// will still be encoded using quoted-printable encoding.
|
||||
Unencoded Encoding = "8bit"
|
||||
)
|
||||
|
||||
// SetBoundary sets a custom multipart boundary.
|
||||
func (m *Message) SetBoundary(boundary string) {
|
||||
m.boundary = boundary
|
||||
}
|
||||
|
||||
// SetHeader sets a value to the given header field.
|
||||
func (m *Message) SetHeader(field string, value ...string) {
|
||||
m.encodeHeader(value)
|
||||
m.header[field] = value
|
||||
}
|
||||
|
||||
func (m *Message) encodeHeader(values []string) {
|
||||
for i := range values {
|
||||
values[i] = m.encodeString(values[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) encodeString(value string) string {
|
||||
return m.hEncoder.Encode(m.charset, value)
|
||||
}
|
||||
|
||||
// SetHeaders sets the message headers.
|
||||
func (m *Message) SetHeaders(h map[string][]string) {
|
||||
for k, v := range h {
|
||||
m.SetHeader(k, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAddressHeader sets an address to the given header field.
|
||||
func (m *Message) SetAddressHeader(field, address, name string) {
|
||||
m.header[field] = []string{m.FormatAddress(address, name)}
|
||||
}
|
||||
|
||||
// FormatAddress formats an address and a name as a valid RFC 5322 address.
|
||||
func (m *Message) FormatAddress(address, name string) string {
|
||||
if name == "" {
|
||||
return address
|
||||
}
|
||||
|
||||
enc := m.encodeString(name)
|
||||
if enc == name {
|
||||
m.buf.WriteByte('"')
|
||||
for i := 0; i < len(name); i++ {
|
||||
b := name[i]
|
||||
if b == '\\' || b == '"' {
|
||||
m.buf.WriteByte('\\')
|
||||
}
|
||||
m.buf.WriteByte(b)
|
||||
}
|
||||
m.buf.WriteByte('"')
|
||||
} else if hasSpecials(name) {
|
||||
m.buf.WriteString(bEncoding.Encode(m.charset, name))
|
||||
} else {
|
||||
m.buf.WriteString(enc)
|
||||
}
|
||||
m.buf.WriteString(" <")
|
||||
m.buf.WriteString(address)
|
||||
m.buf.WriteByte('>')
|
||||
|
||||
addr := m.buf.String()
|
||||
m.buf.Reset()
|
||||
return addr
|
||||
}
|
||||
|
||||
func hasSpecials(text string) bool {
|
||||
for i := 0; i < len(text); i++ {
|
||||
switch c := text[i]; c {
|
||||
case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDateHeader sets a date to the given header field.
|
||||
func (m *Message) SetDateHeader(field string, date time.Time) {
|
||||
m.header[field] = []string{m.FormatDate(date)}
|
||||
}
|
||||
|
||||
// FormatDate formats a date as a valid RFC 5322 date.
|
||||
func (m *Message) FormatDate(date time.Time) string {
|
||||
return date.Format(time.RFC1123Z)
|
||||
}
|
||||
|
||||
// GetHeader gets a header field.
|
||||
func (m *Message) GetHeader(field string) []string {
|
||||
return m.header[field]
|
||||
}
|
||||
|
||||
// SetBody sets the body of the message. It replaces any content previously set
|
||||
// by SetBody, SetBodyWriter, AddAlternative or AddAlternativeWriter.
|
||||
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
|
||||
m.SetBodyWriter(contentType, newCopier(body), settings...)
|
||||
}
|
||||
|
||||
// SetBodyWriter sets the body of the message. It can be useful with the
|
||||
// text/template or html/template packages.
|
||||
func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||||
m.parts = []*part{m.newPart(contentType, f, settings)}
|
||||
}
|
||||
|
||||
// AddAlternative adds an alternative part to the message.
|
||||
//
|
||||
// It is commonly used to send HTML emails that default to the plain text
|
||||
// version for backward compatibility. AddAlternative appends the new part to
|
||||
// the end of the message. So the plain text part should be added before the
|
||||
// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
|
||||
func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
|
||||
m.AddAlternativeWriter(contentType, newCopier(body), settings...)
|
||||
}
|
||||
|
||||
func newCopier(s string) func(io.Writer) error {
|
||||
return func(w io.Writer) error {
|
||||
_, err := io.WriteString(w, s)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// AddAlternativeWriter adds an alternative part to the message. It can be
|
||||
// useful with the text/template or html/template packages.
|
||||
func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||||
m.parts = append(m.parts, m.newPart(contentType, f, settings))
|
||||
}
|
||||
|
||||
func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
|
||||
p := &part{
|
||||
contentType: contentType,
|
||||
copier: f,
|
||||
encoding: m.encoding,
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
s(p)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// A PartSetting can be used as an argument in Message.SetBody,
|
||||
// Message.SetBodyWriter, Message.AddAlternative or Message.AddAlternativeWriter
|
||||
// to configure the part added to a message.
|
||||
type PartSetting func(*part)
|
||||
|
||||
// SetPartEncoding sets the encoding of the part added to the message. By
|
||||
// default, parts use the same encoding than the message.
|
||||
func SetPartEncoding(e Encoding) PartSetting {
|
||||
return PartSetting(func(p *part) {
|
||||
p.encoding = e
|
||||
})
|
||||
}
|
||||
|
||||
type file struct {
|
||||
Name string
|
||||
Header map[string][]string
|
||||
CopyFunc func(w io.Writer) error
|
||||
}
|
||||
|
||||
func (f *file) setHeader(field, value string) {
|
||||
f.Header[field] = []string{value}
|
||||
}
|
||||
|
||||
// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
|
||||
type FileSetting func(*file)
|
||||
|
||||
// SetHeader is a file setting to set the MIME header of the message part that
|
||||
// contains the file content.
|
||||
//
|
||||
// Mandatory headers are automatically added if they are not set when sending
|
||||
// the email.
|
||||
func SetHeader(h map[string][]string) FileSetting {
|
||||
return func(f *file) {
|
||||
for k, v := range h {
|
||||
f.Header[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rename is a file setting to set the name of the attachment if the name is
|
||||
// different than the filename on disk.
|
||||
func Rename(name string) FileSetting {
|
||||
return func(f *file) {
|
||||
f.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// SetCopyFunc is a file setting to replace the function that runs when the
|
||||
// message is sent. It should copy the content of the file to the io.Writer.
|
||||
//
|
||||
// The default copy function opens the file with the given filename, and copy
|
||||
// its content to the io.Writer.
|
||||
func SetCopyFunc(f func(io.Writer) error) FileSetting {
|
||||
return func(fi *file) {
|
||||
fi.CopyFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file {
|
||||
f := &file{
|
||||
Name: filepath.Base(name),
|
||||
Header: make(map[string][]string),
|
||||
CopyFunc: func(w io.Writer) error {
|
||||
h, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, h); err != nil {
|
||||
h.Close()
|
||||
return err
|
||||
}
|
||||
return h.Close()
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
s(f)
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
return []*file{f}
|
||||
}
|
||||
|
||||
return append(list, f)
|
||||
}
|
||||
|
||||
// Attach attaches the files to the email.
|
||||
func (m *Message) Attach(filename string, settings ...FileSetting) {
|
||||
m.attachments = m.appendFile(m.attachments, filename, settings)
|
||||
}
|
||||
|
||||
// Embed embeds the images to the email.
|
||||
func (m *Message) Embed(filename string, settings ...FileSetting) {
|
||||
m.embedded = m.appendFile(m.embedded, filename, settings)
|
||||
}
|
21
core/mail/mime.go
Normal file
21
core/mail/mime.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
// +build go1.5
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"mime/quotedprintable"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var newQPWriter = quotedprintable.NewWriter
|
||||
|
||||
type mimeEncoder struct {
|
||||
mime.WordEncoder
|
||||
}
|
||||
|
||||
var (
|
||||
bEncoding = mimeEncoder{mime.BEncoding}
|
||||
qEncoding = mimeEncoder{mime.QEncoding}
|
||||
lastIndexByte = strings.LastIndexByte
|
||||
)
|
25
core/mail/mime_go14.go
Normal file
25
core/mail/mime_go14.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// +build !go1.5
|
||||
|
||||
package mail
|
||||
|
||||
import "gopkg.in/alexcesaro/quotedprintable.v3"
|
||||
|
||||
var newQPWriter = quotedprintable.NewWriter
|
||||
|
||||
type mimeEncoder struct {
|
||||
quotedprintable.WordEncoder
|
||||
}
|
||||
|
||||
var (
|
||||
bEncoding = mimeEncoder{quotedprintable.BEncoding}
|
||||
qEncoding = mimeEncoder{quotedprintable.QEncoding}
|
||||
lastIndexByte = func(s string, c byte) int {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
|
||||
if s[i] == c {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
)
|
116
core/mail/send.go
Normal file
116
core/mail/send.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
stdmail "net/mail"
|
||||
)
|
||||
|
||||
// Sender is the interface that wraps the Send method.
|
||||
//
|
||||
// Send sends an email to the given addresses.
|
||||
type Sender interface {
|
||||
Send(from string, to []string, msg io.WriterTo) error
|
||||
}
|
||||
|
||||
// SendCloser is the interface that groups the Send and Close methods.
|
||||
type SendCloser interface {
|
||||
Sender
|
||||
Close() error
|
||||
}
|
||||
|
||||
// A SendFunc is a function that sends emails to the given addresses.
|
||||
//
|
||||
// The SendFunc type is an adapter to allow the use of ordinary functions as
|
||||
// email senders. If f is a function with the appropriate signature, SendFunc(f)
|
||||
// is a Sender object that calls f.
|
||||
type SendFunc func(from string, to []string, msg io.WriterTo) error
|
||||
|
||||
// Send calls f(from, to, msg).
|
||||
func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error {
|
||||
return f(from, to, msg)
|
||||
}
|
||||
|
||||
// Send sends emails using the given Sender.
|
||||
func Send(s Sender, msg ...*Message) error {
|
||||
for i, m := range msg {
|
||||
if err := send(s, m); err != nil {
|
||||
return fmt.Errorf("gomail: could not send email %d: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(s Sender, m *Message) error {
|
||||
from, err := m.getFrom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
to, err := m.getRecipients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Send(from, to, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) getFrom() (string, error) {
|
||||
from := m.header["Sender"]
|
||||
if len(from) == 0 {
|
||||
from = m.header["From"]
|
||||
if len(from) == 0 {
|
||||
return "", errors.New(`gomail: invalid message, "From" field is absent`)
|
||||
}
|
||||
}
|
||||
|
||||
return parseAddress(from[0])
|
||||
}
|
||||
|
||||
func (m *Message) getRecipients() ([]string, error) {
|
||||
n := 0
|
||||
for _, field := range []string{"To", "Cc", "Bcc"} {
|
||||
if addresses, ok := m.header[field]; ok {
|
||||
n += len(addresses)
|
||||
}
|
||||
}
|
||||
list := make([]string, 0, n)
|
||||
|
||||
for _, field := range []string{"To", "Cc", "Bcc"} {
|
||||
if addresses, ok := m.header[field]; ok {
|
||||
for _, a := range addresses {
|
||||
addr, err := parseAddress(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = addAddress(list, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func addAddress(list []string, addr string) []string {
|
||||
for _, a := range list {
|
||||
if addr == a {
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
return append(list, addr)
|
||||
}
|
||||
|
||||
func parseAddress(field string) (string, error) {
|
||||
addr, err := stdmail.ParseAddress(field)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
|
||||
}
|
||||
return addr.Address, nil
|
||||
}
|
292
core/mail/smtp.go
Normal file
292
core/mail/smtp.go
Normal file
|
@ -0,0 +1,292 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Dialer is a dialer to an SMTP server.
|
||||
type Dialer struct {
|
||||
// Host represents the host of the SMTP server.
|
||||
Host string
|
||||
// Port represents the port of the SMTP server.
|
||||
Port int
|
||||
// Username is the username to use to authenticate to the SMTP server.
|
||||
Username string
|
||||
// Password is the password to use to authenticate to the SMTP server.
|
||||
Password string
|
||||
// Auth represents the authentication mechanism used to authenticate to the
|
||||
// SMTP server.
|
||||
Auth smtp.Auth
|
||||
// SSL defines whether an SSL connection is used. It should be false in
|
||||
// most cases since the authentication mechanism should use the STARTTLS
|
||||
// extension instead.
|
||||
SSL bool
|
||||
// TLSConfig represents the TLS configuration used for the TLS (when the
|
||||
// STARTTLS extension is used) or SSL connection.
|
||||
TLSConfig *tls.Config
|
||||
// StartTLSPolicy represents the TLS security level required to
|
||||
// communicate with the SMTP server.
|
||||
//
|
||||
// This defaults to OpportunisticStartTLS for backwards compatibility,
|
||||
// but we recommend MandatoryStartTLS for all modern SMTP servers.
|
||||
//
|
||||
// This option has no effect if SSL is set to true.
|
||||
StartTLSPolicy StartTLSPolicy
|
||||
// LocalName is the hostname sent to the SMTP server with the HELO command.
|
||||
// By default, "localhost" is sent.
|
||||
LocalName string
|
||||
// Timeout to use for read/write operations. Defaults to 10 seconds, can
|
||||
// be set to 0 to disable timeouts.
|
||||
Timeout time.Duration
|
||||
// Whether we should retry mailing if the connection returned an error,
|
||||
// defaults to true.
|
||||
RetryFailure bool
|
||||
}
|
||||
|
||||
// NewDialer returns a new SMTP Dialer. The given parameters are used to connect
|
||||
// to the SMTP server.
|
||||
func NewDialer(host string, port int, username, password string) *Dialer {
|
||||
return &Dialer{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
SSL: port == 465,
|
||||
Timeout: 10 * time.Second,
|
||||
RetryFailure: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPlainDialer returns a new SMTP Dialer. The given parameters are used to
|
||||
// connect to the SMTP server.
|
||||
//
|
||||
// Deprecated: Use NewDialer instead.
|
||||
func NewPlainDialer(host string, port int, username, password string) *Dialer {
|
||||
return NewDialer(host, port, username, password)
|
||||
}
|
||||
|
||||
// NetDialTimeout specifies the DialTimeout function to establish a connection
|
||||
// to the SMTP server. This can be used to override dialing in the case that a
|
||||
// proxy or other special behavior is needed.
|
||||
var NetDialTimeout = net.DialTimeout
|
||||
|
||||
// Dial dials and authenticates to an SMTP server. The returned SendCloser
|
||||
// should be closed when done using it.
|
||||
func (d *Dialer) Dial() (SendCloser, error) {
|
||||
conn, err := NetDialTimeout("tcp", addr(d.Host, d.Port), d.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.SSL {
|
||||
conn = tlsClient(conn, d.tlsConfig())
|
||||
}
|
||||
|
||||
c, err := smtpNewClient(conn, d.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.Timeout > 0 {
|
||||
conn.SetDeadline(time.Now().Add(d.Timeout))
|
||||
}
|
||||
|
||||
if d.LocalName != "" {
|
||||
if err := c.Hello(d.LocalName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !d.SSL && d.StartTLSPolicy != NoStartTLS {
|
||||
ok, _ := c.Extension("STARTTLS")
|
||||
if !ok && d.StartTLSPolicy == MandatoryStartTLS {
|
||||
err := StartTLSUnsupportedError{
|
||||
Policy: d.StartTLSPolicy}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
if err := c.StartTLS(d.tlsConfig()); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.Auth == nil && d.Username != "" {
|
||||
if ok, auths := c.Extension("AUTH"); ok {
|
||||
if strings.Contains(auths, "CRAM-MD5") {
|
||||
d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password)
|
||||
} else if strings.Contains(auths, "LOGIN") &&
|
||||
!strings.Contains(auths, "PLAIN") {
|
||||
d.Auth = &loginAuth{
|
||||
username: d.Username,
|
||||
password: d.Password,
|
||||
host: d.Host,
|
||||
}
|
||||
} else {
|
||||
d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.Auth != nil {
|
||||
if err = c.Auth(d.Auth); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &smtpSender{c, conn, d}, nil
|
||||
}
|
||||
|
||||
func (d *Dialer) tlsConfig() *tls.Config {
|
||||
if d.TLSConfig == nil {
|
||||
return &tls.Config{ServerName: d.Host}
|
||||
}
|
||||
return d.TLSConfig
|
||||
}
|
||||
|
||||
// StartTLSPolicy constants are valid values for Dialer.StartTLSPolicy.
|
||||
type StartTLSPolicy int
|
||||
|
||||
const (
|
||||
// OpportunisticStartTLS means that SMTP transactions are encrypted if
|
||||
// STARTTLS is supported by the SMTP server. Otherwise, messages are
|
||||
// sent in the clear. This is the default setting.
|
||||
OpportunisticStartTLS StartTLSPolicy = iota
|
||||
// MandatoryStartTLS means that SMTP transactions must be encrypted.
|
||||
// SMTP transactions are aborted unless STARTTLS is supported by the
|
||||
// SMTP server.
|
||||
MandatoryStartTLS
|
||||
// NoStartTLS means encryption is disabled and messages are sent in the
|
||||
// clear.
|
||||
NoStartTLS = -1
|
||||
)
|
||||
|
||||
func (policy *StartTLSPolicy) String() string {
|
||||
switch *policy {
|
||||
case OpportunisticStartTLS:
|
||||
return "OpportunisticStartTLS"
|
||||
case MandatoryStartTLS:
|
||||
return "MandatoryStartTLS"
|
||||
case NoStartTLS:
|
||||
return "NoStartTLS"
|
||||
default:
|
||||
return fmt.Sprintf("StartTLSPolicy:%v", *policy)
|
||||
}
|
||||
}
|
||||
|
||||
// StartTLSUnsupportedError is returned by Dial when connecting to an SMTP
|
||||
// server that does not support STARTTLS.
|
||||
type StartTLSUnsupportedError struct {
|
||||
Policy StartTLSPolicy
|
||||
}
|
||||
|
||||
func (e StartTLSUnsupportedError) Error() string {
|
||||
return "gomail: " + e.Policy.String() + " required, but " +
|
||||
"SMTP server does not support STARTTLS"
|
||||
}
|
||||
|
||||
func addr(host string, port int) string {
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
|
||||
// DialAndSend opens a connection to the SMTP server, sends the given emails and
|
||||
// closes the connection.
|
||||
func (d *Dialer) DialAndSend(m ...*Message) error {
|
||||
s, err := d.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
return Send(s, m...)
|
||||
}
|
||||
|
||||
type smtpSender struct {
|
||||
smtpClient
|
||||
conn net.Conn
|
||||
d *Dialer
|
||||
}
|
||||
|
||||
func (c *smtpSender) retryError(err error) bool {
|
||||
if !c.d.RetryFailure {
|
||||
return false
|
||||
}
|
||||
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
return true
|
||||
}
|
||||
|
||||
return err == io.EOF
|
||||
}
|
||||
|
||||
func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
|
||||
if c.d.Timeout > 0 {
|
||||
c.conn.SetDeadline(time.Now().Add(c.d.Timeout))
|
||||
}
|
||||
|
||||
if err := c.Mail(from); err != nil {
|
||||
if c.retryError(err) {
|
||||
// This is probably due to a timeout, so reconnect and try again.
|
||||
sc, derr := c.d.Dial()
|
||||
if derr == nil {
|
||||
if s, ok := sc.(*smtpSender); ok {
|
||||
*c = *s
|
||||
return c.Send(from, to, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err := c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = msg.WriteTo(w); err != nil {
|
||||
w.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (c *smtpSender) Close() error {
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Stubbed out for tests.
|
||||
var (
|
||||
tlsClient = tls.Client
|
||||
smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
|
||||
return smtp.NewClient(conn, host)
|
||||
}
|
||||
)
|
||||
|
||||
type smtpClient interface {
|
||||
Hello(string) error
|
||||
Extension(string) (bool, string)
|
||||
StartTLS(*tls.Config) error
|
||||
Auth(smtp.Auth) error
|
||||
Mail(string) error
|
||||
Rcpt(string) error
|
||||
Data() (io.WriteCloser, error)
|
||||
Quit() error
|
||||
Close() error
|
||||
}
|
309
core/mail/writeto.go
Normal file
309
core/mail/writeto.go
Normal file
|
@ -0,0 +1,309 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WriteTo implements io.WriterTo. It dumps the whole message into w.
|
||||
func (m *Message) WriteTo(w io.Writer) (int64, error) {
|
||||
mw := &messageWriter{w: w}
|
||||
mw.writeMessage(m)
|
||||
return mw.n, mw.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeMessage(m *Message) {
|
||||
if _, ok := m.header["Mime-Version"]; !ok {
|
||||
w.writeString("Mime-Version: 1.0\r\n")
|
||||
}
|
||||
if _, ok := m.header["Date"]; !ok {
|
||||
w.writeHeader("Date", m.FormatDate(now()))
|
||||
}
|
||||
w.writeHeaders(m.header)
|
||||
|
||||
if m.hasMixedPart() {
|
||||
w.openMultipart("mixed", m.boundary)
|
||||
}
|
||||
|
||||
if m.hasRelatedPart() {
|
||||
w.openMultipart("related", m.boundary)
|
||||
}
|
||||
|
||||
if m.hasAlternativePart() {
|
||||
w.openMultipart("alternative", m.boundary)
|
||||
}
|
||||
for _, part := range m.parts {
|
||||
w.writePart(part, m.charset)
|
||||
}
|
||||
if m.hasAlternativePart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.embedded, false)
|
||||
if m.hasRelatedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.attachments, true)
|
||||
if m.hasMixedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) hasMixedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasRelatedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasAlternativePart() bool {
|
||||
return len(m.parts) > 1
|
||||
}
|
||||
|
||||
type messageWriter struct {
|
||||
w io.Writer
|
||||
n int64
|
||||
writers [3]*multipart.Writer
|
||||
partWriter io.Writer
|
||||
depth uint8
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *messageWriter) openMultipart(mimeType, boundary string) {
|
||||
mw := multipart.NewWriter(w)
|
||||
if boundary != "" {
|
||||
mw.SetBoundary(boundary)
|
||||
}
|
||||
contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
|
||||
w.writers[w.depth] = mw
|
||||
|
||||
if w.depth == 0 {
|
||||
w.writeHeader("Content-Type", contentType)
|
||||
w.writeString("\r\n")
|
||||
} else {
|
||||
w.createPart(map[string][]string{
|
||||
"Content-Type": {contentType},
|
||||
})
|
||||
}
|
||||
w.depth++
|
||||
}
|
||||
|
||||
func (w *messageWriter) createPart(h map[string][]string) {
|
||||
w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h)
|
||||
}
|
||||
|
||||
func (w *messageWriter) closeMultipart() {
|
||||
if w.depth > 0 {
|
||||
w.writers[w.depth-1].Close()
|
||||
w.depth--
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writePart(p *part, charset string) {
|
||||
w.writeHeaders(map[string][]string{
|
||||
"Content-Type": {p.contentType + "; charset=" + charset},
|
||||
"Content-Transfer-Encoding": {string(p.encoding)},
|
||||
})
|
||||
w.writeBody(p.copier, p.encoding)
|
||||
}
|
||||
|
||||
func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
|
||||
for _, f := range files {
|
||||
if _, ok := f.Header["Content-Type"]; !ok {
|
||||
mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
|
||||
if mediaType == "" {
|
||||
mediaType = "application/octet-stream"
|
||||
}
|
||||
f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
|
||||
f.setHeader("Content-Transfer-Encoding", string(Base64))
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Disposition"]; !ok {
|
||||
var disp string
|
||||
if isAttachment {
|
||||
disp = "attachment"
|
||||
} else {
|
||||
disp = "inline"
|
||||
}
|
||||
f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if !isAttachment {
|
||||
if _, ok := f.Header["Content-ID"]; !ok {
|
||||
f.setHeader("Content-ID", "<"+f.Name+">")
|
||||
}
|
||||
}
|
||||
w.writeHeaders(f.Header)
|
||||
w.writeBody(f.CopyFunc, Base64)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) Write(p []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, errors.New("gomail: cannot write as writer is in error")
|
||||
}
|
||||
|
||||
var n int
|
||||
n, w.err = w.w.Write(p)
|
||||
w.n += int64(n)
|
||||
return n, w.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeString(s string) {
|
||||
n, _ := io.WriteString(w.w, s)
|
||||
w.n += int64(n)
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeader(k string, v ...string) {
|
||||
w.writeString(k)
|
||||
if len(v) == 0 {
|
||||
w.writeString(":\r\n")
|
||||
return
|
||||
}
|
||||
w.writeString(": ")
|
||||
|
||||
// Max header line length is 78 characters in RFC 5322 and 76 characters
|
||||
// in RFC 2047. So for the sake of simplicity we use the 76 characters
|
||||
// limit.
|
||||
charsLeft := 76 - len(k) - len(": ")
|
||||
|
||||
for i, s := range v {
|
||||
// If the line is already too long, insert a newline right away.
|
||||
if charsLeft < 1 {
|
||||
if i == 0 {
|
||||
w.writeString("\r\n ")
|
||||
} else {
|
||||
w.writeString(",\r\n ")
|
||||
}
|
||||
charsLeft = 75
|
||||
} else if i != 0 {
|
||||
w.writeString(", ")
|
||||
charsLeft -= 2
|
||||
}
|
||||
|
||||
// While the header content is too long, fold it by inserting a newline.
|
||||
for len(s) > charsLeft {
|
||||
s = w.writeLine(s, charsLeft)
|
||||
charsLeft = 75
|
||||
}
|
||||
w.writeString(s)
|
||||
if i := lastIndexByte(s, '\n'); i != -1 {
|
||||
charsLeft = 75 - (len(s) - i - 1)
|
||||
} else {
|
||||
charsLeft -= len(s)
|
||||
}
|
||||
}
|
||||
w.writeString("\r\n")
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeLine(s string, charsLeft int) string {
|
||||
// If there is already a newline before the limit. Write the line.
|
||||
if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
|
||||
for i := charsLeft - 1; i >= 0; i-- {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// We could not insert a newline cleanly so look for a space or a newline
|
||||
// even if it is after the limit.
|
||||
for i := 75; i < len(s); i++ {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
if s[i] == '\n' {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Too bad, no space or newline in the whole string. Just write everything.
|
||||
w.writeString(s)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeaders(h map[string][]string) {
|
||||
if w.depth == 0 {
|
||||
for k, v := range h {
|
||||
if k != "Bcc" {
|
||||
w.writeHeader(k, v...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.createPart(h)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) {
|
||||
var subWriter io.Writer
|
||||
if w.depth == 0 {
|
||||
w.writeString("\r\n")
|
||||
subWriter = w.w
|
||||
} else {
|
||||
subWriter = w.partWriter
|
||||
}
|
||||
|
||||
if enc == Base64 {
|
||||
wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
} else if enc == Unencoded {
|
||||
w.err = f(subWriter)
|
||||
} else {
|
||||
wc := newQPWriter(subWriter)
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// As required by RFC 2045, 6.7. (page 21) for quoted-printable, and
|
||||
// RFC 2045, 6.8. (page 25) for base64.
|
||||
const maxLineLen = 76
|
||||
|
||||
// base64LineWriter limits text encoded in base64 to 76 characters per line
|
||||
type base64LineWriter struct {
|
||||
w io.Writer
|
||||
lineLen int
|
||||
}
|
||||
|
||||
func newBase64LineWriter(w io.Writer) *base64LineWriter {
|
||||
return &base64LineWriter{w: w}
|
||||
}
|
||||
|
||||
func (w *base64LineWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
for len(p)+w.lineLen > maxLineLen {
|
||||
w.w.Write(p[:maxLineLen-w.lineLen])
|
||||
w.w.Write([]byte("\r\n"))
|
||||
p = p[maxLineLen-w.lineLen:]
|
||||
n += maxLineLen - w.lineLen
|
||||
w.lineLen = 0
|
||||
}
|
||||
|
||||
w.w.Write(p)
|
||||
w.lineLen += len(p)
|
||||
|
||||
return n + len(p), nil
|
||||
}
|
||||
|
||||
// Stubbed out for testing.
|
||||
var now = time.Now
|
|
@ -35,6 +35,11 @@ func WriteNotFoundError(w http.ResponseWriter, method string, id string) {
|
|||
w.Write([]byte("{Error: 'Not found'}"))
|
||||
}
|
||||
|
||||
// WriteNotFound notifies HTTP client of 'record not found' error.
|
||||
func WriteNotFound(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// WriteServerError notifies HTTP client of general application error.
|
||||
func WriteServerError(w http.ResponseWriter, method string, err error) {
|
||||
writeStatus(w, http.StatusBadRequest)
|
||||
|
|
|
@ -94,7 +94,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// User list from Documize
|
||||
dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx)
|
||||
dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx, "")
|
||||
if err != nil {
|
||||
result.Message = "Error: unable to fetch Documize users"
|
||||
result.IsError = true
|
||||
|
|
|
@ -82,10 +82,10 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
perm := pm.Permission{}
|
||||
perm.OrgID = ctx.OrgID
|
||||
perm.Who = "user"
|
||||
perm.Who = pm.UserPermission
|
||||
perm.WhoID = ctx.UserID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "category"
|
||||
perm.Scope = pm.ScopeRow
|
||||
perm.Location = pm.LocationCategory
|
||||
perm.RefID = cat.RefID
|
||||
perm.Action = pm.CategoryView
|
||||
|
||||
|
@ -155,15 +155,11 @@ func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
cat, err := h.Store.Category.GetAllBySpace(ctx, spaceID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(cat) == 0 {
|
||||
cat = []category.Category{}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, cat)
|
||||
}
|
||||
|
||||
|
@ -320,10 +316,6 @@ func (h *Handler) GetSummary(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
s = []category.SummaryModel{}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, s)
|
||||
}
|
||||
|
||||
|
|
|
@ -80,8 +80,9 @@ func (s Scope) GetAllBySpace(ctx domain.RequestContext, spaceID string) (c []cat
|
|||
))
|
||||
ORDER BY category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
if err == sql.ErrNoRows || len(c) == 0 {
|
||||
err = nil
|
||||
c = []category.Category{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select all categories for space %s", spaceID))
|
||||
|
@ -190,20 +191,20 @@ func (s Scope) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows in
|
|||
// GetSpaceCategorySummary returns number of documents and users for space categories.
|
||||
func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string) (c []category.SummaryModel, err error) {
|
||||
err = s.Runtime.Db.Select(&c, `
|
||||
SELECT 'documents' as type, categoryid, COUNT(*) as count FROM categorymember WHERE orgid=? AND labelid=? GROUP BY categoryid, type
|
||||
SELECT 'documents' as type, categoryid, COUNT(*) as count
|
||||
FROM categorymember
|
||||
WHERE orgid=? AND labelid=? GROUP BY categoryid, type
|
||||
UNION ALL
|
||||
SELECT 'users' as type, refid AS categoryid, count(*) AS count FROM permission WHERE orgid=? AND who='user' AND location='category'
|
||||
AND refid IN (SELECT refid FROM category WHERE orgid=? AND labelid=?)
|
||||
GROUP BY refid, type
|
||||
UNION ALL
|
||||
SELECT 'users' as type, p.refid AS categoryid, count(*) AS count FROM rolemember r LEFT JOIN permission p ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.who='role' AND p.location='category'
|
||||
AND p.refid IN (SELECT refid FROM category WHERE orgid=? AND labelid=?)
|
||||
GROUP BY p.refid, type`,
|
||||
ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, spaceID)
|
||||
SELECT 'users' as type, refid AS categoryid, count(*) AS count
|
||||
FROM permission
|
||||
WHERE orgid=? AND location='category'
|
||||
AND refid IN (SELECT refid FROM category WHERE orgid=? AND labelid=?)
|
||||
GROUP BY refid, type`,
|
||||
ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, spaceID /*, ctx.OrgID, ctx.OrgID, spaceID*/)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
if err == sql.ErrNoRows || len(c) == 0 {
|
||||
err = nil
|
||||
c = []category.SummaryModel{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("select category summary for space %s", spaceID))
|
||||
|
|
349
domain/group/endpoint.go
Normal file
349
domain/group/endpoint.go
Normal file
|
@ -0,0 +1,349 @@
|
|||
// Copyright 2018 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 group
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/request"
|
||||
"github.com/documize/community/core/response"
|
||||
"github.com/documize/community/core/uniqueid"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/model/audit"
|
||||
"github.com/documize/community/model/group"
|
||||
)
|
||||
|
||||
// Handler contains the runtime information such as logging and database.
|
||||
type Handler struct {
|
||||
Runtime *env.Runtime
|
||||
Store *domain.Store
|
||||
}
|
||||
|
||||
// Add saves new user group.
|
||||
func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
||||
method := "group.Add"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
if !ctx.Administrator {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "body")
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
var g group.Group
|
||||
err = json.Unmarshal(body, &g)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "group")
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
g.RefID = uniqueid.Generate()
|
||||
g.OrgID = ctx.OrgID
|
||||
|
||||
if len(g.Name) == 0 {
|
||||
response.WriteMissingDataError(w, method, "name")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Store.Group.Add(ctx, g)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
g, err = h.Store.Group.Get(ctx, g.RefID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeGroupAdd)
|
||||
|
||||
response.WriteJSON(w, g)
|
||||
}
|
||||
|
||||
// Groups returns all user groups for org.
|
||||
func (h *Handler) Groups(w http.ResponseWriter, r *http.Request) {
|
||||
method := "group.Groups"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
g, err := h.Store.Group.GetAll(ctx)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, g)
|
||||
}
|
||||
|
||||
// Update saves group name and description changes.
|
||||
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
method := "group.Update"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
if !ctx.Administrator {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := request.Param(r, "groupID")
|
||||
if len(groupID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "groupID")
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "body")
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
var g group.Group
|
||||
err = json.Unmarshal(body, &g)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "group")
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
g.OrgID = ctx.OrgID
|
||||
g.RefID = groupID
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Store.Group.Update(ctx, g)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeGroupUpdate)
|
||||
|
||||
g, err = h.Store.Group.Get(ctx, g.RefID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, g)
|
||||
}
|
||||
|
||||
// Delete removes group and associated member records.
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
method := "group.Delete"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
if !ctx.Administrator {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := request.Param(r, "groupID")
|
||||
if len(groupID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "groupID")
|
||||
return
|
||||
}
|
||||
|
||||
g, err := h.Store.Group.Get(ctx, groupID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.Store.Group.Delete(ctx, g.RefID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeGroupDelete)
|
||||
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
||||
// GetGroupMembers returns all users associated with given group.
|
||||
func (h *Handler) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
|
||||
method := "group.GetGroupMembers"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
// Should be no reason for non-admin to see members
|
||||
if !ctx.Administrator {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := request.Param(r, "groupID")
|
||||
if len(groupID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "groupID")
|
||||
return
|
||||
}
|
||||
|
||||
m, err := h.Store.Group.GetGroupMembers(ctx, groupID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, m)
|
||||
}
|
||||
|
||||
// JoinGroup adds user to group.
|
||||
func (h *Handler) JoinGroup(w http.ResponseWriter, r *http.Request) {
|
||||
method := "group.JoinGroup"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
// Should be no reason for non-admin to see members
|
||||
if !ctx.Administrator {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := request.Param(r, "groupID")
|
||||
if len(groupID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "groupID")
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.Param(r, "userID")
|
||||
if len(userID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "userID")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// safety first
|
||||
err = h.Store.Group.LeaveGroup(ctx, groupID, userID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// now we can join group
|
||||
err = h.Store.Group.JoinGroup(ctx, groupID, userID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeGroupJoin)
|
||||
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
||||
// LeaveGroup removes user to group.
|
||||
func (h *Handler) LeaveGroup(w http.ResponseWriter, r *http.Request) {
|
||||
method := "group.LeaveGroup"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
// Should be no reason for non-admin to see members
|
||||
if !ctx.Administrator {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
groupID := request.Param(r, "groupID")
|
||||
if len(groupID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "groupID")
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.Param(r, "userID")
|
||||
if len(userID) == 0 {
|
||||
response.WriteMissingDataError(w, method, "userID")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Store.Group.LeaveGroup(ctx, groupID, userID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeGroupLeave)
|
||||
|
||||
response.WriteEmpty(w)
|
||||
}
|
165
domain/group/mysql/store.go
Normal file
165
domain/group/mysql/store.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
// Copyright 2018 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/store/mysql"
|
||||
"github.com/documize/community/model/group"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Scope provides data access to MySQL.
|
||||
type Scope struct {
|
||||
Runtime *env.Runtime
|
||||
}
|
||||
|
||||
// Add inserts new user group into store.
|
||||
func (s Scope) Add(ctx domain.RequestContext, g group.Group) (err error) {
|
||||
g.Created = time.Now().UTC()
|
||||
g.Revised = time.Now().UTC()
|
||||
|
||||
_, err = ctx.Transaction.Exec("INSERT INTO role (refid, orgid, role, purpose, created, revised) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
g.RefID, g.OrgID, g.Name, g.Purpose, g.Created, g.Revised)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "insert group")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get returns requested group.
|
||||
func (s Scope) Get(ctx domain.RequestContext, refID string) (g group.Group, err error) {
|
||||
err = s.Runtime.Db.Get(&g,
|
||||
`SELECT id, refid, orgid, role as name, purpose, created, revised FROM role WHERE orgid=? AND refid=?`,
|
||||
ctx.OrgID, refID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "select group")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetAll returns all user groups for current orgID.
|
||||
func (s Scope) GetAll(ctx domain.RequestContext) (groups []group.Group, err error) {
|
||||
err = s.Runtime.Db.Select(&groups,
|
||||
`SELECT a.id, a.refid, a.orgid, a.role as name, a.purpose, a.created, a.revised, COUNT(b.roleid) AS members
|
||||
FROM role a
|
||||
LEFT JOIN rolemember b ON a.refid=b.roleid
|
||||
WHERE a.orgid=?
|
||||
GROUP BY a.id, a.refid, a.orgid, a.role, a.purpose, a.created, a.revised
|
||||
ORDER BY a.role`,
|
||||
ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows || len(groups) == 0 {
|
||||
err = nil
|
||||
groups = []group.Group{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "select groups")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Update group name and description.
|
||||
func (s Scope) Update(ctx domain.RequestContext, g group.Group) (err error) {
|
||||
g.Revised = time.Now().UTC()
|
||||
|
||||
_, err = ctx.Transaction.Exec("UPDATE role SET role=?, purpose=?, revised=? WHERE orgid=? AND refid=?",
|
||||
g.Name, g.Purpose, g.Revised, ctx.OrgID, g.RefID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "update group")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Delete removes group from store.
|
||||
func (s Scope) Delete(ctx domain.RequestContext, refID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
b.DeleteConstrained(ctx.Transaction, "role", ctx.OrgID, refID)
|
||||
return b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM rolemember WHERE orgid=\"%s\" AND roleid=\"%s\"", ctx.OrgID, refID))
|
||||
}
|
||||
|
||||
// GetGroupMembers returns all user associated with given group.
|
||||
func (s Scope) GetGroupMembers(ctx domain.RequestContext, groupID string) (members []group.Member, err error) {
|
||||
err = s.Runtime.Db.Select(&members,
|
||||
`SELECT a.id, a.orgid, a.roleid, a.userid,
|
||||
IFNULL(b.firstname, '') as firstname, IFNULL(b.lastname, '') as lastname
|
||||
FROM rolemember a
|
||||
LEFT JOIN user b ON b.refid=a.userid
|
||||
WHERE a.orgid=? AND a.roleid=?
|
||||
ORDER BY b.firstname, b.lastname`,
|
||||
ctx.OrgID, groupID)
|
||||
|
||||
if err == sql.ErrNoRows || len(members) == 0 {
|
||||
err = nil
|
||||
members = []group.Member{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "select members")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// JoinGroup adds user to group.
|
||||
func (s Scope) JoinGroup(ctx domain.RequestContext, groupID, userID string) (err error) {
|
||||
_, err = ctx.Transaction.Exec("INSERT INTO rolemember (orgid, roleid, userid) VALUES (?, ?, ?)", ctx.OrgID, groupID, userID)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "insert group member")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// LeaveGroup removes user from group.
|
||||
func (s Scope) LeaveGroup(ctx domain.RequestContext, groupID, userID string) (err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
_, err = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM rolemember WHERE orgid=\"%s\" AND roleid=\"%s\" AND userid=\"%s\"", ctx.OrgID, groupID, userID))
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "clear group member")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetMembers returns members for every group.
|
||||
// Useful when you need to bulk fetch membership records
|
||||
// for subsequent processing.
|
||||
func (s Scope) GetMembers(ctx domain.RequestContext) (r []group.Record, err error) {
|
||||
err = s.Runtime.Db.Select(&r,
|
||||
`SELECT a.id, a.orgid, a.roleid, a.userid, b.role as name, b.purpose
|
||||
FROM rolemember a, role b
|
||||
WHERE a.orgid=? AND a.roleid=b.refid
|
||||
ORDER BY a.userid`,
|
||||
ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows || len(r) == 0 {
|
||||
err = nil
|
||||
r = []group.Record{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "select group members")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -76,7 +76,7 @@ background-color: #f6f6f6;
|
|||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">View document</a>
|
||||
<a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">View document</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
|
|
|
@ -14,57 +14,50 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/documize/community/server/web"
|
||||
"github.com/documize/community/domain/smtp"
|
||||
)
|
||||
|
||||
// DocumentApprover notifies user who has just been granted document approval rights.
|
||||
func (m *Mailer) DocumentApprover(recipient, inviter, url, document string) {
|
||||
method := "DocumentApprover"
|
||||
m.LoadCredentials()
|
||||
|
||||
file, err := web.ReadFile("mail/document-approver.html")
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
|
||||
emailTemplate := string(file)
|
||||
m.Initialize()
|
||||
|
||||
// check inviter name
|
||||
if inviter == "Hello You" || len(inviter) == 0 {
|
||||
inviter = "Your colleague"
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s has granted you document approval", inviter)
|
||||
|
||||
e := NewEmail()
|
||||
e.From = m.Credentials.SMTPsender
|
||||
e.To = []string{recipient}
|
||||
e.Subject = subject
|
||||
em := smtp.EmailMessage{}
|
||||
em.Subject = fmt.Sprintf("%s has granted you document approval", inviter)
|
||||
em.ToEmail = recipient
|
||||
em.ToName = recipient
|
||||
|
||||
parameters := struct {
|
||||
Subject string
|
||||
Inviter string
|
||||
Url string
|
||||
URL string
|
||||
Document string
|
||||
}{
|
||||
subject,
|
||||
em.Subject,
|
||||
inviter,
|
||||
url,
|
||||
document,
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
|
||||
t.Execute(buffer, ¶meters)
|
||||
e.HTML = buffer.Bytes()
|
||||
html, err := m.ParseTemplate("mail/document-approver.html", parameters)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
em.BodyHTML = html
|
||||
|
||||
err = e.Send(m.GetHost(), m.GetAuth())
|
||||
ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
|
||||
}
|
||||
if !ok {
|
||||
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ background-color: #f6f6f6;
|
|||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a>
|
||||
<a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
|
|
|
@ -82,7 +82,7 @@ background-color: #f6f6f6;
|
|||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a>
|
||||
<a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to access Documize</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
|
|
|
@ -9,61 +9,57 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
// jshint ignore:start
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"bytes"
|
||||
"html/template"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/mail"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/setting"
|
||||
ds "github.com/documize/community/domain/smtp"
|
||||
"github.com/documize/community/server/web"
|
||||
)
|
||||
|
||||
// Mailer provides emailing facilities
|
||||
type Mailer struct {
|
||||
Runtime *env.Runtime
|
||||
Store *domain.Store
|
||||
Context domain.RequestContext
|
||||
Credentials Credentials
|
||||
Runtime *env.Runtime
|
||||
Store *domain.Store
|
||||
Context domain.RequestContext
|
||||
Config ds.Config
|
||||
Dialer *mail.Dialer
|
||||
}
|
||||
|
||||
// Credentials holds SMTP endpoint and authentication methods
|
||||
type Credentials struct {
|
||||
SMTPuserid string
|
||||
SMTPpassword string
|
||||
SMTPhost string
|
||||
SMTPport string
|
||||
SMTPsender string
|
||||
// Initialize prepares mailer instance for action.
|
||||
func (m *Mailer) Initialize() {
|
||||
m.Config = setting.GetSMTPConfig(m.Store)
|
||||
m.Dialer, _ = ds.Connect(m.Config)
|
||||
}
|
||||
|
||||
// GetAuth to return SMTP authentication details
|
||||
func (m *Mailer) GetAuth() smtp.Auth {
|
||||
a := smtp.PlainAuth("", m.Credentials.SMTPuserid, m.Credentials.SMTPpassword, m.Credentials.SMTPhost)
|
||||
return a
|
||||
// Send prepares and sends email.
|
||||
func (m *Mailer) Send(em ds.EmailMessage) (ok bool, err error) {
|
||||
ok, err = ds.SendMessage(m.Dialer, m.Config, em)
|
||||
return
|
||||
}
|
||||
|
||||
// GetHost to return SMTP host details
|
||||
func (m *Mailer) GetHost() string {
|
||||
h := m.Credentials.SMTPhost + ":" + m.Credentials.SMTPport
|
||||
return h
|
||||
}
|
||||
|
||||
// LoadCredentials loads up SMTP details from database
|
||||
func (m *Mailer) LoadCredentials() {
|
||||
userID, _ := m.Store.Setting.Get("SMTP", "userid")
|
||||
m.Credentials.SMTPuserid = strings.TrimSpace(userID)
|
||||
|
||||
pwd, _ := m.Store.Setting.Get("SMTP", "password")
|
||||
m.Credentials.SMTPpassword = strings.TrimSpace(pwd)
|
||||
|
||||
host, _ := m.Store.Setting.Get("SMTP", "host")
|
||||
m.Credentials.SMTPhost = strings.TrimSpace(host)
|
||||
|
||||
port, _ := m.Store.Setting.Get("SMTP", "port")
|
||||
m.Credentials.SMTPport = strings.TrimSpace(port)
|
||||
|
||||
sender, _ := m.Store.Setting.Get("SMTP", "sender")
|
||||
m.Credentials.SMTPsender = strings.TrimSpace(sender)
|
||||
// ParseTemplate produces email template.
|
||||
func (m *Mailer) ParseTemplate(filename string, params interface{}) (html string, err error) {
|
||||
html = ""
|
||||
|
||||
file, err := web.ReadFile(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
emailTemplate := string(file)
|
||||
buffer := new(bytes.Buffer)
|
||||
|
||||
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
|
||||
t.Execute(buffer, ¶ms)
|
||||
|
||||
html = buffer.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ background-color: #f6f6f6;
|
|||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to reset your password</a>
|
||||
<a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Click here to reset your password</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
|
|
|
@ -75,7 +75,7 @@ background-color: #f6f6f6;
|
|||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Login to Documize</a>
|
||||
<a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Login to Documize</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
|
|
|
@ -81,7 +81,7 @@ background-color: #f6f6f6;
|
|||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Go to Documize</a>
|
||||
<a href="{{.URL}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">Go to Documize</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
|
||||
|
|
|
@ -1,374 +0,0 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
/*
|
||||
Elements of the software in this file were modified from github.com/jordan-wright/email and
|
||||
are subject to the licence below:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Jordan Wright
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Package mail sends transactional emails.
|
||||
// The code in smtp.go is designed to provide an "email interface for humans".
|
||||
// Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLineLength is the maximum line length per RFC 2045
|
||||
MaxLineLength = 76
|
||||
)
|
||||
|
||||
// Email is the type used for email messages
|
||||
type Email struct {
|
||||
From string
|
||||
To []string
|
||||
Bcc []string
|
||||
Cc []string
|
||||
Subject string
|
||||
Text []byte // Plaintext message (optional)
|
||||
HTML []byte // Html message (optional)
|
||||
Headers textproto.MIMEHeader
|
||||
Attachments []*Attachment
|
||||
ReadReceipt []string
|
||||
}
|
||||
|
||||
// NewEmail creates an Email, and returns the pointer to it.
|
||||
func NewEmail() *Email {
|
||||
return &Email{Headers: textproto.MIMEHeader{}}
|
||||
}
|
||||
|
||||
// Attach is used to attach content from an io.Reader to the email.
|
||||
// Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type
|
||||
// The function will return the created Attachment for reference, as well as nil for the error, if successful.
|
||||
func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) {
|
||||
var buffer bytes.Buffer
|
||||
if _, err = io.Copy(&buffer, r); err != nil {
|
||||
return
|
||||
}
|
||||
at := &Attachment{
|
||||
Filename: filename,
|
||||
Header: textproto.MIMEHeader{},
|
||||
Content: buffer.Bytes(),
|
||||
}
|
||||
// Get the Content-Type to be used in the MIMEHeader
|
||||
if c != "" {
|
||||
at.Header.Set("Content-Type", c)
|
||||
} else {
|
||||
// If the Content-Type is blank, set the Content-Type to "application/octet-stream"
|
||||
at.Header.Set("Content-Type", "application/octet-stream")
|
||||
}
|
||||
at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename))
|
||||
at.Header.Set("Content-ID", fmt.Sprintf("<%s>", filename))
|
||||
at.Header.Set("Content-Transfer-Encoding", "base64")
|
||||
e.Attachments = append(e.Attachments, at)
|
||||
return at, nil
|
||||
}
|
||||
|
||||
// AttachFile is used to attach content to the email.
|
||||
// It attempts to open the file referenced by filename and, if successful, creates an Attachment.
|
||||
// This Attachment is then appended to the slice of Email.Attachments.
|
||||
// The function will then return the Attachment for reference, as well as nil for the error, if successful.
|
||||
func (e *Email) AttachFile(filename string) (a *Attachment, err error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ct := mime.TypeByExtension(filepath.Ext(filename))
|
||||
basename := filepath.Base(filename)
|
||||
return e.Attach(f, basename, ct)
|
||||
}
|
||||
|
||||
// msgHeaders merges the Email's various fields and custom headers together in a
|
||||
// standards compliant way to create a MIMEHeader to be used in the resulting
|
||||
// message. It does not alter e.Headers.
|
||||
//
|
||||
// "e"'s fields To, Cc, From, Subject will be used unless they are present in
|
||||
// e.Headers. Unless set in e.Headers, "Date" will filled with the current time.
|
||||
func (e *Email) msgHeaders() textproto.MIMEHeader {
|
||||
res := make(textproto.MIMEHeader, len(e.Headers)+4)
|
||||
if e.Headers != nil {
|
||||
for _, h := range []string{"To", "Cc", "From", "Subject", "Date"} {
|
||||
if v, ok := e.Headers[h]; ok {
|
||||
res[h] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set headers if there are values.
|
||||
if _, ok := res["To"]; !ok && len(e.To) > 0 {
|
||||
res.Set("To", strings.Join(e.To, ", "))
|
||||
}
|
||||
if _, ok := res["Cc"]; !ok && len(e.Cc) > 0 {
|
||||
res.Set("Cc", strings.Join(e.Cc, ", "))
|
||||
}
|
||||
if _, ok := res["Subject"]; !ok && e.Subject != "" {
|
||||
res.Set("Subject", e.Subject)
|
||||
}
|
||||
// Date and From are required headers.
|
||||
if _, ok := res["From"]; !ok {
|
||||
res.Set("From", e.From)
|
||||
}
|
||||
if _, ok := res["Date"]; !ok {
|
||||
res.Set("Date", time.Now().Format(time.RFC1123Z))
|
||||
}
|
||||
if _, ok := res["Mime-Version"]; !ok {
|
||||
res.Set("Mime-Version", "1.0")
|
||||
}
|
||||
for field, vals := range e.Headers {
|
||||
if _, ok := res[field]; !ok {
|
||||
res[field] = vals
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
|
||||
func (e *Email) Bytes() ([]byte, error) {
|
||||
// TODO: better guess buffer size
|
||||
buff := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
|
||||
headers := e.msgHeaders()
|
||||
w := multipart.NewWriter(buff)
|
||||
// TODO: determine the content type based on message/attachment mix.
|
||||
headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
|
||||
headerToBytes(buff, headers)
|
||||
io.WriteString(buff, "\r\n")
|
||||
// Start the multipart/mixed part
|
||||
fmt.Fprintf(buff, "--%s\r\n", w.Boundary())
|
||||
header := textproto.MIMEHeader{}
|
||||
// Check to see if there is a Text or HTML field
|
||||
if len(e.Text) > 0 || len(e.HTML) > 0 {
|
||||
subWriter := multipart.NewWriter(buff)
|
||||
// Create the multipart alternative part
|
||||
header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary()))
|
||||
// Write the header
|
||||
headerToBytes(buff, header)
|
||||
// Create the body sections
|
||||
if len(e.Text) > 0 {
|
||||
header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8"))
|
||||
header.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
if _, err := subWriter.CreatePart(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write the text
|
||||
if err := quotePrintEncode(buff, e.Text); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(e.HTML) > 0 {
|
||||
header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8"))
|
||||
header.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
if _, err := subWriter.CreatePart(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write the text
|
||||
if err := quotePrintEncode(buff, e.HTML); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := subWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Create attachment part, if necessary
|
||||
for _, a := range e.Attachments {
|
||||
ap, err := w.CreatePart(a.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write the base64Wrapped content to the part
|
||||
base64Wrap(ap, a.Content)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
|
||||
// Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
|
||||
// This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
|
||||
func (e *Email) Send(addr string, a smtp.Auth) error {
|
||||
// Merge the To, Cc, and Bcc fields
|
||||
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
for i := 0; i < len(to); i++ {
|
||||
addr, err := mail.ParseAddress(to[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to[i] = addr.Address
|
||||
}
|
||||
// Check to make sure there is at least one recipient and one "From" address
|
||||
if e.From == "" || len(to) == 0 {
|
||||
return errors.New("Must specify at least one From address and one To address")
|
||||
}
|
||||
from, err := mail.ParseAddress(e.From)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return smtpSendMail(addr, a, from.Address, to, raw)
|
||||
}
|
||||
|
||||
var smtpSendMail = smtp.SendMail // so that it can be overloaded for testing
|
||||
|
||||
// Attachment is a struct representing an email attachment.
|
||||
// Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
|
||||
type Attachment struct {
|
||||
Filename string
|
||||
Header textproto.MIMEHeader
|
||||
Content []byte
|
||||
}
|
||||
|
||||
// quotePrintEncode writes the quoted-printable text to the IO Writer (according to RFC 2045)
|
||||
func quotePrintEncode(w io.Writer, body []byte) error {
|
||||
var buf [3]byte
|
||||
mc := 0
|
||||
for _, c := range body {
|
||||
// We're assuming Unix style text formats as input (LF line break), and
|
||||
// quoted-printable uses CRLF line breaks. (Literal CRs will become
|
||||
// "=0D", but probably shouldn't be there to begin with!)
|
||||
if c == '\n' {
|
||||
_, err := io.WriteString(w, "\r\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mc = 0
|
||||
continue
|
||||
}
|
||||
|
||||
var nextOut []byte
|
||||
if isPrintable[c] {
|
||||
buf[0] = c
|
||||
nextOut = buf[:1]
|
||||
} else {
|
||||
nextOut = buf[:]
|
||||
qpEscape(nextOut, c)
|
||||
}
|
||||
|
||||
// Add a soft line break if the next (encoded) byte would push this line
|
||||
// to or past the limit.
|
||||
if mc+len(nextOut) >= MaxLineLength {
|
||||
if _, err := io.WriteString(w, "=\r\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
mc = 0
|
||||
}
|
||||
|
||||
if _, err := w.Write(nextOut); err != nil {
|
||||
return err
|
||||
}
|
||||
mc += len(nextOut)
|
||||
}
|
||||
// No trailing end-of-line?? Soft line break, then. TODO: is this sane?
|
||||
if mc > 0 {
|
||||
_, err := io.WriteString(w, "=\r\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isPrintable holds true if the byte given is "printable" according to RFC 2045, false otherwise
|
||||
var isPrintable [256]bool
|
||||
|
||||
func init() {
|
||||
for c := '!'; c <= '<'; c++ {
|
||||
isPrintable[c] = true
|
||||
}
|
||||
for c := '>'; c <= '~'; c++ {
|
||||
isPrintable[c] = true
|
||||
}
|
||||
isPrintable[' '] = true
|
||||
isPrintable['\n'] = true
|
||||
isPrintable['\t'] = true
|
||||
}
|
||||
|
||||
// qpEscape is a helper function for quotePrintEncode which escapes a
|
||||
// non-printable byte. Expects len(dest) == 3.
|
||||
func qpEscape(dest []byte, c byte) {
|
||||
const nums = "0123456789ABCDEF"
|
||||
dest[0] = '='
|
||||
dest[1] = nums[(c&0xf0)>>4]
|
||||
dest[2] = nums[(c & 0xf)]
|
||||
}
|
||||
|
||||
// base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
|
||||
// The output is then written to the specified io.Writer
|
||||
func base64Wrap(w io.Writer, b []byte) {
|
||||
// 57 raw bytes per 76-byte base64 line.
|
||||
const maxRaw = 57
|
||||
// Buffer for each line, including trailing CRLF.
|
||||
buffer := make([]byte, MaxLineLength+len("\r\n"))
|
||||
copy(buffer[MaxLineLength:], "\r\n")
|
||||
// Process raw chunks until there's no longer enough to fill a line.
|
||||
for len(b) >= maxRaw {
|
||||
base64.StdEncoding.Encode(buffer, b[:maxRaw])
|
||||
w.Write(buffer)
|
||||
b = b[maxRaw:]
|
||||
}
|
||||
// Handle the last chunk of bytes.
|
||||
if len(b) > 0 {
|
||||
out := buffer[:base64.StdEncoding.EncodedLen(len(b))]
|
||||
base64.StdEncoding.Encode(out, b)
|
||||
out = append(out, "\r\n"...)
|
||||
w.Write(out)
|
||||
}
|
||||
}
|
||||
|
||||
// headerToBytes renders "header" to "buff". If there are multiple values for a
|
||||
// field, multiple "Field: value\r\n" lines will be emitted.
|
||||
func headerToBytes(buff *bytes.Buffer, header textproto.MIMEHeader) {
|
||||
for field, vals := range header {
|
||||
for _, subval := range vals {
|
||||
io.WriteString(buff, field+": "+subval+"\r\n")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,114 +9,100 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
// jshint ignore:start
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/documize/community/server/web"
|
||||
"github.com/documize/community/domain/smtp"
|
||||
)
|
||||
|
||||
// ShareSpaceExistingUser provides an existing user with a link to a newly shared space.
|
||||
func (m *Mailer) ShareSpaceExistingUser(recipient, inviter, url, folder, intro string) {
|
||||
method := "ShareSpaceExistingUser"
|
||||
m.LoadCredentials()
|
||||
|
||||
file, err := web.ReadFile("mail/share-space-existing-user.html")
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
|
||||
emailTemplate := string(file)
|
||||
m.Initialize()
|
||||
|
||||
// check inviter name
|
||||
if inviter == "Hello You" || len(inviter) == 0 {
|
||||
inviter = "Your colleague"
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s has shared %s with you", inviter, folder)
|
||||
|
||||
e := NewEmail()
|
||||
e.From = m.Credentials.SMTPsender
|
||||
e.To = []string{recipient}
|
||||
e.Subject = subject
|
||||
em := smtp.EmailMessage{}
|
||||
em.Subject = fmt.Sprintf("%s has shared %s with you", inviter, folder)
|
||||
em.ToEmail = recipient
|
||||
em.ToName = recipient
|
||||
|
||||
parameters := struct {
|
||||
Subject string
|
||||
Inviter string
|
||||
Url string
|
||||
URL string
|
||||
Folder string
|
||||
Intro string
|
||||
}{
|
||||
subject,
|
||||
em.Subject,
|
||||
inviter,
|
||||
url,
|
||||
folder,
|
||||
intro,
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
|
||||
t.Execute(buffer, ¶meters)
|
||||
e.HTML = buffer.Bytes()
|
||||
html, err := m.ParseTemplate("mail/share-space-existing-user.html", parameters)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
em.BodyHTML = html
|
||||
|
||||
err = e.Send(m.GetHost(), m.GetAuth())
|
||||
ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
|
||||
}
|
||||
if !ok {
|
||||
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
|
||||
}
|
||||
}
|
||||
|
||||
// ShareSpaceNewUser invites new user providing Credentials, explaining the product and stating who is inviting them.
|
||||
func (m *Mailer) ShareSpaceNewUser(recipient, inviter, url, space, invitationMessage string) {
|
||||
method := "ShareSpaceNewUser"
|
||||
m.LoadCredentials()
|
||||
|
||||
file, err := web.ReadFile("mail/share-space-new-user.html")
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
|
||||
emailTemplate := string(file)
|
||||
m.Initialize()
|
||||
|
||||
// check inviter name
|
||||
if inviter == "Hello You" || len(inviter) == 0 {
|
||||
inviter = "Your colleague"
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s has shared %s with you on Documize", inviter, space)
|
||||
|
||||
e := NewEmail()
|
||||
e.From = m.Credentials.SMTPsender
|
||||
e.To = []string{recipient}
|
||||
e.Subject = subject
|
||||
em := smtp.EmailMessage{}
|
||||
em.Subject = fmt.Sprintf("%s has shared %s with you on Documize", inviter, space)
|
||||
em.ToEmail = recipient
|
||||
em.ToName = recipient
|
||||
|
||||
parameters := struct {
|
||||
Subject string
|
||||
Inviter string
|
||||
Url string
|
||||
URL string
|
||||
Invitation string
|
||||
Folder string
|
||||
}{
|
||||
subject,
|
||||
em.Subject,
|
||||
inviter,
|
||||
url,
|
||||
invitationMessage,
|
||||
space,
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
|
||||
t.Execute(buffer, ¶meters)
|
||||
e.HTML = buffer.Bytes()
|
||||
html, err := m.ParseTemplate("mail/share-space-new-user.html", parameters)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
em.BodyHTML = html
|
||||
|
||||
err = e.Send(m.GetHost(), m.GetAuth())
|
||||
ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
|
||||
}
|
||||
if !ok {
|
||||
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,149 +9,130 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
// jshint ignore:start
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/documize/community/server/web"
|
||||
"github.com/documize/community/domain/smtp"
|
||||
)
|
||||
|
||||
// InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them.
|
||||
func (m *Mailer) InviteNewUser(recipient, inviter, url, username, password string) {
|
||||
method := "InviteNewUser"
|
||||
m.LoadCredentials()
|
||||
|
||||
file, err := web.ReadFile("mail/invite-new-user.html")
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
|
||||
emailTemplate := string(file)
|
||||
m.Initialize()
|
||||
|
||||
// check inviter name
|
||||
if inviter == "Hello You" || len(inviter) == 0 {
|
||||
inviter = "Your colleague"
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s has invited you to Documize", inviter)
|
||||
|
||||
e := NewEmail()
|
||||
e.From = m.Credentials.SMTPsender
|
||||
e.To = []string{recipient}
|
||||
e.Subject = subject
|
||||
em := smtp.EmailMessage{}
|
||||
em.Subject = fmt.Sprintf("%s has invited you to Documize", inviter)
|
||||
em.ToEmail = recipient
|
||||
em.ToName = recipient
|
||||
|
||||
parameters := struct {
|
||||
Subject string
|
||||
Inviter string
|
||||
Url string
|
||||
URL string
|
||||
Username string
|
||||
Password string
|
||||
}{
|
||||
subject,
|
||||
em.Subject,
|
||||
inviter,
|
||||
url,
|
||||
recipient,
|
||||
password,
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
|
||||
t.Execute(buffer, ¶meters)
|
||||
e.HTML = buffer.Bytes()
|
||||
html, err := m.ParseTemplate("mail/invite-new-user.html", parameters)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
em.BodyHTML = html
|
||||
|
||||
err = e.Send(m.GetHost(), m.GetAuth())
|
||||
ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
|
||||
}
|
||||
if !ok {
|
||||
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
|
||||
}
|
||||
}
|
||||
|
||||
// InviteExistingUser invites a known user to an organization.
|
||||
func (m *Mailer) InviteExistingUser(recipient, inviter, url string) {
|
||||
method := "InviteExistingUser"
|
||||
m.LoadCredentials()
|
||||
|
||||
file, err := web.ReadFile("mail/invite-existing-user.html")
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
|
||||
emailTemplate := string(file)
|
||||
m.Initialize()
|
||||
|
||||
// check inviter name
|
||||
if inviter == "Hello You" || len(inviter) == 0 {
|
||||
inviter = "Your colleague"
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("%s has invited you to their Documize account", inviter)
|
||||
|
||||
e := NewEmail()
|
||||
e.From = m.Credentials.SMTPsender
|
||||
e.To = []string{recipient}
|
||||
e.Subject = subject
|
||||
em := smtp.EmailMessage{}
|
||||
em.Subject = fmt.Sprintf("%s has invited you to their Documize account", inviter)
|
||||
em.ToEmail = recipient
|
||||
em.ToName = recipient
|
||||
|
||||
parameters := struct {
|
||||
Subject string
|
||||
Inviter string
|
||||
Url string
|
||||
URL string
|
||||
}{
|
||||
subject,
|
||||
em.Subject,
|
||||
inviter,
|
||||
url,
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
|
||||
t.Execute(buffer, ¶meters)
|
||||
e.HTML = buffer.Bytes()
|
||||
html, err := m.ParseTemplate("mail/invite-existing-user.html", parameters)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
em.BodyHTML = html
|
||||
|
||||
err = e.Send(m.GetHost(), m.GetAuth())
|
||||
ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
|
||||
}
|
||||
if !ok {
|
||||
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
|
||||
}
|
||||
}
|
||||
|
||||
// PasswordReset sends a reset email with an embedded token.
|
||||
func (m *Mailer) PasswordReset(recipient, url string) {
|
||||
method := "PasswordReset"
|
||||
m.LoadCredentials()
|
||||
m.Initialize()
|
||||
|
||||
file, err := web.ReadFile("mail/password-reset.html")
|
||||
em := smtp.EmailMessage{}
|
||||
em.Subject = "Documize password reset request"
|
||||
em.ToEmail = recipient
|
||||
em.ToName = recipient
|
||||
|
||||
parameters := struct {
|
||||
Subject string
|
||||
URL string
|
||||
}{
|
||||
em.Subject,
|
||||
url,
|
||||
}
|
||||
|
||||
html, err := m.ParseTemplate("mail/password-reset.html", parameters)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
|
||||
return
|
||||
}
|
||||
em.BodyHTML = html
|
||||
|
||||
emailTemplate := string(file)
|
||||
|
||||
subject := "Documize password reset request"
|
||||
|
||||
e := NewEmail()
|
||||
e.From = m.Credentials.SMTPsender //e.g. "Documize <hello@documize.com>"
|
||||
e.To = []string{recipient}
|
||||
e.Subject = subject
|
||||
|
||||
parameters := struct {
|
||||
Subject string
|
||||
Url string
|
||||
}{
|
||||
subject,
|
||||
url,
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
|
||||
t.Execute(buffer, ¶meters)
|
||||
e.HTML = buffer.Bytes()
|
||||
|
||||
err = e.Send(m.GetHost(), m.GetAuth())
|
||||
ok, err := smtp.SendMessage(m.Dialer, m.Config, em)
|
||||
if err != nil {
|
||||
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
|
||||
}
|
||||
if !ok {
|
||||
m.Runtime.Log.Info(fmt.Sprintf("%s unable to send email"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ func (h *Handler) Meta(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
org, err := h.Store.Organization.GetOrganizationByDomain(data.URL)
|
||||
if err != nil {
|
||||
h.Runtime.Log.Error("unable to fetch request meta for "+data.URL, err)
|
||||
response.WriteForbiddenError(w)
|
||||
h.Runtime.Log.Info("unable to fetch request meta for " + data.URL)
|
||||
response.WriteNotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -79,16 +79,15 @@ func (s Scope) GetOrganizationByDomain(subdomain string) (o org.Organization, er
|
|||
return
|
||||
}
|
||||
|
||||
err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE domain=? AND active=1",
|
||||
subdomain)
|
||||
|
||||
// match on given domain name
|
||||
err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE domain=? AND active=1", subdomain)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
|
||||
// we try to match on empty domain as last resort
|
||||
// match on empty domain as last resort
|
||||
err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE domain='' AND active=1")
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrap(err, "unable to execute select for empty subdomain")
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/mail"
|
||||
"github.com/documize/community/model/audit"
|
||||
"github.com/documize/community/model/group"
|
||||
"github.com/documize/community/model/permission"
|
||||
"github.com/documize/community/model/space"
|
||||
"github.com/documize/community/model/user"
|
||||
|
@ -123,52 +124,85 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
hasEveryoneRole := false
|
||||
roleCount := 0
|
||||
|
||||
// Permissions can be assigned to both groups and individual users.
|
||||
// Pre-fetch users with group membership to help us work out
|
||||
// if user belongs to a group with permissions.
|
||||
groupMembers, err := h.Store.Group.GetMembers(ctx)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, perm := range model.Permissions {
|
||||
perm.OrgID = ctx.OrgID
|
||||
perm.SpaceID = id
|
||||
|
||||
isGroup := perm.Who == permission.GroupPermission
|
||||
groupRecords := []group.Record{}
|
||||
|
||||
if isGroup {
|
||||
// get group records for just this group
|
||||
groupRecords = group.FilterGroupRecords(groupMembers, perm.WhoID)
|
||||
}
|
||||
|
||||
// Ensure the space owner always has access!
|
||||
if perm.UserID == ctx.UserID {
|
||||
if (!isGroup && perm.WhoID == ctx.UserID) ||
|
||||
(isGroup && group.UserHasGroupMembership(groupMembers, perm.WhoID, ctx.UserID)) {
|
||||
me = true
|
||||
}
|
||||
|
||||
// Only persist if there is a role!
|
||||
if permission.HasAnyPermission(perm) {
|
||||
// identify publically shared spaces
|
||||
if perm.UserID == "" {
|
||||
perm.UserID = "0"
|
||||
if perm.WhoID == "" {
|
||||
perm.WhoID = user.EveryoneUserID
|
||||
}
|
||||
|
||||
if perm.UserID == "0" {
|
||||
if perm.WhoID == user.EveryoneUserID {
|
||||
hasEveryoneRole = true
|
||||
}
|
||||
|
||||
// Encode group/user permission and save to store.
|
||||
r := permission.EncodeUserPermissions(perm)
|
||||
roleCount++
|
||||
|
||||
for _, p := range r {
|
||||
err = h.Store.Permission.AddPermission(ctx, p)
|
||||
if err != nil {
|
||||
h.Runtime.Log.Error("set permission", err)
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
}
|
||||
}
|
||||
|
||||
// We send out space invitation emails to those users
|
||||
// that have *just* been given permissions.
|
||||
if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting {
|
||||
if _, isExisting := previousRoleUsers[perm.WhoID]; !isExisting {
|
||||
// we skip 'everyone'
|
||||
if perm.WhoID != user.EveryoneUserID {
|
||||
whoToEmail := []string{}
|
||||
|
||||
// we skip 'everyone' (user id != empty string)
|
||||
if perm.UserID != "0" && perm.UserID != "" {
|
||||
existingUser, err := h.Store.User.Get(ctx, perm.UserID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
break
|
||||
if isGroup {
|
||||
// send email to each group member
|
||||
for i := range groupRecords {
|
||||
whoToEmail = append(whoToEmail, groupRecords[i].UserID)
|
||||
}
|
||||
} else {
|
||||
// send email to individual user
|
||||
whoToEmail = append(whoToEmail, perm.WhoID)
|
||||
}
|
||||
|
||||
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
|
||||
go mailer.ShareSpaceExistingUser(existingUser.Email, inviter.Fullname(), url, sp.Name, model.Message)
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, existingUser.Email))
|
||||
for i := range whoToEmail {
|
||||
existingUser, err := h.Store.User.Get(ctx, whoToEmail[i])
|
||||
if err != nil {
|
||||
h.Runtime.Log.Error(method, err)
|
||||
continue
|
||||
}
|
||||
|
||||
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
|
||||
go mailer.ShareSpaceExistingUser(existingUser.Email, inviter.Fullname(), url, sp.Name, model.Message)
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, existingUser.Email))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -178,10 +212,10 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
if !me {
|
||||
perm := permission.Permission{}
|
||||
perm.OrgID = ctx.OrgID
|
||||
perm.Who = "user"
|
||||
perm.Who = permission.UserPermission
|
||||
perm.WhoID = ctx.UserID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "space"
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = id
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
|
@ -232,13 +266,11 @@ func (h *Handler) GetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
perms, err := h.Store.Permission.GetSpacePermissions(ctx, spaceID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
if len(perms) == 0 {
|
||||
perms = []permission.Permission{}
|
||||
}
|
||||
|
||||
userPerms := make(map[string][]permission.Permission)
|
||||
for _, p := range perms {
|
||||
|
@ -250,6 +282,40 @@ func (h *Handler) GetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
records = append(records, permission.DecodeUserPermissions(up))
|
||||
}
|
||||
|
||||
// populate user/group name for thing that has permission record
|
||||
groups, err := h.Store.Group.GetAll(ctx)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range records {
|
||||
if records[i].Who == permission.GroupPermission {
|
||||
for j := range groups {
|
||||
if records[i].WhoID == groups[j].RefID {
|
||||
records[i].Name = groups[j].Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if records[i].Who == permission.UserPermission {
|
||||
if records[i].WhoID == user.EveryoneUserID {
|
||||
records[i].Name = user.EveryoneUserName
|
||||
} else {
|
||||
u, err := h.Store.User.Get(ctx, records[i].WhoID)
|
||||
if err != nil {
|
||||
h.Runtime.Log.Info(fmt.Sprintf("user not found %s", records[i].WhoID))
|
||||
h.Runtime.Log.Error(method, err)
|
||||
continue
|
||||
}
|
||||
|
||||
records[i].Name = u.Fullname()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, records)
|
||||
}
|
||||
|
||||
|
@ -265,13 +331,10 @@ func (h *Handler) GetUserSpacePermissions(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, spaceID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
if len(perms) == 0 {
|
||||
perms = []permission.Permission{}
|
||||
}
|
||||
|
||||
record := permission.DecodeUserPermissions(perms)
|
||||
response.WriteJSON(w, record)
|
||||
|
@ -293,9 +356,6 @@ func (h *Handler) GetCategoryViewers(w http.ResponseWriter, r *http.Request) {
|
|||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
if len(u) == 0 {
|
||||
u = []user.User{}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, u)
|
||||
}
|
||||
|
@ -311,16 +371,58 @@ func (h *Handler) GetCategoryPermissions(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
u, err := h.Store.Permission.GetCategoryPermissions(ctx, categoryID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
perms, err := h.Store.Permission.GetCategoryPermissions(ctx, categoryID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
if len(u) == 0 {
|
||||
u = []permission.Permission{}
|
||||
|
||||
userPerms := make(map[string][]permission.Permission)
|
||||
for _, p := range perms {
|
||||
userPerms[p.WhoID] = append(userPerms[p.WhoID], p)
|
||||
}
|
||||
|
||||
response.WriteJSON(w, u)
|
||||
records := []permission.CategoryRecord{}
|
||||
for _, up := range userPerms {
|
||||
records = append(records, permission.DecodeUserCategoryPermissions(up))
|
||||
}
|
||||
|
||||
// populate user/group name for thing that has permission record
|
||||
groups, err := h.Store.Group.GetAll(ctx)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range records {
|
||||
if records[i].Who == permission.GroupPermission {
|
||||
for j := range groups {
|
||||
if records[i].WhoID == groups[j].RefID {
|
||||
records[i].Name = groups[j].Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if records[i].Who == permission.UserPermission {
|
||||
if records[i].WhoID == user.EveryoneUserID {
|
||||
records[i].Name = user.EveryoneUserName
|
||||
} else {
|
||||
u, err := h.Store.User.Get(ctx, records[i].WhoID)
|
||||
if err != nil {
|
||||
h.Runtime.Log.Info(fmt.Sprintf("user not found %s", records[i].WhoID))
|
||||
h.Runtime.Log.Error(method, err)
|
||||
continue
|
||||
}
|
||||
|
||||
records[i].Name = u.Fullname()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, records)
|
||||
}
|
||||
|
||||
// SetCategoryPermissions persists specified category permissions
|
||||
|
@ -348,7 +450,7 @@ func (h *Handler) SetCategoryPermissions(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
var model = []permission.CategoryViewRequestModel{}
|
||||
var model = []permission.CategoryRecord{}
|
||||
err = json.Unmarshal(body, &model)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
|
@ -358,6 +460,7 @@ func (h *Handler) SetCategoryPermissions(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
if !HasPermission(ctx, *h.Store, spaceID, permission.SpaceManage, permission.SpaceOwner) {
|
||||
response.WriteForbiddenError(w)
|
||||
h.Runtime.Log.Info("no permission to set category permissions")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -380,10 +483,10 @@ func (h *Handler) SetCategoryPermissions(w http.ResponseWriter, r *http.Request)
|
|||
for _, m := range model {
|
||||
perm := permission.Permission{}
|
||||
perm.OrgID = ctx.OrgID
|
||||
perm.Who = "user"
|
||||
perm.WhoID = m.UserID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "category"
|
||||
perm.Who = m.Who
|
||||
perm.WhoID = m.WhoID
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationCategory
|
||||
perm.RefID = m.CategoryID
|
||||
perm.Action = permission.CategoryView
|
||||
|
||||
|
@ -418,9 +521,6 @@ func (h *Handler) GetDocumentPermissions(w http.ResponseWriter, r *http.Request)
|
|||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
if len(perms) == 0 {
|
||||
perms = []permission.Permission{}
|
||||
}
|
||||
|
||||
userPerms := make(map[string][]permission.Permission)
|
||||
for _, p := range perms {
|
||||
|
@ -451,9 +551,6 @@ func (h *Handler) GetUserDocumentPermissions(w http.ResponseWriter, r *http.Requ
|
|||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
}
|
||||
if len(perms) == 0 {
|
||||
perms = []permission.Permission{}
|
||||
}
|
||||
|
||||
record := permission.DecodeUserDocumentPermissions(perms)
|
||||
response.WriteJSON(w, record)
|
||||
|
@ -483,11 +580,6 @@ func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
// if !HasPermission(ctx, *h.Store, doc.LabelID, permission.SpaceManage, permission.SpaceOwner) {
|
||||
// response.WriteForbiddenError(w)
|
||||
// return
|
||||
// }
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
|
@ -547,17 +639,37 @@ func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s/d/%s/%s",
|
||||
sp.RefID, stringutil.MakeSlug(sp.Name), doc.RefID, stringutil.MakeSlug(doc.Title)))
|
||||
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s/d/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name), doc.RefID, stringutil.MakeSlug(doc.Title)))
|
||||
|
||||
// Permissions can be assigned to both groups and individual users.
|
||||
// Pre-fetch users with group membership to help us work out
|
||||
// if user belongs to a group with permissions.
|
||||
groupMembers, err := h.Store.Group.GetMembers(ctx)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, perm := range model {
|
||||
perm.OrgID = ctx.OrgID
|
||||
perm.DocumentID = id
|
||||
|
||||
// get group records for just this group
|
||||
isGroup := perm.Who == permission.GroupPermission
|
||||
groupRecords := []group.Record{}
|
||||
if isGroup {
|
||||
groupRecords = group.FilterGroupRecords(groupMembers, perm.WhoID)
|
||||
}
|
||||
|
||||
// Only persist if there is a role!
|
||||
if permission.HasAnyDocumentPermission(perm) {
|
||||
r := permission.EncodeUserDocumentPermissions(perm)
|
||||
if perm.WhoID == "" {
|
||||
perm.WhoID = user.EveryoneUserID
|
||||
}
|
||||
|
||||
r := permission.EncodeUserDocumentPermissions(perm)
|
||||
for _, p := range r {
|
||||
err = h.Store.Permission.AddPermission(ctx, p)
|
||||
if err != nil {
|
||||
|
@ -566,19 +678,32 @@ func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
|
||||
// Send email notification to users who have been given document approver role
|
||||
if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting {
|
||||
if _, isExisting := previousRoleUsers[perm.WhoID]; !isExisting {
|
||||
// we skip 'everyone' as it has no email address!
|
||||
if perm.WhoID != user.EveryoneUserID && perm.DocumentRoleApprove {
|
||||
whoToEmail := []string{}
|
||||
|
||||
// we skip 'everyone' (user id != empty string)
|
||||
if perm.UserID != "0" && perm.UserID != "" && perm.DocumentRoleApprove {
|
||||
existingUser, err := h.Store.User.Get(ctx, perm.UserID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
break
|
||||
if isGroup {
|
||||
// send email to each group member
|
||||
for i := range groupRecords {
|
||||
whoToEmail = append(whoToEmail, groupRecords[i].UserID)
|
||||
}
|
||||
} else {
|
||||
// send email to individual user
|
||||
whoToEmail = append(whoToEmail, perm.WhoID)
|
||||
}
|
||||
|
||||
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
|
||||
go mailer.DocumentApprover(existingUser.Email, inviter.Fullname(), url, doc.Title)
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s has made %s document approver for: %s", inviter.Email, existingUser.Email, doc.Title))
|
||||
for i := range whoToEmail {
|
||||
existingUser, err := h.Store.User.Get(ctx, whoToEmail[i])
|
||||
if err != nil {
|
||||
h.Runtime.Log.Error(method, err)
|
||||
continue
|
||||
}
|
||||
|
||||
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
|
||||
go mailer.DocumentApprover(existingUser.Email, inviter.Fullname(), url, doc.Title)
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s has made %s document approver for: %s", inviter.Email, existingUser.Email, doc.Title))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func (s Scope) AddPermission(ctx domain.RequestContext, r permission.Permission)
|
|||
r.Created = time.Now().UTC()
|
||||
|
||||
_, err = ctx.Transaction.Exec("INSERT INTO permission (orgid, who, whoid, action, scope, location, refid, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
r.OrgID, r.Who, r.WhoID, string(r.Action), r.Scope, r.Location, r.RefID, r.Created)
|
||||
r.OrgID, string(r.Who), r.WhoID, string(r.Action), string(r.Scope), string(r.Location), r.RefID, r.Created)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to execute insert permission")
|
||||
|
@ -48,26 +48,34 @@ func (s Scope) AddPermission(ctx domain.RequestContext, r permission.Permission)
|
|||
func (s Scope) AddPermissions(ctx domain.RequestContext, r permission.Permission, actions ...permission.Action) (err error) {
|
||||
for _, a := range actions {
|
||||
r.Action = a
|
||||
s.AddPermission(ctx, r)
|
||||
|
||||
err := s.AddPermission(ctx, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserSpacePermissions returns space permissions for user.
|
||||
// Context is used to for user ID.
|
||||
// Context is used to for userID because must match by userID
|
||||
// or everyone ID of 0.
|
||||
func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='space' AND refid=? AND who='user' AND (whoid=? OR whoid='0')
|
||||
FROM permission
|
||||
WHERE orgid=? AND location='space' AND refid=? AND who='user' AND (whoid=? OR whoid='0')
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='space' AND refid=?
|
||||
AND p.who='role' AND (r.userid=? OR r.userid='0')`,
|
||||
FROM permission p
|
||||
LEFT JOIN rolemember r ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.location='space' AND refid=? AND p.who='role' AND (r.userid=? OR r.userid='0')`,
|
||||
ctx.OrgID, spaceID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
r = []permission.Permission{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select user permissions %s", ctx.UserID))
|
||||
|
@ -77,18 +85,21 @@ func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string
|
|||
}
|
||||
|
||||
// GetSpacePermissions returns space permissions for all users.
|
||||
// We do not filter by userID because we return permissions for all users.
|
||||
func (s Scope) GetSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='space' AND refid=? AND who='user'
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='space' AND p.refid=?
|
||||
AND p.who='role'`,
|
||||
FROM permission p
|
||||
LEFT JOIN rolemember r ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.location='space' AND p.refid=? AND p.who='role'`,
|
||||
ctx.OrgID, spaceID, ctx.OrgID, spaceID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
r = []permission.Permission{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select space permissions %s", ctx.UserID))
|
||||
|
@ -97,6 +108,138 @@ func (s Scope) GetSpacePermissions(ctx domain.RequestContext, spaceID string) (r
|
|||
return
|
||||
}
|
||||
|
||||
// GetCategoryPermissions returns category permissions for all users.
|
||||
func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission
|
||||
WHERE orgid=? AND location='category' AND who='user' AND (refid=? OR refid='0')
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p
|
||||
LEFT JOIN rolemember r ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.location='category' AND p.who='role' AND (p.refid=? OR p.refid='0')`,
|
||||
ctx.OrgID, catID, ctx.OrgID, catID)
|
||||
|
||||
if err == sql.ErrNoRows || len(r) == 0 {
|
||||
err = nil
|
||||
r = []permission.Permission{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions %s", catID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetCategoryUsers returns space permissions for all users.
|
||||
func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []user.User, err error) {
|
||||
err = s.Runtime.Db.Select(&u, `
|
||||
SELECT u.id, IFNULL(u.refid, '') AS refid, IFNULL(u.firstname, '') AS firstname, IFNULL(u.lastname, '') as lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
|
||||
FROM user u LEFT JOIN account a ON u.refid = a.userid
|
||||
WHERE a.orgid=? AND a.active=1 AND u.refid IN (
|
||||
SELECT whoid from permission
|
||||
WHERE orgid=? AND who='user' AND location='category' AND refid=?
|
||||
UNION ALL
|
||||
SELECT r.userid from rolemember r
|
||||
LEFT JOIN permission p ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.who='role' AND p.location='category' AND p.refid=?
|
||||
)
|
||||
GROUP by u.id
|
||||
ORDER BY firstname, lastname`,
|
||||
ctx.OrgID, ctx.OrgID, catID, ctx.OrgID, catID)
|
||||
|
||||
if err == sql.ErrNoRows || len(u) == 0 {
|
||||
err = nil
|
||||
u = []user.User{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select users for category %s", catID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserCategoryPermissions returns category permissions for given user.
|
||||
func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='category' AND who='user' AND (whoid=? OR whoid='0')
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p
|
||||
LEFT JOIN rolemember r ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.location='category' AND p.who='role' AND (r.userid=? OR r.userid='0')`,
|
||||
ctx.OrgID, userID, ctx.OrgID, userID)
|
||||
|
||||
if err == sql.ErrNoRows || len(r) == 0 {
|
||||
err = nil
|
||||
r = []permission.Permission{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions for user %s", userID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserDocumentPermissions returns document permissions for user.
|
||||
// Context is used to for user ID.
|
||||
func (s Scope) GetUserDocumentPermissions(ctx domain.RequestContext, documentID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='document' AND refid=? AND who='user' AND (whoid=? OR whoid='0')
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p
|
||||
LEFT JOIN rolemember r ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.location='document' AND refid=? AND p.who='role' AND (r.userid=? OR r.userid='0')`,
|
||||
ctx.OrgID, documentID, ctx.UserID, ctx.OrgID, documentID, ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
r = []permission.Permission{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select user document permissions %s", ctx.UserID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetDocumentPermissions returns documents permissions for all users.
|
||||
// We do not filter by userID because we return permissions for all users.
|
||||
func (s Scope) GetDocumentPermissions(ctx domain.RequestContext, documentID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='document' AND refid=? AND who='user'
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p
|
||||
LEFT JOIN rolemember r ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.location='document' AND p.refid=? AND p.who='role'`,
|
||||
ctx.OrgID, documentID, ctx.OrgID, documentID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
r = []permission.Permission{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select document permissions %s", ctx.UserID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteDocumentPermissions removes records from permissions table for given document.
|
||||
func (s Scope) DeleteDocumentPermissions(ctx domain.RequestContext, documentID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
|
||||
sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='document' AND refid='%s'", ctx.OrgID, documentID)
|
||||
|
||||
return b.DeleteWhere(ctx.Transaction, sql)
|
||||
}
|
||||
|
||||
// DeleteSpacePermissions removes records from permissions table for given space ID.
|
||||
func (s Scope) DeleteSpacePermissions(ctx domain.RequestContext, spaceID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
|
@ -146,121 +289,3 @@ func (s Scope) DeleteSpaceCategoryPermissions(ctx domain.RequestContext, spaceID
|
|||
|
||||
return b.DeleteWhere(ctx.Transaction, sql)
|
||||
}
|
||||
|
||||
// GetCategoryPermissions returns category permissions for all users.
|
||||
func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='category' AND refid=? AND who='user'
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='space' AND p.refid=?
|
||||
AND p.who='role'`,
|
||||
ctx.OrgID, catID, ctx.OrgID, catID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions %s", catID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetCategoryUsers returns space permissions for all users.
|
||||
func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []user.User, err error) {
|
||||
err = s.Runtime.Db.Select(&u, `
|
||||
SELECT u.id, IFNULL(u.refid, '') AS refid, IFNULL(u.firstname, '') AS firstname, IFNULL(u.lastname, '') as lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
|
||||
FROM user u LEFT JOIN account a ON u.refid = a.userid
|
||||
WHERE a.orgid=? AND a.active=1 AND u.refid IN (
|
||||
SELECT whoid from permission WHERE orgid=? AND who='user' AND location='category' AND refid=? UNION ALL
|
||||
SELECT r.userid from rolemember r LEFT JOIN permission p ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role'
|
||||
AND p.location='category' AND p.refid=?
|
||||
)
|
||||
GROUP by u.id
|
||||
ORDER BY firstname, lastname`,
|
||||
ctx.OrgID, ctx.OrgID, catID, ctx.OrgID, catID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select users for category %s", catID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserCategoryPermissions returns category permissions for given user.
|
||||
func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='category' AND who='user' AND (whoid=? OR whoid='0')
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid
|
||||
WHERE p.orgid=? AND p.location='category' AND p.who='role'`,
|
||||
ctx.OrgID, userID, ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions for user %s", userID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserDocumentPermissions returns document permissions for user.
|
||||
// Context is used to for user ID.
|
||||
func (s Scope) GetUserDocumentPermissions(ctx domain.RequestContext, documentID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='document' AND refid=? AND who='user' AND (whoid=? OR whoid='0')
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='document' AND refid=?
|
||||
AND p.who='role' AND (r.userid=? OR r.userid='0')`,
|
||||
ctx.OrgID, documentID, ctx.UserID, ctx.OrgID, documentID, ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select user document permissions %s", ctx.UserID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetDocumentPermissions returns documents permissions for all users.
|
||||
func (s Scope) GetDocumentPermissions(ctx domain.RequestContext, documentID string) (r []permission.Permission, err error) {
|
||||
err = s.Runtime.Db.Select(&r, `
|
||||
SELECT id, orgid, who, whoid, action, scope, location, refid
|
||||
FROM permission WHERE orgid=? AND location='document' AND refid=? AND who='user'
|
||||
UNION ALL
|
||||
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
|
||||
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='document' AND p.refid=?
|
||||
AND p.who='role'`,
|
||||
ctx.OrgID, documentID, ctx.OrgID, documentID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute select document permissions %s", ctx.UserID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteDocumentPermissions removes records from permissions table for given document.
|
||||
func (s Scope) DeleteDocumentPermissions(ctx domain.RequestContext, documentID string) (rows int64, err error) {
|
||||
b := mysql.BaseQuery{}
|
||||
|
||||
sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='document' AND refid='%s'", ctx.OrgID, documentID)
|
||||
|
||||
return b.DeleteWhere(ctx.Transaction, sql)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"database/sql"
|
||||
|
||||
"github.com/documize/community/domain"
|
||||
group "github.com/documize/community/model/group"
|
||||
pm "github.com/documize/community/model/permission"
|
||||
u "github.com/documize/community/model/user"
|
||||
)
|
||||
|
@ -30,7 +31,7 @@ func CanViewSpaceDocument(ctx domain.RequestContext, s domain.Store, labelID str
|
|||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.RefID == labelID && role.Location == "space" && role.Scope == "object" &&
|
||||
if role.RefID == labelID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
|
||||
pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
|
||||
return true
|
||||
}
|
||||
|
@ -58,7 +59,7 @@ func CanViewDocument(ctx domain.RequestContext, s domain.Store, documentID strin
|
|||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" &&
|
||||
if role.RefID == document.LabelID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
|
||||
pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
|
||||
return true
|
||||
}
|
||||
|
@ -88,7 +89,7 @@ func CanChangeDocument(ctx domain.RequestContext, s domain.Store, documentID str
|
|||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && role.Action == pm.DocumentEdit {
|
||||
if role.RefID == document.LabelID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow && role.Action == pm.DocumentEdit {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +137,7 @@ func CanUploadDocument(ctx domain.RequestContext, s domain.Store, spaceID string
|
|||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" &&
|
||||
if role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
|
||||
pm.ContainsPermission(role.Action, pm.DocumentAdd) {
|
||||
return true
|
||||
}
|
||||
|
@ -155,7 +156,7 @@ func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) boo
|
|||
return false
|
||||
}
|
||||
for _, role := range roles {
|
||||
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" &&
|
||||
if role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
|
||||
pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
|
||||
return true
|
||||
}
|
||||
|
@ -176,7 +177,7 @@ func HasPermission(ctx domain.RequestContext, s domain.Store, spaceID string, ac
|
|||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" {
|
||||
if role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow {
|
||||
for _, a := range actions {
|
||||
if role.Action == a {
|
||||
return true
|
||||
|
@ -193,34 +194,63 @@ func GetDocumentApprovers(ctx domain.RequestContext, s domain.Store, spaceID, do
|
|||
users = []u.User{}
|
||||
prev := make(map[string]bool) // used to ensure we only process user once
|
||||
|
||||
// check space permissions
|
||||
sp, err := s.Permission.GetSpacePermissions(ctx, spaceID)
|
||||
for _, p := range sp {
|
||||
if p.Action == pm.DocumentApprove {
|
||||
user, err := s.User.Get(ctx, p.WhoID)
|
||||
if err == nil {
|
||||
prev[user.RefID] = true
|
||||
users = append(users, user)
|
||||
} else {
|
||||
return users, err
|
||||
}
|
||||
}
|
||||
// Permissions can be assigned to both groups and individual users.
|
||||
// Pre-fetch users with group membership to help us work out
|
||||
// if user belongs to a group with permissions.
|
||||
groupMembers, err := s.Group.GetMembers(ctx)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
||||
// check document permissions
|
||||
// space permissions
|
||||
sp, err := s.Permission.GetSpacePermissions(ctx, spaceID)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
// document permissions
|
||||
dp, err := s.Permission.GetDocumentPermissions(ctx, documentID)
|
||||
for _, p := range dp {
|
||||
if p.Action == pm.DocumentApprove {
|
||||
user, err := s.User.Get(ctx, p.WhoID)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
||||
// all permissions
|
||||
all := sp
|
||||
all = append(all, dp...)
|
||||
|
||||
for _, p := range all {
|
||||
// only approvers
|
||||
if p.Action != pm.DocumentApprove {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Who == pm.GroupPermission {
|
||||
// get group records for just this group
|
||||
groupRecords := group.FilterGroupRecords(groupMembers, p.WhoID)
|
||||
|
||||
for i := range groupRecords {
|
||||
user, err := s.User.Get(ctx, groupRecords[i].UserID)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
if _, isExisting := prev[user.RefID]; !isExisting {
|
||||
users = append(users, user)
|
||||
prev[user.RefID] = true
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
if p.Who == pm.UserPermission {
|
||||
user, err := s.User.Get(ctx, p.WhoID)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
||||
if _, isExisting := prev[user.RefID]; !isExisting {
|
||||
users = append(users, user)
|
||||
prev[user.RefID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return users, err
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ package setting
|
|||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
|
@ -22,6 +23,7 @@ import (
|
|||
"github.com/documize/community/core/event"
|
||||
"github.com/documize/community/core/response"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/smtp"
|
||||
"github.com/documize/community/model/audit"
|
||||
)
|
||||
|
||||
|
@ -91,7 +93,37 @@ func (h *Handler) SetSMTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSystemSMTP)
|
||||
|
||||
response.WriteEmpty(w)
|
||||
// test connection
|
||||
var result struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
result.Message = "Email sent successfully!"
|
||||
|
||||
u, err := h.Store.User.Get(ctx, ctx.UserID)
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Message = err.Error()
|
||||
h.Runtime.Log.Error(method, err)
|
||||
response.WriteJSON(w, result)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := GetSMTPConfig(h.Store)
|
||||
dialer, err := smtp.Connect(cfg)
|
||||
em := smtp.EmailMessage{}
|
||||
em.Subject = "Documize SMTP Test"
|
||||
em.BodyHTML = "<p>This is a test email from Documize using current SMTP settings.</p>"
|
||||
em.ToEmail = u.Email
|
||||
em.ToName = u.Fullname()
|
||||
|
||||
result.Success, err = smtp.SendMessage(dialer, cfg, em)
|
||||
if !result.Success {
|
||||
result.Message = fmt.Sprintf("Unable to send test email: %s", err.Error())
|
||||
}
|
||||
|
||||
response.WriteJSON(w, result)
|
||||
}
|
||||
|
||||
// License returns product license
|
||||
|
|
60
domain/setting/smtp.go
Normal file
60
domain/setting/smtp.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
// 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 setting manages both global and user level settings
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain/smtp"
|
||||
)
|
||||
|
||||
// GetSMTPConfig returns SMTP configuration.
|
||||
func GetSMTPConfig(s *domain.Store) (c smtp.Config) {
|
||||
c = smtp.Config{}
|
||||
|
||||
// server
|
||||
c.Host, _ = s.Setting.Get("SMTP", "host")
|
||||
port, _ := s.Setting.Get("SMTP", "port")
|
||||
c.Port, _ = strconv.Atoi(port)
|
||||
|
||||
// credentials
|
||||
c.Username, _ = s.Setting.Get("SMTP", "userid")
|
||||
c.Password, _ = s.Setting.Get("SMTP", "password")
|
||||
|
||||
// sender
|
||||
c.SenderEmail, _ = s.Setting.Get("SMTP", "sender")
|
||||
c.SenderName, _ = s.Setting.Get("SMTP", "senderName")
|
||||
if c.SenderName == "" {
|
||||
c.SenderName = "Documize"
|
||||
}
|
||||
|
||||
// anon auth?
|
||||
anon, _ := s.Setting.Get("SMTP", "anonymous")
|
||||
c.AnonymousAuth, _ = strconv.ParseBool(anon)
|
||||
|
||||
// base64 encode creds?
|
||||
b64, _ := s.Setting.Get("SMTP", "base64creds")
|
||||
c.Base64EncodeCredentials, _ = strconv.ParseBool(b64)
|
||||
|
||||
// SSL?
|
||||
ssl, _ := s.Setting.Get("SMTP", "usessl")
|
||||
c.UseSSL, _ = strconv.ParseBool(ssl)
|
||||
|
||||
// verify SSL?
|
||||
verifySSL, _ := s.Setting.Get("SMTP", "verifyssl")
|
||||
c.SkipSSLVerify, _ = strconv.ParseBool(verifySSL)
|
||||
c.SkipSSLVerify = true
|
||||
|
||||
return
|
||||
}
|
114
domain/smtp/send.go
Normal file
114
domain/smtp/send.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
// 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 smtp provides access to SMTP server for sending email.
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/documize/community/core/mail"
|
||||
)
|
||||
|
||||
// Config is used to create SMTP server connection
|
||||
type Config struct {
|
||||
// IP/hostname of SMTP server
|
||||
Host string
|
||||
|
||||
// Port number of SMTP server
|
||||
Port int
|
||||
|
||||
// Username for authentication with SMTP server
|
||||
Username string
|
||||
|
||||
// Password for authentication with SMTP server
|
||||
Password string
|
||||
|
||||
// SenderEmail is FROM address
|
||||
SenderEmail string
|
||||
|
||||
// SenderName is FROM display name
|
||||
SenderName string
|
||||
|
||||
// AnonymousAuth does not send username/password to server
|
||||
AnonymousAuth bool
|
||||
|
||||
// Base64EncodeCredentials encodes User and Password as base64 before sending to SMTP server
|
||||
Base64EncodeCredentials bool
|
||||
|
||||
// UseSSL uses SMTP SSL connection with SMTP server
|
||||
UseSSL bool
|
||||
|
||||
// SkipSSLVerify allows unverified certificates
|
||||
SkipSSLVerify bool
|
||||
}
|
||||
|
||||
// Connect returns open connection to server for sending email
|
||||
func Connect(c Config) (d *mail.Dialer, err error) {
|
||||
// prepare credentials
|
||||
u := strings.TrimSpace(c.Username)
|
||||
p := strings.TrimSpace(c.Password)
|
||||
|
||||
// anonymous, no credentials
|
||||
if c.AnonymousAuth {
|
||||
u = ""
|
||||
p = ""
|
||||
}
|
||||
|
||||
// base64 encode if required
|
||||
if c.Base64EncodeCredentials {
|
||||
u = base64.StdEncoding.EncodeToString([]byte(u))
|
||||
p = base64.StdEncoding.EncodeToString([]byte(p))
|
||||
}
|
||||
|
||||
// basic server
|
||||
d = mail.NewDialer(c.Host, c.Port, u, p)
|
||||
|
||||
// use SSL
|
||||
d.SSL = c.UseSSL
|
||||
|
||||
// verify SSL cert chain
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: c.SkipSSLVerify}
|
||||
|
||||
// TLS mode
|
||||
d.StartTLSPolicy = mail.OpportunisticStartTLS
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// EmailMessage represents email to be sent.
|
||||
type EmailMessage struct {
|
||||
ToEmail string
|
||||
ToName string
|
||||
Subject string
|
||||
BodyHTML string
|
||||
}
|
||||
|
||||
// SendMessage sends email using specified SMTP connection
|
||||
func SendMessage(d *mail.Dialer, c Config, em EmailMessage) (b bool, err error) {
|
||||
m := mail.NewMessage()
|
||||
|
||||
// participants
|
||||
m.SetHeader("From", m.FormatAddress(c.SenderEmail, c.SenderName))
|
||||
m.SetHeader("To", m.FormatAddress(em.ToEmail, em.ToName))
|
||||
|
||||
// content
|
||||
m.SetHeader("Subject", em.Subject)
|
||||
m.SetBody("text/html", em.BodyHTML)
|
||||
|
||||
// send email
|
||||
if err = d.DialAndSend(m); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
|
@ -110,10 +110,10 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
perm := permission.Permission{}
|
||||
perm.OrgID = sp.OrgID
|
||||
perm.Who = "user"
|
||||
perm.Who = permission.UserPermission
|
||||
perm.WhoID = ctx.UserID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "space"
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = sp.RefID
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
|
@ -800,10 +800,10 @@ func (h *Handler) Invite(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
perm := permission.Permission{}
|
||||
perm.OrgID = sp.OrgID
|
||||
perm.Who = "user"
|
||||
perm.Who = permission.UserPermission
|
||||
perm.WhoID = u.RefID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "space"
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = sp.RefID
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
|
|
|
@ -64,10 +64,10 @@ func inviteNewUserToSharedSpace(ctx domain.RequestContext, rt *env.Runtime, s *d
|
|||
|
||||
perm := permission.Permission{}
|
||||
perm.OrgID = sp.OrgID
|
||||
perm.Who = "user"
|
||||
perm.Who = permission.UserPermission
|
||||
perm.WhoID = userID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "space"
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = sp.RefID
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
|
|
|
@ -42,8 +42,8 @@ func TestSpace(t *testing.T) {
|
|||
perm.OrgID = ctx.OrgID
|
||||
perm.Who = "user"
|
||||
perm.WhoID = ctx.UserID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "space"
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = spaceID
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
|
@ -109,8 +109,8 @@ func TestSpace(t *testing.T) {
|
|||
perm.OrgID = ctx.OrgID
|
||||
perm.Who = "user"
|
||||
perm.WhoID = ctx.UserID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "space"
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = spaceID2
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
|
@ -170,8 +170,8 @@ func TestSpace(t *testing.T) {
|
|||
perm.OrgID = ctx.OrgID
|
||||
perm.Who = "user"
|
||||
perm.WhoID = ctx.UserID
|
||||
perm.Scope = "object"
|
||||
perm.Location = "space"
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = spaceID
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/documize/community/model/block"
|
||||
"github.com/documize/community/model/category"
|
||||
"github.com/documize/community/model/doc"
|
||||
"github.com/documize/community/model/group"
|
||||
"github.com/documize/community/model/link"
|
||||
"github.com/documize/community/model/org"
|
||||
"github.com/documize/community/model/page"
|
||||
|
@ -39,6 +40,7 @@ type Store struct {
|
|||
Block BlockStorer
|
||||
Category CategoryStorer
|
||||
Document DocumentStorer
|
||||
Group GroupStorer
|
||||
Link LinkStorer
|
||||
Organization OrganizationStorer
|
||||
Page PageStorer
|
||||
|
@ -108,7 +110,7 @@ type UserStorer interface {
|
|||
GetByToken(ctx RequestContext, token string) (u user.User, err error)
|
||||
GetBySerial(ctx RequestContext, serial string) (u user.User, err error)
|
||||
GetActiveUsersForOrganization(ctx RequestContext) (u []user.User, err error)
|
||||
GetUsersForOrganization(ctx RequestContext) (u []user.User, err error)
|
||||
GetUsersForOrganization(ctx RequestContext, filter string) (u []user.User, err error)
|
||||
GetSpaceUsers(ctx RequestContext, spaceID string) (u []user.User, err error)
|
||||
GetUsersForSpaces(ctx RequestContext, spaces []string) (u []user.User, err error)
|
||||
UpdateUser(ctx RequestContext, u user.User) (err error)
|
||||
|
@ -116,6 +118,7 @@ type UserStorer interface {
|
|||
DeactiveUser(ctx RequestContext, userID string) (err error)
|
||||
ForgotUserPassword(ctx RequestContext, email, token string) (err error)
|
||||
CountActiveUsers() (c int)
|
||||
MatchUsers(ctx RequestContext, text string, maxMatches int) (u []user.User, err error)
|
||||
}
|
||||
|
||||
// AccountStorer defines required methods for account management
|
||||
|
@ -265,3 +268,16 @@ type PageStorer interface {
|
|||
GetDocumentRevisions(ctx RequestContext, documentID string) (revisions []page.Revision, err error)
|
||||
DeletePageRevisions(ctx RequestContext, pageID string) (rows int64, err error)
|
||||
}
|
||||
|
||||
// GroupStorer defines required methods for persisting user groups and memberships
|
||||
type GroupStorer interface {
|
||||
Add(ctx RequestContext, g group.Group) (err error)
|
||||
Get(ctx RequestContext, refID string) (g group.Group, err error)
|
||||
GetAll(ctx RequestContext) (g []group.Group, err error)
|
||||
Update(ctx RequestContext, g group.Group) (err error)
|
||||
Delete(ctx RequestContext, refID string) (rows int64, err error)
|
||||
GetGroupMembers(ctx RequestContext, groupID string) (m []group.Member, err error)
|
||||
GetMembers(ctx RequestContext) (r []group.Record, err error)
|
||||
JoinGroup(ctx RequestContext, groupID, userID string) (err error)
|
||||
LeaveGroup(ctx RequestContext, groupID, userID string) (err error)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ package user
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -34,6 +35,7 @@ import (
|
|||
"github.com/documize/community/domain/organization"
|
||||
"github.com/documize/community/model/account"
|
||||
"github.com/documize/community/model/audit"
|
||||
"github.com/documize/community/model/group"
|
||||
"github.com/documize/community/model/user"
|
||||
)
|
||||
|
||||
|
@ -207,7 +209,6 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
|||
go mailer.InviteNewUser(userModel.Email, inviter.Fullname(), url, userModel.Email, requestedPassword)
|
||||
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s invited by %s on %s", userModel.Email, inviter.Email, ctx.AppURL))
|
||||
|
||||
} else {
|
||||
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
|
||||
go mailer.InviteExistingUser(userModel.Email, inviter.Fullname(), ctx.GetAppURL(""))
|
||||
|
@ -228,6 +229,8 @@ func (h *Handler) GetOrganizationUsers(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
filter := request.Query(r, "filter")
|
||||
|
||||
active, err := strconv.ParseBool(request.Query(r, "active"))
|
||||
if err != nil {
|
||||
active = false
|
||||
|
@ -243,20 +246,33 @@ func (h *Handler) GetOrganizationUsers(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
} else {
|
||||
u, err = h.Store.User.GetUsersForOrganization(ctx)
|
||||
u, err = h.Store.User.GetUsersForOrganization(ctx, filter)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(u) == 0 {
|
||||
u = []user.User{}
|
||||
// prefetch all group membership records
|
||||
groups, err := h.Store.Group.GetMembers(ctx)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
// for each user...
|
||||
for i := range u {
|
||||
// 1. attach user accounts
|
||||
AttachUserAccounts(ctx, *h.Store, ctx.OrgID, &u[i])
|
||||
|
||||
// 2. attach user groups
|
||||
u[i].Groups = []group.Record{}
|
||||
for j := range groups {
|
||||
if groups[j].UserID == u[i].RefID {
|
||||
u[i].Groups = append(u[i].Groups, groups[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, u)
|
||||
|
@ -594,7 +610,7 @@ func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// ResetPassword stores the newly chosen password for the user.
|
||||
func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
method := "user.ForgotUserPassword"
|
||||
method := "user.ResetPassword"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
ctx.Subdomain = organization.GetSubdomainFromHost(r)
|
||||
|
||||
|
@ -644,3 +660,191 @@ func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
||||
// MatchUsers returns users where provided text
|
||||
// matches firstname, lastname, email
|
||||
func (h *Handler) MatchUsers(w http.ResponseWriter, r *http.Request) {
|
||||
method := "user.MatchUsers"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "text")
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
searchText := string(body)
|
||||
|
||||
u, err := h.Store.User.MatchUsers(ctx, searchText, 100)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, u)
|
||||
}
|
||||
|
||||
// BulkImport imports comma-delimited list of users:
|
||||
// firstname, lastname, email
|
||||
func (h *Handler) BulkImport(w http.ResponseWriter, r *http.Request) {
|
||||
method := "user.BulkImport"
|
||||
ctx := domain.GetRequestContext(r)
|
||||
|
||||
if !ctx.Administrator {
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
defer streamutil.Close(r.Body)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
response.WriteBadRequestError(w, method, "text")
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
usersList := string(body)
|
||||
|
||||
cr := csv.NewReader(strings.NewReader(usersList))
|
||||
cr.TrimLeadingSpace = true
|
||||
cr.FieldsPerRecord = 3
|
||||
|
||||
records, err := cr.ReadAll()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
inviter, err := h.Store.User.Get(ctx, ctx.UserID)
|
||||
if err != nil {
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range records {
|
||||
userModel := user.User{}
|
||||
userModel.Firstname = strings.TrimSpace(v[0])
|
||||
userModel.Lastname = strings.TrimSpace(v[1])
|
||||
userModel.Email = strings.ToLower(strings.TrimSpace(v[2]))
|
||||
|
||||
if len(userModel.Email) == 0 || len(userModel.Firstname) == 0 || len(userModel.Lastname) == 0 {
|
||||
h.Runtime.Log.Info(method + " missing firstname, lastname, or email")
|
||||
continue
|
||||
}
|
||||
|
||||
userModel.Initials = stringutil.MakeInitials(userModel.Firstname, userModel.Lastname)
|
||||
requestedPassword := secrets.GenerateRandomPassword()
|
||||
userModel.Salt = secrets.GenerateSalt()
|
||||
userModel.Password = secrets.GeneratePassword(requestedPassword, userModel.Salt)
|
||||
|
||||
// only create account if not dupe
|
||||
addUser := true
|
||||
addAccount := true
|
||||
var userID string
|
||||
|
||||
userDupe, err := h.Store.User.GetByEmail(ctx, userModel.Email)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
h.Runtime.Log.Error(method, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if userModel.Email == userDupe.Email {
|
||||
addUser = false
|
||||
userID = userDupe.RefID
|
||||
h.Runtime.Log.Info("Dupe user found, will not add " + userModel.Email)
|
||||
}
|
||||
|
||||
if addUser {
|
||||
userID = uniqueid.Generate()
|
||||
userModel.RefID = userID
|
||||
|
||||
err = h.Store.User.Add(ctx, userModel)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.Runtime.Log.Info("Adding user")
|
||||
} else {
|
||||
AttachUserAccounts(ctx, *h.Store, ctx.OrgID, &userDupe)
|
||||
|
||||
for _, a := range userDupe.Accounts {
|
||||
if a.OrgID == ctx.OrgID {
|
||||
addAccount = false
|
||||
h.Runtime.Log.Info("Dupe account found, will not add")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set up user account for the org
|
||||
if addAccount {
|
||||
var a account.Account
|
||||
a.RefID = uniqueid.Generate()
|
||||
a.UserID = userID
|
||||
a.OrgID = ctx.OrgID
|
||||
a.Editor = true
|
||||
a.Admin = false
|
||||
a.Active = true
|
||||
|
||||
err = h.Store.Account.Add(ctx, a)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if addUser {
|
||||
event.Handler().Publish(string(event.TypeAddUser))
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeUserAdd)
|
||||
}
|
||||
|
||||
if addAccount {
|
||||
event.Handler().Publish(string(event.TypeAddAccount))
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeAccountAdd)
|
||||
}
|
||||
|
||||
// If we did not add user or give them access (account) then we error back
|
||||
if !addUser && !addAccount {
|
||||
h.Runtime.Log.Info(method + " duplicate user not added")
|
||||
continue
|
||||
}
|
||||
|
||||
// Invite new user and prepare invitation email (that contains SSO link)
|
||||
if addUser && addAccount {
|
||||
size := len(requestedPassword)
|
||||
|
||||
auth := fmt.Sprintf("%s:%s:%s", ctx.AppURL, userModel.Email, requestedPassword[:size])
|
||||
encrypted := secrets.EncodeBase64([]byte(auth))
|
||||
|
||||
url := fmt.Sprintf("%s/%s", ctx.GetAppURL("auth/sso"), url.QueryEscape(string(encrypted)))
|
||||
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
|
||||
go mailer.InviteNewUser(userModel.Email, inviter.Fullname(), url, userModel.Email, requestedPassword)
|
||||
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s invited by %s on %s", userModel.Email, inviter.Email, ctx.AppURL))
|
||||
} else {
|
||||
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
|
||||
go mailer.InviteExistingUser(userModel.Email, inviter.Fullname(), ctx.GetAppURL(""))
|
||||
|
||||
h.Runtime.Log.Info(fmt.Sprintf("%s is giving access to an existing user %s", inviter.Email, userModel.Email))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
response.WriteEmpty(w)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ package mysql
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -117,6 +118,10 @@ func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []use
|
|||
ORDER BY u.firstname,u.lastname`,
|
||||
ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows || len(u) == 0 {
|
||||
err = nil
|
||||
u = []user.User{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", ctx.OrgID))
|
||||
}
|
||||
|
@ -126,13 +131,24 @@ func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []use
|
|||
|
||||
// GetUsersForOrganization returns a slice containing all of the user records for the organizaiton
|
||||
// identified in the Persister.
|
||||
func (s Scope) GetUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) {
|
||||
func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string) (u []user.User, err error) {
|
||||
filter = strings.TrimSpace(strings.ToLower(filter))
|
||||
likeQuery := ""
|
||||
if len(filter) > 0 {
|
||||
likeQuery = " AND (LOWER(u.firstname) LIKE '%" + filter + "%' OR LOWER(u.lastname) LIKE '%" + filter + "%' OR LOWER(u.email) LIKE '%" + filter + "%') "
|
||||
}
|
||||
|
||||
err = s.Runtime.Db.Select(&u,
|
||||
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised,
|
||||
u.global, a.active, a.editor, a.admin, a.users as viewusers
|
||||
FROM user u, account a
|
||||
WHERE u.refid=a.userid AND a.orgid=?
|
||||
ORDER BY u.firstname, u.lastname`, ctx.OrgID)
|
||||
WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+
|
||||
`ORDER BY u.firstname, u.lastname LIMIT 100`, ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows || len(u) == 0 {
|
||||
err = nil
|
||||
u = []user.User{}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf(" get users for org %s", ctx.OrgID))
|
||||
|
@ -154,6 +170,10 @@ func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []use
|
|||
ORDER BY u.firstname, u.lastname
|
||||
`, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, spaceID)
|
||||
|
||||
if err == sql.ErrNoRows || len(u) == 0 {
|
||||
err = nil
|
||||
u = []user.User{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("get space users for org %s", ctx.OrgID))
|
||||
}
|
||||
|
@ -182,6 +202,10 @@ func (s Scope) GetUsersForSpaces(ctx domain.RequestContext, spaces []string) (u
|
|||
query = s.Runtime.Db.Rebind(query)
|
||||
err = s.Runtime.Db.Select(&u, query, args...)
|
||||
|
||||
if err == sql.ErrNoRows || len(u) == 0 {
|
||||
err = nil
|
||||
u = []user.User{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("get users for spaces for user %s", ctx.UserID))
|
||||
}
|
||||
|
@ -255,3 +279,30 @@ func (s Scope) CountActiveUsers() (c int) {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// MatchUsers returns users that have match to either firstname, lastname or email.
|
||||
func (s Scope) MatchUsers(ctx domain.RequestContext, text string, maxMatches int) (u []user.User, err error) {
|
||||
text = strings.TrimSpace(strings.ToLower(text))
|
||||
likeQuery := ""
|
||||
if len(text) > 0 {
|
||||
likeQuery = " AND (LOWER(firstname) LIKE '%" + text + "%' OR LOWER(lastname) LIKE '%" + text + "%' OR LOWER(email) LIKE '%" + text + "%') "
|
||||
}
|
||||
|
||||
err = s.Runtime.Db.Select(&u,
|
||||
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised,
|
||||
u.global, a.active, a.editor, a.admin, a.users as viewusers
|
||||
FROM user u, account a
|
||||
WHERE a.orgid=? AND u.refid=a.userid AND a.active=1 `+likeQuery+
|
||||
`ORDER BY u.firstname,u.lastname LIMIT `+strconv.Itoa(maxMatches),
|
||||
ctx.OrgID)
|
||||
|
||||
if err == sql.ErrNoRows || len(u) == 0 {
|
||||
err = nil
|
||||
u = []user.User{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("matching users for org %s", ctx.OrgID))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
block "github.com/documize/community/domain/block/mysql"
|
||||
category "github.com/documize/community/domain/category/mysql"
|
||||
doc "github.com/documize/community/domain/document/mysql"
|
||||
group "github.com/documize/community/domain/group/mysql"
|
||||
link "github.com/documize/community/domain/link/mysql"
|
||||
org "github.com/documize/community/domain/organization/mysql"
|
||||
page "github.com/documize/community/domain/page/mysql"
|
||||
|
@ -42,6 +43,7 @@ func StoreMySQL(r *env.Runtime, s *domain.Store) {
|
|||
s.Block = block.Scope{Runtime: r}
|
||||
s.Category = category.Scope{Runtime: r}
|
||||
s.Document = doc.Scope{Runtime: r}
|
||||
s.Group = group.Scope{Runtime: r}
|
||||
s.Link = link.Scope{Runtime: r}
|
||||
s.Organization = org.Scope{Runtime: r}
|
||||
s.Page = page.Scope{Runtime: r}
|
||||
|
|
|
@ -41,8 +41,8 @@ func main() {
|
|||
// product details
|
||||
rt.Product = env.ProdInfo{}
|
||||
rt.Product.Major = "1"
|
||||
rt.Product.Minor = "57"
|
||||
rt.Product.Patch = "3"
|
||||
rt.Product.Minor = "58"
|
||||
rt.Product.Patch = "0"
|
||||
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
|
||||
rt.Product.Edition = "Community"
|
||||
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -17,8 +17,10 @@ export default Component.extend({
|
|||
SMTPHostEmptyError: empty('model.smtp.host'),
|
||||
SMTPPortEmptyError: empty('model.smtp.port'),
|
||||
SMTPSenderEmptyError: empty('model.smtp.sender'),
|
||||
SMTPUserIdEmptyError: empty('model.smtp.userid'),
|
||||
SMTPPasswordEmptyError: empty('model.smtp.password'),
|
||||
senderNameError: empty('model.smtp.senderName'),
|
||||
|
||||
buttonText: 'Save & Test',
|
||||
testSMTP: null,
|
||||
|
||||
actions: {
|
||||
saveSMTP() {
|
||||
|
@ -34,16 +36,22 @@ export default Component.extend({
|
|||
$("#smtp-sender").focus();
|
||||
return;
|
||||
}
|
||||
if (this.get('SMTPUserIdEmptyError')) {
|
||||
$("#smtp-userid").focus();
|
||||
return;
|
||||
}
|
||||
if (this.get('SMTPPasswordEmptyError')) {
|
||||
$("#smtp-password").focus();
|
||||
if (this.get('senderNameError')) {
|
||||
$("#smtp-senderName").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('saveSMTP')().then(() => {
|
||||
this.set('testSMTP', {
|
||||
success: true,
|
||||
message: ''
|
||||
},
|
||||
);
|
||||
|
||||
this.set('buttonText', 'Please wait...');
|
||||
|
||||
this.get('saveSMTP')().then((result) => {
|
||||
this.set('buttonText', 'Save & Test');
|
||||
this.set('testSMTP', result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,180 +10,64 @@
|
|||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import Component from '@ember/component';
|
||||
import { schedule, debounce } from '@ember/runloop';
|
||||
import AuthProvider from '../../mixins/auth';
|
||||
import ModalMixin from '../../mixins/modal';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend(AuthProvider, ModalMixin, {
|
||||
editUser: null,
|
||||
deleteUser: null,
|
||||
filter: '',
|
||||
hasSelectedUsers: false,
|
||||
showDeleteDialog: false,
|
||||
bulkUsers: '',
|
||||
newUser: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.password = {};
|
||||
this.filteredUsers = [];
|
||||
this.selectedUsers = [];
|
||||
this.set('newUser', { firstname: '', lastname: '', email: '', active: true });
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
let users = this.get('users');
|
||||
|
||||
users.forEach(user => {
|
||||
user.set('me', user.get('id') === this.get('session.session.authenticated.user.id'));
|
||||
user.set('selected', false);
|
||||
});
|
||||
|
||||
this.set('users', users);
|
||||
this.set('filteredUsers', users);
|
||||
},
|
||||
|
||||
onKeywordChange: function () {
|
||||
debounce(this, this.filterUsers, 350);
|
||||
}.observes('filter'),
|
||||
|
||||
filterUsers() {
|
||||
let users = this.get('users');
|
||||
let filteredUsers = [];
|
||||
let filter = this.get('filter').toLowerCase();
|
||||
|
||||
users.forEach(user => {
|
||||
if (user.get('fullname').toLowerCase().includes(filter) || user.get('email').toLowerCase().includes(filter)) {
|
||||
filteredUsers.pushObject(user);
|
||||
}
|
||||
});
|
||||
|
||||
this.set('filteredUsers', filteredUsers);
|
||||
},
|
||||
|
||||
|
||||
actions: {
|
||||
toggleSelect(user) {
|
||||
user.set('selected', !user.get('selected'));
|
||||
|
||||
let su = this.get('selectedUsers');
|
||||
if (user.get('selected')) {
|
||||
su.push(user.get('id'));
|
||||
} else {
|
||||
su = _.reject(su, function(id){ return id === user.get('id') });
|
||||
}
|
||||
|
||||
this.set('selectedUsers', su);
|
||||
this.set('hasSelectedUsers', su.length > 0);
|
||||
onOpenUserModal() {
|
||||
this.modalOpen("#add-user-modal", {"show": true}, '#newUserFirstname');
|
||||
},
|
||||
|
||||
toggleActive(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('active', !user.get('active'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
toggleEditor(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('editor', !user.get('editor'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
toggleAdmin(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('admin', !user.get('admin'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
toggleUsers(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('viewUsers', !user.get('viewUsers'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
onShowEdit(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
let userCopy = user.getProperties('id', 'created', 'revised', 'firstname', 'lastname', 'email', 'initials', 'active', 'editor', 'admin', 'viewUsers', 'accounts');
|
||||
|
||||
this.set('editUser', userCopy);
|
||||
this.set('password', {
|
||||
password: "",
|
||||
confirmation: ""
|
||||
});
|
||||
|
||||
$('#edit-user-modal').on('show.bs.modal', function(event) { // eslint-disable-line no-unused-vars
|
||||
schedule('afterRender', () => {
|
||||
$("#edit-firstname").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('#edit-user-modal').modal('dispose');
|
||||
$('#edit-user-modal').modal({show: true});
|
||||
},
|
||||
|
||||
onUpdate() {
|
||||
let user = this.get('editUser');
|
||||
let password = this.get('password');
|
||||
|
||||
if (is.empty(user.firstname)) {
|
||||
$("#edit-firstname").addClass("is-invalid").focus();
|
||||
onAddUser() {
|
||||
if (is.empty(this.get('newUser.firstname'))) {
|
||||
$("#newUserFirstname").addClass('is-invalid').focus();
|
||||
return;
|
||||
}
|
||||
if (is.empty(user.lastname)) {
|
||||
$("#edit-lastname").addClass("is-invalid").focus();
|
||||
$("#newUserFirstname").removeClass('is-invalid');
|
||||
|
||||
if (is.empty(this.get('newUser.lastname'))) {
|
||||
$("#newUserLastname").addClass('is-invalid').focus();
|
||||
return;
|
||||
}
|
||||
if (is.empty(user.email) || is.not.email(user.email)) {
|
||||
$("#edit-email").addClass("is-invalid").focus();
|
||||
$("#newUserLastname").removeClass('is-invalid');
|
||||
|
||||
if (is.empty(this.get('newUser.email')) || is.not.email(this.get('newUser.email'))) {
|
||||
$("#newUserEmail").addClass('is-invalid').focus();
|
||||
return;
|
||||
}
|
||||
$("#newUserEmail").removeClass('is-invalid');
|
||||
|
||||
$('#edit-user-modal').modal('hide');
|
||||
$('#edit-user-modal').modal('dispose');
|
||||
let user = this.get('newUser');
|
||||
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
|
||||
if (is.not.empty(password.password) && is.not.empty(password.confirmation) &&
|
||||
password.password === password.confirmation) {
|
||||
|
||||
let pw = this.get('onPassword');
|
||||
pw(user, password.password);
|
||||
}
|
||||
},
|
||||
|
||||
onShowDelete(id) {
|
||||
this.set('deleteUser', this.users.findBy("id", id));
|
||||
this.set('showDeleteDialog', true);
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
this.set('showDeleteDialog', false);
|
||||
|
||||
this.set('selectedUsers', []);
|
||||
this.set('hasSelectedUsers', false);
|
||||
|
||||
let cb = this.get('onDelete');
|
||||
cb(this.get('deleteUser.id'));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
onBulkDelete() {
|
||||
let su = this.get('selectedUsers');
|
||||
|
||||
su.forEach(userId => {
|
||||
let cb = this.get('onDelete');
|
||||
cb(userId);
|
||||
this.get('onAddUser')(user).then(() => {
|
||||
this.set('newUser', { firstname: '', lastname: '', email: '', active: true });
|
||||
});
|
||||
|
||||
this.set('selectedUsers', []);
|
||||
this.set('hasSelectedUsers', false);
|
||||
this.modalClose("#add-user-modal");
|
||||
},
|
||||
|
||||
this.modalClose('#admin-user-delete-modal');
|
||||
onAddUsers() {
|
||||
if (is.empty(this.get('bulkUsers'))) {
|
||||
$("#bulkUsers").addClass('is-invalid').focus();
|
||||
return;
|
||||
}
|
||||
$("#bulkUsers").removeClass('is-invalid');
|
||||
|
||||
this.get('onAddUsers')(this.get('bulkUsers')).then(() => {
|
||||
this.set('bulkUsers', '');
|
||||
});
|
||||
|
||||
this.modalClose("#add-user-modal");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
181
gui/app/components/customize/user-groups.js
Normal file
181
gui/app/components/customize/user-groups.js
Normal file
|
@ -0,0 +1,181 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { debounce } from '@ember/runloop';
|
||||
import Component from '@ember/component';
|
||||
import AuthProvider from '../../mixins/auth';
|
||||
import ModalMixin from '../../mixins/modal';
|
||||
|
||||
export default Component.extend(AuthProvider, ModalMixin, {
|
||||
groupSvc: service('group'),
|
||||
userSvc: service('user'),
|
||||
newGroup: null,
|
||||
searchText: '',
|
||||
showUsers: false,
|
||||
showMembers: true,
|
||||
users: null,
|
||||
members: null,
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
this.loadGroups();
|
||||
this.setDefaults();
|
||||
},
|
||||
|
||||
loadGroups() {
|
||||
this.get('groupSvc').getAll().then((groups) => {
|
||||
this.set('groups', groups);
|
||||
});
|
||||
},
|
||||
|
||||
setDefaults() {
|
||||
this.set('newGroup', { name: '', purpose: '' });
|
||||
},
|
||||
|
||||
loadGroupInfo() {
|
||||
let groupId = this.get('membersGroup.id');
|
||||
let searchText = this.get('searchText');
|
||||
|
||||
this.get('groupSvc').getGroupMembers(groupId).then((members) => {
|
||||
this.set('members', members);
|
||||
|
||||
this.get('userSvc').matchUsers(searchText).then((users) => {
|
||||
users.forEach((user) => {
|
||||
let m = members.findBy('userId', user.get('id'));
|
||||
user.set('isMember', is.not.undefined(m));
|
||||
})
|
||||
|
||||
if (this.get('showMembers') && members.length === 0) {
|
||||
this.set('showMembers', false);
|
||||
this.set('showUsers', true);
|
||||
}
|
||||
|
||||
this.set('users', users);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
onOpenGroupModal() {
|
||||
this.modalOpen("#add-group-modal", {"show": true}, '#new-group-name');
|
||||
},
|
||||
|
||||
onAddGroup(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let newGroup = this.get('newGroup');
|
||||
|
||||
if (is.empty(newGroup.name)) {
|
||||
$("#new-group-name").addClass("is-invalid").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('groupSvc').add(newGroup).then(() => {
|
||||
this.loadGroups();
|
||||
});
|
||||
|
||||
this.modalClose("#add-group-modal");
|
||||
this.setDefaults();
|
||||
},
|
||||
|
||||
onShowDeleteModal(groupId) {
|
||||
this.set('deleteGroup', { name: '', id: groupId });
|
||||
this.modalOpen("#delete-group-modal", {"show": true}, '#delete-group-name');
|
||||
},
|
||||
|
||||
onDeleteGroup(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let deleteGroup = this.get('deleteGroup');
|
||||
let group = this.get('groups').findBy('id', deleteGroup.id);
|
||||
|
||||
if (is.empty(deleteGroup.name) || group.get('name') !== deleteGroup.name) {
|
||||
$("#delete-group-name").addClass("is-invalid").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('groupSvc').delete(deleteGroup.id).then(() => {
|
||||
this.loadGroups();
|
||||
});
|
||||
|
||||
this.modalClose("#delete-group-modal");
|
||||
this.set('deleteGroup', { name: '', id: '' });
|
||||
},
|
||||
|
||||
onShowEditModal(groupId) {
|
||||
this.set('editGroup', this.get('groups').findBy('id', groupId));
|
||||
this.modalOpen("#edit-group-modal", {"show": true}, '#edit-group-name');
|
||||
},
|
||||
|
||||
onEditGroup(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let group = this.get('editGroup');
|
||||
|
||||
if (is.empty(group.get('name'))) {
|
||||
$("#edit-group-name").addClass("is-invalid").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('groupSvc').update(group).then(() => {
|
||||
this.load();
|
||||
});
|
||||
|
||||
this.modalClose("#edit-group-modal");
|
||||
this.set('editGroup', null);
|
||||
},
|
||||
|
||||
onShowMembersModal(groupId) {
|
||||
this.set('membersGroup', this.get('groups').findBy('id', groupId));
|
||||
this.modalOpen("#group-members-modal", {"show": true}, '#group-members-search');
|
||||
this.set('members', null);
|
||||
this.set('users', null);
|
||||
this.set('showMembers', true);
|
||||
this.set('showUsers', false);
|
||||
this.loadGroupInfo();
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
debounce(this, function() {
|
||||
let searchText = this.get('searchText');
|
||||
this.loadGroupInfo();
|
||||
|
||||
if (is.not.empty(searchText)) {
|
||||
this.set('showMembers', false);
|
||||
this.set('showUsers', true);
|
||||
} else {
|
||||
this.set('showMembers', true);
|
||||
this.set('showUsers', false);
|
||||
}
|
||||
}, 250);
|
||||
},
|
||||
|
||||
onLeaveGroup(userId) {
|
||||
let groupId = this.get('membersGroup.id');
|
||||
|
||||
this.get('groupSvc').leave(groupId, userId).then(() => {
|
||||
this.loadGroupInfo();
|
||||
this.loadGroups();
|
||||
});
|
||||
},
|
||||
|
||||
onJoinGroup(userId) {
|
||||
let groupId = this.get('membersGroup.id');
|
||||
|
||||
this.get('groupSvc').join(groupId, userId).then(() => {
|
||||
this.loadGroupInfo();
|
||||
this.loadGroups();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
228
gui/app/components/customize/user-list.js
Normal file
228
gui/app/components/customize/user-list.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { schedule, debounce } from '@ember/runloop';
|
||||
import Component from '@ember/component';
|
||||
import AuthProvider from '../../mixins/auth';
|
||||
import ModalMixin from '../../mixins/modal';
|
||||
|
||||
export default Component.extend(AuthProvider, ModalMixin, {
|
||||
groupSvc: service('group'),
|
||||
editUser: null,
|
||||
deleteUser: null,
|
||||
filter: '',
|
||||
hasSelectedUsers: false,
|
||||
showDeleteDialog: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.password = {};
|
||||
this.selectedUsers = [];
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.get('groupSvc').getAll().then((groups) => {
|
||||
this.set('groups', groups);
|
||||
});
|
||||
|
||||
let users = this.get('users');
|
||||
|
||||
users.forEach(user => {
|
||||
user.set('me', user.get('id') === this.get('session.session.authenticated.user.id'));
|
||||
user.set('selected', false);
|
||||
});
|
||||
|
||||
this.set('users', users);
|
||||
},
|
||||
|
||||
onKeywordChange: function () {
|
||||
debounce(this, this.filterUsers, 350);
|
||||
}.observes('filter'),
|
||||
|
||||
filterUsers() {
|
||||
this.get('onFilter')(this.get('filter'));
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleSelect(user) {
|
||||
user.set('selected', !user.get('selected'));
|
||||
|
||||
let su = this.get('selectedUsers');
|
||||
if (user.get('selected')) {
|
||||
su.push(user.get('id'));
|
||||
} else {
|
||||
su = _.reject(su, function(id){ return id === user.get('id') });
|
||||
}
|
||||
|
||||
this.set('selectedUsers', su);
|
||||
this.set('hasSelectedUsers', su.length > 0);
|
||||
},
|
||||
|
||||
toggleActive(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('active', !user.get('active'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
toggleEditor(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('editor', !user.get('editor'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
toggleAdmin(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('admin', !user.get('admin'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
toggleUsers(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
user.set('viewUsers', !user.get('viewUsers'));
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
},
|
||||
|
||||
onShowEdit(id) {
|
||||
let user = this.users.findBy("id", id);
|
||||
let userCopy = user.getProperties('id', 'created', 'revised', 'firstname', 'lastname', 'email', 'initials', 'active', 'editor', 'admin', 'viewUsers', 'accounts');
|
||||
|
||||
this.set('editUser', userCopy);
|
||||
this.set('password', {
|
||||
password: "",
|
||||
confirmation: ""
|
||||
});
|
||||
|
||||
$('#edit-user-modal').on('show.bs.modal', function(event) { // eslint-disable-line no-unused-vars
|
||||
schedule('afterRender', () => {
|
||||
$("#edit-firstname").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('#edit-user-modal').modal('dispose');
|
||||
$('#edit-user-modal').modal({show: true});
|
||||
},
|
||||
|
||||
onUpdate() {
|
||||
let user = this.get('editUser');
|
||||
let password = this.get('password');
|
||||
|
||||
if (is.empty(user.firstname)) {
|
||||
$("#edit-firstname").addClass("is-invalid").focus();
|
||||
return;
|
||||
}
|
||||
if (is.empty(user.lastname)) {
|
||||
$("#edit-lastname").addClass("is-invalid").focus();
|
||||
return;
|
||||
}
|
||||
if (is.empty(user.email) || is.not.email(user.email)) {
|
||||
$("#edit-email").addClass("is-invalid").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#edit-user-modal').modal('hide');
|
||||
$('#edit-user-modal').modal('dispose');
|
||||
|
||||
let cb = this.get('onSave');
|
||||
cb(user);
|
||||
|
||||
if (is.not.empty(password.password) && is.not.empty(password.confirmation) &&
|
||||
password.password === password.confirmation) {
|
||||
|
||||
let pw = this.get('onPassword');
|
||||
pw(user, password.password);
|
||||
}
|
||||
},
|
||||
|
||||
onShowDelete(id) {
|
||||
this.set('deleteUser', this.users.findBy("id", id));
|
||||
this.set('showDeleteDialog', true);
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
this.set('showDeleteDialog', false);
|
||||
|
||||
this.set('selectedUsers', []);
|
||||
this.set('hasSelectedUsers', false);
|
||||
|
||||
let cb = this.get('onDelete');
|
||||
cb(this.get('deleteUser.id'));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
onBulkDelete() {
|
||||
let su = this.get('selectedUsers');
|
||||
|
||||
su.forEach(userId => {
|
||||
let cb = this.get('onDelete');
|
||||
cb(userId);
|
||||
});
|
||||
|
||||
this.set('selectedUsers', []);
|
||||
this.set('hasSelectedUsers', false);
|
||||
|
||||
this.modalClose('#admin-user-delete-modal');
|
||||
},
|
||||
|
||||
onShowGroupsModal(userId) {
|
||||
let user = this.get('users').findBy('id', userId);
|
||||
this.set('selectedUser', user);
|
||||
|
||||
let userGroups = user.get('groups');
|
||||
|
||||
// mark up groups user belongs to...
|
||||
let groups = this.get('groups');
|
||||
groups.forEach((g) => {
|
||||
let hasGroup = userGroups.findBy('roleId', g.get('id'));
|
||||
g.set('isMember', is.not.undefined(hasGroup));
|
||||
})
|
||||
this.set('groups', groups);
|
||||
|
||||
this.modalOpen("#group-member-modal", {"show": true});
|
||||
},
|
||||
|
||||
onLeaveGroup(groupId) {
|
||||
let userId = this.get('selectedUser.id');
|
||||
let group = this.get('groups').findBy('id', groupId);
|
||||
group.set('isMember', false);
|
||||
|
||||
if (is.undefined(groupId) || is.undefined(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('groupSvc').leave(groupId, userId).then(() => {
|
||||
this.filterUsers();
|
||||
});
|
||||
},
|
||||
|
||||
onJoinGroup(groupId) {
|
||||
let userId = this.get('selectedUser.id');
|
||||
let group = this.get('groups').findBy('id', groupId);
|
||||
group.set('isMember', true);
|
||||
|
||||
if (is.undefined(groupId) || is.undefined(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('groupSvc').join(groupId, userId).then(() => {
|
||||
this.filterUsers();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import { empty, and } from '@ember/object/computed';
|
||||
import Component from '@ember/component';
|
||||
import { isEmpty } from '@ember/utils';
|
||||
import { get, set } from '@ember/object';
|
||||
import AuthProvider from '../../mixins/auth';
|
||||
|
||||
export default Component.extend(AuthProvider, {
|
||||
firstnameEmpty: empty('newUser.firstname'),
|
||||
lastnameEmpty: empty('newUser.lastname'),
|
||||
emailEmpty: empty('newUser.email'),
|
||||
hasFirstnameEmptyError: and('firstnameEmpty', 'firstnameError'),
|
||||
hasLastnameEmptyError: and('lastnameEmpty', 'lastnameError'),
|
||||
hasEmailEmptyError: and('emailEmpty', 'emailError'),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.newUser = { firstname: "", lastname: "", email: "", active: true };
|
||||
},
|
||||
|
||||
actions: {
|
||||
add() {
|
||||
if (isEmpty(this.get('newUser.firstname'))) {
|
||||
set(this, 'firstnameError', true);
|
||||
return $("#newUserFirstname").focus();
|
||||
}
|
||||
if (isEmpty(this.get('newUser.lastname'))) {
|
||||
set(this, 'lastnameError', true);
|
||||
return $("#newUserLastname").focus();
|
||||
}
|
||||
if (isEmpty(this.get('newUser.email')) || is.not.email(this.get('newUser.email'))) {
|
||||
set(this, 'emailError', true);
|
||||
return $("#newUserEmail").focus();
|
||||
}
|
||||
|
||||
let user = get(this, 'newUser');
|
||||
|
||||
get(this, 'add')(user).then(() => {
|
||||
this.set('newUser', { firstname: "", lastname: "", email: "", active: true });
|
||||
set(this, 'firstnameError', false);
|
||||
set(this, 'lastnameError', false);
|
||||
set(this, 'emailError', false);
|
||||
$("#newUserFirstname").focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -10,14 +10,16 @@
|
|||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import Component from '@ember/component';
|
||||
import { A } from "@ember/array"
|
||||
import { inject as service } from '@ember/service';
|
||||
import TooltipMixin from '../../mixins/tooltip';
|
||||
import ModalMixin from '../../mixins/modal';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend(ModalMixin, TooltipMixin, {
|
||||
userService: service('user'),
|
||||
categoryService: service('category'),
|
||||
spaceSvc: service('folder'),
|
||||
groupSvc: service('group'),
|
||||
categorySvc: service('category'),
|
||||
appMeta: service(),
|
||||
store: service(),
|
||||
newCategory: '',
|
||||
|
@ -42,43 +44,42 @@ export default Component.extend(ModalMixin, TooltipMixin, {
|
|||
|
||||
load() {
|
||||
// get categories
|
||||
this.get('categoryService').getAll(this.get('folder.id')).then((c) => {
|
||||
this.get('categorySvc').getAll(this.get('folder.id')).then((c) => {
|
||||
this.set('category', c);
|
||||
|
||||
// get summary of documents and users for each category in space
|
||||
this.get('categoryService').getSummary(this.get('folder.id')).then((s) => {
|
||||
this.get('categorySvc').getSummary(this.get('folder.id')).then((s) => {
|
||||
c.forEach((cat) => {
|
||||
let docs = _.findWhere(s, {categoryId: cat.get('id'), type: 'documents'});
|
||||
let docCount = is.not.undefined(docs) ? docs.count : 0;
|
||||
|
||||
let users = _.findWhere(s, {categoryId: cat.get('id'), type: 'users'});
|
||||
let userCount = is.not.undefined(users) ? users.count : 0;
|
||||
let docs = _.where(s, {categoryId: cat.get('id'), type: 'documents'});
|
||||
let docCount = 0;
|
||||
docs.forEach((d) => { docCount = docCount + d.count });
|
||||
|
||||
let users = _.where(s, {categoryId: cat.get('id'), type: 'users'});
|
||||
let userCount = 0;
|
||||
users.forEach((u) => { userCount = userCount + u.count });
|
||||
|
||||
cat.set('documents', docCount);
|
||||
cat.set('users', userCount);
|
||||
});
|
||||
});
|
||||
|
||||
// get users that this space admin user can see
|
||||
this.get('userService').getSpaceUsers(this.get('folder.id')).then((users) => {
|
||||
// set up Everyone user
|
||||
let u = {
|
||||
orgId: this.get('folder.orgId'),
|
||||
folderId: this.get('folder.id'),
|
||||
userId: '',
|
||||
firstname: 'Everyone',
|
||||
lastname: '',
|
||||
};
|
||||
|
||||
let data = this.get('store').normalize('user', u)
|
||||
users.pushObject(this.get('store').push(data));
|
||||
|
||||
users = users.sortBy('firstname', 'lastname');
|
||||
this.set('users', users);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
permissionRecord(who, whoId, name) {
|
||||
let raw = {
|
||||
id: whoId,
|
||||
orgId: this.get('folder.orgId'),
|
||||
categoryId: this.get('currentCategory.id'),
|
||||
whoId: whoId,
|
||||
who: who,
|
||||
name: name,
|
||||
categoryView: false,
|
||||
};
|
||||
|
||||
let rec = this.get('store').normalize('category-permission', raw);
|
||||
return this.get('store').push(rec);
|
||||
},
|
||||
|
||||
setEdit(id, val) {
|
||||
let cats = this.get('category');
|
||||
let cat = cats.findBy('id', id);
|
||||
|
@ -109,7 +110,7 @@ export default Component.extend(ModalMixin, TooltipMixin, {
|
|||
folderId: this.get('folder.id')
|
||||
};
|
||||
|
||||
this.get('categoryService').add(c).then(() => {
|
||||
this.get('categorySvc').add(c).then(() => {
|
||||
this.load();
|
||||
});
|
||||
},
|
||||
|
@ -124,7 +125,7 @@ export default Component.extend(ModalMixin, TooltipMixin, {
|
|||
onDelete() {
|
||||
this.modalClose('#category-delete-modal');
|
||||
|
||||
this.get('categoryService').delete(this.get('deleteId')).then(() => {
|
||||
this.get('categorySvc').delete(this.get('deleteId')).then(() => {
|
||||
this.load();
|
||||
});
|
||||
},
|
||||
|
@ -150,7 +151,7 @@ export default Component.extend(ModalMixin, TooltipMixin, {
|
|||
cat = this.setEdit(id, false);
|
||||
$('#edit-category-' + cat.get('id')).removeClass('is-invalid');
|
||||
|
||||
this.get('categoryService').save(cat).then(() => {
|
||||
this.get('categorySvc').save(cat).then(() => {
|
||||
this.load();
|
||||
});
|
||||
|
||||
|
@ -160,45 +161,46 @@ export default Component.extend(ModalMixin, TooltipMixin, {
|
|||
onShowAccessPicker(catId) {
|
||||
this.set('showCategoryAccess', true);
|
||||
|
||||
let users = this.get('users');
|
||||
let categoryPermissions = A([]);
|
||||
let category = this.get('category').findBy('id', catId);
|
||||
|
||||
this.get('categoryService').getPermissions(category.get('id')).then((viewers) => {
|
||||
// mark those users as selected that have already been given permission
|
||||
// to see the current category;
|
||||
users.forEach((user) => {
|
||||
let userId = user.get('id');
|
||||
let selected = viewers.isAny('whoId', userId);
|
||||
user.set('selected', selected);
|
||||
this.set('currentCategory', category);
|
||||
this.set('categoryPermissions', categoryPermissions);
|
||||
|
||||
// get space permissions
|
||||
this.get('spaceSvc').getPermissions(this.get('folder.id')).then((spacePermissions) => {
|
||||
spacePermissions.forEach((sp) => {
|
||||
let cp = this.permissionRecord(sp.get('who'), sp.get('whoId'), sp.get('name'));
|
||||
cp.set('selected', false);
|
||||
categoryPermissions.pushObject(cp);
|
||||
});
|
||||
|
||||
this.set('categoryUsers', users);
|
||||
this.set('currentCategory', category);
|
||||
this.get('categorySvc').getPermissions(category.get('id')).then((perms) => {
|
||||
// mark those users as selected that have permission to see the current category
|
||||
perms.forEach((perm) => {
|
||||
let c = categoryPermissions.findBy('whoId', perm.get('whoId'));
|
||||
if (is.not.undefined(c)) {
|
||||
c.set('selected', true);
|
||||
}
|
||||
});
|
||||
|
||||
this.set('categoryPermissions', categoryPermissions.sortBy('who', 'name'));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onToggle(item) {
|
||||
item.set('selected', !item.get('selected'));
|
||||
},
|
||||
|
||||
onGrantAccess() {
|
||||
this.set('showCategoryAccess', false);
|
||||
|
||||
let folder = this.get('folder');
|
||||
let category = this.get('currentCategory');
|
||||
let users = this.get('categoryUsers').filterBy('selected', true);
|
||||
let viewers = [];
|
||||
let perms = this.get('categoryPermissions').filterBy('selected', true);
|
||||
|
||||
users.forEach((user) => {
|
||||
let userId = user.get('id');
|
||||
|
||||
let v = {
|
||||
orgId: this.get('folder.orgId'),
|
||||
folderId: this.get('folder.id'),
|
||||
categoryId: category.get('id'),
|
||||
userId: userId
|
||||
};
|
||||
|
||||
viewers.push(v);
|
||||
});
|
||||
|
||||
this.get('categoryService').setViewers(folder.get('id'), category.get('id'), viewers).then(() => {
|
||||
this.get('categorySvc').setViewers(folder.get('id'), category.get('id'), perms).then(() => {
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,93 +9,128 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
import { setProperties } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { A } from "@ember/array"
|
||||
import { debounce } from '@ember/runloop';
|
||||
import ModalMixin from '../../mixins/modal';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend(ModalMixin, {
|
||||
folderService: service('folder'),
|
||||
userService: service('user'),
|
||||
appMeta: service(),
|
||||
groupSvc: service('group'),
|
||||
spaceSvc: service('folder'),
|
||||
userSvc: service('user'),
|
||||
appMeta: service(),
|
||||
store: service(),
|
||||
|
||||
spacePermissions: null,
|
||||
users: null,
|
||||
searchText: '',
|
||||
|
||||
didReceiveAttrs() {
|
||||
this.get('userService').getSpaceUsers(this.get('folder.id')).then((users) => {
|
||||
this.set('users', users);
|
||||
let spacePermissions = A([]);
|
||||
let constants = this.get('constants');
|
||||
|
||||
// set up users
|
||||
let folderPermissions = [];
|
||||
// get groups
|
||||
this.get('groupSvc').getAll().then((groups) => {
|
||||
this.set('groups', groups);
|
||||
|
||||
users.forEach((user) => {
|
||||
let u = {
|
||||
orgId: this.get('folder.orgId'),
|
||||
folderId: this.get('folder.id'),
|
||||
userId: user.get('id'),
|
||||
fullname: user.get('fullname'),
|
||||
spaceView: false,
|
||||
spaceManage: false,
|
||||
spaceOwner: false,
|
||||
documentAdd: false,
|
||||
documentEdit: false,
|
||||
documentDelete: false,
|
||||
documentMove: false,
|
||||
documentCopy: false,
|
||||
documentTemplate: false,
|
||||
documentApprove: false,
|
||||
};
|
||||
|
||||
let data = this.get('store').normalize('space-permission', u)
|
||||
folderPermissions.pushObject(this.get('store').push(data));
|
||||
groups.forEach((g) => {
|
||||
let pr = this.permissionRecord(constants.WhoType.Group, g.get('id'), g.get('name'));
|
||||
pr.set('members', g.get('members'));
|
||||
spacePermissions.pushObject(pr);
|
||||
});
|
||||
|
||||
// set up Everyone user
|
||||
let u = {
|
||||
orgId: this.get('folder.orgId'),
|
||||
folderId: this.get('folder.id'),
|
||||
userId: '0',
|
||||
fullname: ' Everyone',
|
||||
spaceView: false,
|
||||
spaceManage: false,
|
||||
spaceOwner: false,
|
||||
documentAdd: false,
|
||||
documentEdit: false,
|
||||
documentDelete: false,
|
||||
documentMove: false,
|
||||
documentCopy: false,
|
||||
documentTemplate: false,
|
||||
documentApprove: false,
|
||||
};
|
||||
let hasEveryoneId = false;
|
||||
|
||||
let data = this.get('store').normalize('space-permission', u)
|
||||
folderPermissions.pushObject(this.get('store').push(data));
|
||||
|
||||
this.get('folderService').getPermissions(this.get('folder.id')).then((permissions) => {
|
||||
permissions.forEach((permission, index) => { // eslint-disable-line no-unused-vars
|
||||
let record = folderPermissions.findBy('userId', permission.get('userId'));
|
||||
if (is.not.undefined(record)) {
|
||||
record = setProperties(record, permission);
|
||||
// get space permissions
|
||||
this.get('spaceSvc').getPermissions(this.get('folder.id')).then((permissions) => {
|
||||
permissions.forEach((perm, index) => { // eslint-disable-line no-unused-vars
|
||||
// is this permission for group or user?
|
||||
if (perm.get('who') === constants.WhoType.Group) {
|
||||
// group permission
|
||||
spacePermissions.forEach((sp) => {
|
||||
if (sp.get('whoId') == perm.get('whoId')) {
|
||||
sp.setProperties(perm);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// user permission
|
||||
if (perm.get('whoId') === constants.EveryoneUserId) {
|
||||
perm.set('name', ' ' + perm.get('name'));
|
||||
hasEveryoneId = true;
|
||||
}
|
||||
spacePermissions.pushObject(perm);
|
||||
}
|
||||
});
|
||||
|
||||
this.set('permissions', folderPermissions.sortBy('fullname'));
|
||||
// always show everyone
|
||||
if (!hasEveryoneId) {
|
||||
let pr = this.permissionRecord(constants.WhoType.User, constants.EveryoneUserId, ' ' + constants.EveryoneUserName);
|
||||
spacePermissions.pushObject(pr);
|
||||
}
|
||||
|
||||
this.set('spacePermissions', spacePermissions.sortBy('who', 'name'));
|
||||
});
|
||||
});
|
||||
|
||||
this.set('searchText', '');
|
||||
},
|
||||
|
||||
permissionRecord(who, whoId, name) {
|
||||
let raw = {
|
||||
id: whoId,
|
||||
orgId: this.get('folder.orgId'),
|
||||
folderId: this.get('folder.id'),
|
||||
whoId: whoId,
|
||||
who: who,
|
||||
name: name,
|
||||
spaceView: false,
|
||||
spaceManage: false,
|
||||
spaceOwner: false,
|
||||
documentAdd: false,
|
||||
documentEdit: false,
|
||||
documentDelete: false,
|
||||
documentMove: false,
|
||||
documentCopy: false,
|
||||
documentTemplate: false,
|
||||
documentApprove: false,
|
||||
};
|
||||
|
||||
let rec = this.get('store').normalize('space-permission', raw);
|
||||
return this.get('store').push(rec);
|
||||
},
|
||||
|
||||
getDefaultInvitationMessage() {
|
||||
return "Hey there, I am sharing the " + this.get('folder.name') + " space (in " + this.get("appMeta.title") + ") with you so we can both collaborate on documents.";
|
||||
},
|
||||
|
||||
matchUsers(s) {
|
||||
let spacePermissions = this.get('spacePermissions');
|
||||
let filteredUsers = A([]);
|
||||
|
||||
this.get('userSvc').matchUsers(s).then((users) => {
|
||||
|
||||
users.forEach((user) => {
|
||||
let exists = spacePermissions.findBy('whoId', user.get('id'));
|
||||
|
||||
if (is.undefined(exists)) {
|
||||
filteredUsers.pushObject(user);
|
||||
}
|
||||
});
|
||||
|
||||
this.set('filteredUsers', filteredUsers);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
setPermissions() {
|
||||
let message = this.getDefaultInvitationMessage();
|
||||
let permissions = this.get('permissions');
|
||||
let permissions = this.get('spacePermissions');
|
||||
let folder = this.get('folder');
|
||||
let payload = { Message: message, Permissions: permissions };
|
||||
let constants = this.get('constants');
|
||||
|
||||
let hasEveryone = _.find(permissions, function (permission) {
|
||||
return permission.get('userId') === "0" &&
|
||||
let hasEveryone = _.find(permissions, (permission) => {
|
||||
return permission.get('whoId') === constants.EveryoneUserId &&
|
||||
(permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') ||
|
||||
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove'));
|
||||
});
|
||||
|
@ -103,7 +138,7 @@ export default Component.extend(ModalMixin, {
|
|||
// see if more than oen user is granted access to space (excluding everyone)
|
||||
let roleCount = 0;
|
||||
permissions.forEach((permission) => {
|
||||
if (permission.get('userId') !== "0" &&
|
||||
if (permission.get('whoId') !== constants.EveryoneUserId &&
|
||||
(permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') ||
|
||||
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove'))) {
|
||||
roleCount += 1;
|
||||
|
@ -120,9 +155,34 @@ export default Component.extend(ModalMixin, {
|
|||
}
|
||||
}
|
||||
|
||||
this.get('folderService').savePermissions(folder.get('id'), payload).then(() => {
|
||||
this.get('spaceSvc').savePermissions(folder.get('id'), payload).then(() => {
|
||||
this.modalClose('#space-permission-modal');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
debounce(this, function() {
|
||||
let searchText = this.get('searchText').trim();
|
||||
|
||||
if (searchText.length === 0) {
|
||||
this.set('filteredUsers', A([]));
|
||||
return;
|
||||
}
|
||||
|
||||
this.matchUsers(searchText);
|
||||
}, 250);
|
||||
},
|
||||
|
||||
onAdd(user) {
|
||||
let spacePermissions = this.get('spacePermissions');
|
||||
let constants = this.get('constants');
|
||||
|
||||
let exists = spacePermissions.findBy('whoId', user.get('id'));
|
||||
if (is.undefined(exists)) {
|
||||
spacePermissions.pushObject(this.permissionRecord(constants.WhoType.User, user.get('id'), user.get('fullname')));
|
||||
this.set('spacePermissions', spacePermissions);
|
||||
// this.set('spacePermissions', spacePermissions.sortBy('who', 'name'));
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { schedule } from '@ember/runloop';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
@ -18,6 +17,7 @@ import TooltipMixin from '../../mixins/tooltip';
|
|||
import ModalMixin from '../../mixins/modal';
|
||||
import AuthMixin from '../../mixins/auth';
|
||||
import stringUtil from '../../utils/string';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
|
||||
spaceService: service('folder'),
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
// access like so:
|
||||
// let constants = this.get('constants');
|
||||
|
||||
let constants = EmberObject.extend({
|
||||
// Document
|
||||
ProtectionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||
|
@ -48,7 +51,16 @@ let constants = EmberObject.extend({
|
|||
PageType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||
Tab: 'tab',
|
||||
Section: 'section'
|
||||
}
|
||||
},
|
||||
|
||||
// Who a permission record relates to
|
||||
WhoType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||
User: 'user',
|
||||
Group: 'role'
|
||||
},
|
||||
|
||||
EveryoneUserId: "0",
|
||||
EveryoneUserName: "Everyone"
|
||||
});
|
||||
|
||||
export default { constants }
|
23
gui/app/models/category-permission.js
Normal file
23
gui/app/models/category-permission.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
|
||||
export default Model.extend({
|
||||
orgId: attr('string'),
|
||||
categoryId: attr('string'),
|
||||
whoId: attr('string'),
|
||||
who: attr('string'),
|
||||
categoryView: attr('boolean'),
|
||||
name: attr('string'), // read-only
|
||||
selected: attr('boolean') // client-side only
|
||||
});
|
|
@ -15,11 +15,11 @@ import attr from 'ember-data/attr';
|
|||
export default Model.extend({
|
||||
orgId: attr('string'),
|
||||
documentId: attr('string'),
|
||||
userId: attr('string'),
|
||||
fullname: attr('string'), // client-side usage only, not from API
|
||||
|
||||
documentEdit: attr('boolean'), // space level setting
|
||||
documentApprove: attr('boolean'), // space level setting
|
||||
documentRoleEdit: attr('boolean'), // document level setting
|
||||
documentRoleApprove: attr('boolean') // document level setting
|
||||
whoId: attr('string'),
|
||||
who: attr('string'),
|
||||
name: attr('string'),
|
||||
documentEdit: attr('boolean'),
|
||||
documentApprove: attr('boolean'),
|
||||
documentRoleEdit: attr('boolean'),
|
||||
documentRoleApprove: attr('boolean')
|
||||
});
|
27
gui/app/models/group-member.js
Normal file
27
gui/app/models/group-member.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Model.extend({
|
||||
orgId: attr('string'),
|
||||
roleId: attr('string'),
|
||||
userId: attr('string'),
|
||||
|
||||
// for UI only
|
||||
firstname: attr('string'),
|
||||
lastname: attr('string'),
|
||||
fullname: computed('firstname', 'lastname', function () {
|
||||
return `${this.get('firstname')} ${this.get('lastname')}`;
|
||||
})
|
||||
});
|
24
gui/app/models/group.js
Normal file
24
gui/app/models/group.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
|
||||
export default Model.extend({
|
||||
orgId: attr('string'),
|
||||
name: attr('string'),
|
||||
purpose: attr('string'),
|
||||
created: attr(),
|
||||
revised: attr(),
|
||||
|
||||
// for UI
|
||||
members: attr('number', { defaultValue: 0 })
|
||||
});
|
|
@ -11,14 +11,12 @@
|
|||
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
// import { belongsTo, hasMany } from 'ember-data/relationships';
|
||||
|
||||
export default Model.extend({
|
||||
orgId: attr('string'),
|
||||
folderId: attr('string'),
|
||||
userId: attr('string'),
|
||||
fullname: attr('string'), // client-side usage only, not from API
|
||||
|
||||
whoId: attr('string'),
|
||||
who: attr('string'),
|
||||
spaceView: attr('boolean'),
|
||||
spaceManage: attr('boolean'),
|
||||
spaceOwner: attr('boolean'),
|
||||
|
@ -28,5 +26,7 @@ export default Model.extend({
|
|||
documentMove: attr('boolean'),
|
||||
documentCopy: attr('boolean'),
|
||||
documentTemplate: attr('boolean'),
|
||||
documentApprove: attr('boolean')
|
||||
documentApprove: attr('boolean'),
|
||||
name: attr('string'), // read-only
|
||||
members: attr('number') // read-only
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
import { computed } from '@ember/object';
|
||||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
// import { belongsTo, hasMany } from 'ember-data/relationships';
|
||||
|
||||
export default Model.extend({
|
||||
firstname: attr('string'),
|
||||
|
@ -25,6 +24,7 @@ export default Model.extend({
|
|||
viewUsers: attr('boolean', { defaultValue: false }),
|
||||
global: attr('boolean', { defaultValue: false }),
|
||||
accounts: attr(),
|
||||
groups: attr(),
|
||||
created: attr(),
|
||||
revised: attr(),
|
||||
|
||||
|
|
17
gui/app/pods/customize/groups/controller.js
Normal file
17
gui/app/pods/customize/groups/controller.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend({
|
||||
actions: {
|
||||
}
|
||||
});
|
25
gui/app/pods/customize/groups/route.js
Normal file
25
gui/app/pods/customize/groups/route.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
|
||||
|
||||
export default Route.extend(AuthenticatedRouteMixin, {
|
||||
beforeModel () {
|
||||
if (!this.session.isAdmin) {
|
||||
this.transitionTo('auth.login');
|
||||
}
|
||||
},
|
||||
|
||||
activate() {
|
||||
this.get('browser').setTitle('Groups');
|
||||
}
|
||||
});
|
1
gui/app/pods/customize/groups/template.hbs
Normal file
1
gui/app/pods/customize/groups/template.hbs
Normal file
|
@ -0,0 +1 @@
|
|||
{{customize/user-groups}}
|
|
@ -18,7 +18,8 @@ export default Controller.extend({
|
|||
actions: {
|
||||
saveSMTP() {
|
||||
if(this.get('session.isGlobalAdmin')) {
|
||||
return this.get('global').saveSMTPConfig(this.model.smtp).then(() => {
|
||||
return this.get('global').saveSMTPConfig(this.model.smtp).then((response) => {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
<ul class="tabnav-control">
|
||||
{{#link-to 'customize.general' activeClass='selected' class="tab" tagName="li"}}General{{/link-to}}
|
||||
{{#link-to 'customize.folders' activeClass='selected' class="tab" tagName="li"}}Spaces{{/link-to}}
|
||||
{{#link-to 'customize.groups' activeClass='selected' class="tab" tagName="li"}}Groups{{/link-to}}
|
||||
{{#link-to 'customize.users' activeClass='selected' class="tab" tagName="li"}}Users{{/link-to}}
|
||||
{{#if session.isGlobalAdmin}}
|
||||
{{#link-to 'customize.smtp' activeClass='selected' class="tab" tagName="li"}}SMTP{{/link-to}}
|
||||
|
|
|
@ -9,54 +9,49 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
import { set } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend({
|
||||
userService: service('user'),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.newUser = { firstname: "", lastname: "", email: "", active: true };
|
||||
loadUsers(filter) {
|
||||
this.get('userService').getComplete(filter).then((users) => {
|
||||
this.set('model', users);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
add(user) {
|
||||
set(this, 'newUser', user);
|
||||
|
||||
return this.get('userService')
|
||||
.add(this.get('newUser'))
|
||||
.then((user) => {
|
||||
this.get('model').pushObject(user);
|
||||
})
|
||||
.catch(function (error) {
|
||||
let msg = error.status === 409 ? 'Unable to add duplicate user' : 'Unable to add user';
|
||||
this.showNotification(msg);
|
||||
});
|
||||
onAddUser(user) {
|
||||
return this.get('userService').add(user).then((user) => {
|
||||
this.get('model').pushObject(user);
|
||||
});
|
||||
},
|
||||
|
||||
onAddUsers(list) {
|
||||
return this.get('userService').addBulk(list).then(() => {
|
||||
this.loadUsers('');
|
||||
});
|
||||
},
|
||||
|
||||
onDelete(userId) {
|
||||
let self = this;
|
||||
this.get('userService').remove(userId).then(function () {
|
||||
self.get('userService').getComplete().then(function (users) {
|
||||
self.set('model', users);
|
||||
});
|
||||
this.get('userService').remove(userId).then( () => {
|
||||
this.loadUsers('');
|
||||
});
|
||||
},
|
||||
|
||||
onSave(user) {
|
||||
let self = this;
|
||||
this.get('userService').save(user).then(function () {
|
||||
|
||||
self.get('userService').getComplete().then(function (users) {
|
||||
self.set('model', users);
|
||||
});
|
||||
this.get('userService').save(user).then(() => {
|
||||
this.loadUsers('');
|
||||
});
|
||||
},
|
||||
|
||||
onPassword(user, password) {
|
||||
this.get('userService').updatePassword(user.id, password);
|
||||
},
|
||||
|
||||
onFilter(filter) {
|
||||
this.loadUsers(filter);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -30,12 +30,12 @@ export default Route.extend(AuthenticatedRouteMixin, {
|
|||
return new EmberPromise((resolve) => {
|
||||
if (this.get('appMeta.authProvider') == constants.AuthProvider.Keycloak) {
|
||||
this.get('global').syncExternalUsers().then(() => {
|
||||
this.get('userService').getComplete().then((users) =>{
|
||||
this.get('userService').getComplete('').then((users) =>{
|
||||
resolve(users);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.get('userService').getComplete().then((users) => {
|
||||
this.get('userService').getComplete('').then((users) => {
|
||||
resolve(users);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="view-customize">
|
||||
<h1 class="admin-heading">Users</h1>
|
||||
<h2 class="sub-heading">Set basic information, passwords and permissions for {{model.length}} users</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{customize/user-admin users=model
|
||||
onAddUser=(action 'onAddUser')
|
||||
onAddUsers=(action 'onAddUsers')}}
|
||||
|
||||
{{customize/user-settings add=(action 'add')}}
|
||||
|
||||
{{customize/user-admin users=model onDelete=(action "onDelete") onSave=(action "onSave") onPassword=(action "onPassword")}}
|
||||
{{customize/user-list users=model
|
||||
onFilter=(action "onFilter")
|
||||
onDelete=(action "onDelete")
|
||||
onSave=(action "onSave")
|
||||
onPassword=(action "onPassword")}}
|
||||
|
|
|
@ -26,6 +26,9 @@ export default Controller.extend(TooltipMixin, {
|
|||
actions: {
|
||||
onTabChange(tab) {
|
||||
this.set('tab', tab);
|
||||
if (tab === 'content') {
|
||||
this.send('refresh');
|
||||
}
|
||||
},
|
||||
|
||||
onShowPage(pageId) {
|
||||
|
|
|
@ -16,7 +16,8 @@ export default Route.extend({
|
|||
beforeModel() {
|
||||
let pwd = document.head.querySelector("[property=dbhash]").content;
|
||||
if (pwd.length === 0 || pwd === "{{.DBhash}}") {
|
||||
this.transitionTo('auth.login'); // don't allow access to this page if we are not in setup mode, kick them out altogether
|
||||
// don't allow access to this page if we are not in setup mode
|
||||
this.transitionTo('auth.login');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -50,6 +50,9 @@ export default Router.map(function () {
|
|||
this.route('general', {
|
||||
path: 'general'
|
||||
});
|
||||
this.route('groups', {
|
||||
path: 'groups'
|
||||
});
|
||||
this.route('users', {
|
||||
path: 'users'
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ export default ApplicationSerializer.extend({
|
|||
normalize(modelClass, resourceHash) {
|
||||
return {
|
||||
data: {
|
||||
id: resourceHash.userId ? resourceHash.userId : 0,
|
||||
id: resourceHash.whoId ? resourceHash.whoId : 0,
|
||||
type: modelClass.modelName,
|
||||
attributes: resourceHash
|
||||
}
|
13
gui/app/serializers/document-permission.js
Normal file
13
gui/app/serializers/document-permission.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
normalize(modelClass, resourceHash) {
|
||||
return {
|
||||
data: {
|
||||
id: resourceHash.whoId ? resourceHash.whoId : 0,
|
||||
type: modelClass.modelName,
|
||||
attributes: resourceHash
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
|
@ -4,7 +4,7 @@ export default ApplicationSerializer.extend({
|
|||
normalize(modelClass, resourceHash) {
|
||||
return {
|
||||
data: {
|
||||
id: resourceHash.userId ? resourceHash.userId : 0,
|
||||
id: resourceHash.whoId ? resourceHash.whoId : 0,
|
||||
type: modelClass.modelName,
|
||||
attributes: resourceHash
|
||||
}
|
||||
|
|
|
@ -87,7 +87,15 @@ export default BaseService.extend({
|
|||
return this.get('ajax').request(`category/${categoryId}/permission`, {
|
||||
method: 'GET'
|
||||
}).then((response) => {
|
||||
return response;
|
||||
// return response;
|
||||
let data = [];
|
||||
|
||||
data = response.map((obj) => {
|
||||
let data = this.get('store').normalize('category-permission', obj);
|
||||
return this.get('store').push(data);
|
||||
});
|
||||
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -343,7 +343,7 @@ export default Service.extend({
|
|||
perms = this.get('store').push(perms);
|
||||
this.get('folderService').set('permissions', perms);
|
||||
|
||||
let roles = this.get('store').normalize('document-role', response.roles);
|
||||
let roles = this.get('store').normalize('document-permission', response.roles);
|
||||
roles = this.get('store').push(roles);
|
||||
|
||||
let folders = response.folders.map((obj) => {
|
||||
|
|
|
@ -20,6 +20,7 @@ export default BaseService.extend({
|
|||
localStorage: service(),
|
||||
store: service(),
|
||||
currentFolder: null,
|
||||
permissions: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
|
98
gui/app/services/group.js
Normal file
98
gui/app/services/group.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import { inject as service } from '@ember/service';
|
||||
import BaseService from '../services/base';
|
||||
|
||||
export default BaseService.extend({
|
||||
sessionService: service('session'),
|
||||
ajax: service(),
|
||||
localStorage: service(),
|
||||
store: service(),
|
||||
|
||||
// Add user group.
|
||||
add(payload) {
|
||||
return this.get('ajax').post(`group`, {
|
||||
contentType: 'json',
|
||||
data: JSON.stringify(payload)
|
||||
}).then((group) => {
|
||||
let data = this.get('store').normalize('group', group);
|
||||
return this.get('store').push(data);
|
||||
});
|
||||
},
|
||||
|
||||
// Returns all groups for org.
|
||||
getAll() {
|
||||
return this.get('ajax').request(`group`, {
|
||||
method: 'GET'
|
||||
}).then((response) => {
|
||||
let data = [];
|
||||
|
||||
data = response.map((obj) => {
|
||||
let data = this.get('store').normalize('group', obj);
|
||||
return this.get('store').push(data);
|
||||
});
|
||||
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
// Updates an existing group.
|
||||
update(group) {
|
||||
let id = group.get('id');
|
||||
|
||||
return this.get('ajax').request(`group/${id}`, {
|
||||
method: 'PUT',
|
||||
contentType: 'json',
|
||||
data: JSON.stringify(group)
|
||||
}).then((group) => {
|
||||
let data = this.get('store').normalize('group', group);
|
||||
return this.get('store').push(data);
|
||||
});
|
||||
},
|
||||
|
||||
// Delete removes group and associated user membership.
|
||||
delete(groupId) {
|
||||
return this.get('ajax').request(`group/${groupId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
},
|
||||
|
||||
// Returns users associated with given group
|
||||
getGroupMembers(groupId) {
|
||||
return this.get('ajax').request(`group/${groupId}/members`, {
|
||||
method: 'GET'
|
||||
}).then((response) => {
|
||||
let data = [];
|
||||
|
||||
data = response.map((obj) => {
|
||||
let data = this.get('store').normalize('group-member', obj);
|
||||
return this.get('store').push(data);
|
||||
});
|
||||
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
// join adds user to group.
|
||||
join(groupId, userId) {
|
||||
return this.get('ajax').request(`group/${groupId}/join/${userId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
},
|
||||
|
||||
// leave removes user from group.
|
||||
leave(groupId, userId) {
|
||||
return this.get('ajax').request(`group/${groupId}/leave/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
},
|
||||
});
|
|
@ -31,6 +31,16 @@ export default Service.extend({
|
|||
});
|
||||
},
|
||||
|
||||
// Adds comma-delim list of users (firstname, lastname, email).
|
||||
addBulk(list) {
|
||||
return this.get('ajax').request(`users/import`, {
|
||||
type: 'POST',
|
||||
data: list,
|
||||
contentType: 'text'
|
||||
}).then(() => {
|
||||
});
|
||||
},
|
||||
|
||||
// Returns user model for specified user id.
|
||||
getUser(userId) {
|
||||
let url = `users/${userId}`;
|
||||
|
@ -54,8 +64,13 @@ export default Service.extend({
|
|||
},
|
||||
|
||||
// Returns all active and inactive users for organization.
|
||||
getComplete() {
|
||||
return this.get('ajax').request(`users?active=0`).then((response) => {
|
||||
// Only available for admins and limits results to max. 100 users.
|
||||
// Takes filter for user search criteria.
|
||||
getComplete(filter) {
|
||||
filter = filter.trim();
|
||||
if (filter.length > 0) filter = encodeURIComponent(filter);
|
||||
|
||||
return this.get('ajax').request(`users?active=0&filter=${filter}`).then((response) => {
|
||||
return response.map((obj) => {
|
||||
let data = this.get('store').normalize('user', obj);
|
||||
return this.get('store').push(data);
|
||||
|
@ -145,5 +160,24 @@ export default Service.extend({
|
|||
method: "POST",
|
||||
data: password
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// matchUsers on firstname, lastname, email
|
||||
matchUsers(text) {
|
||||
return this.get('ajax').request('users/match', {
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'text',
|
||||
data: text
|
||||
}).then((response) => {
|
||||
let data = [];
|
||||
|
||||
data = response.map((obj) => {
|
||||
let data = this.get('store').normalize('user', obj);
|
||||
return this.get('store').push(data);
|
||||
});
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
4
gui/app/styles/bootstrap.scss
vendored
4
gui/app/styles/bootstrap.scss
vendored
|
@ -106,3 +106,7 @@ $link-hover-decoration: none;
|
|||
.modal-80 {
|
||||
max-width: 80% !important;
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,9 @@ $color-stroke: #e1e1e1;
|
|||
.color-gray {
|
||||
color: $color-gray !important;
|
||||
}
|
||||
.color-gold {
|
||||
color: $color-goldy !important;
|
||||
}
|
||||
|
||||
.background-color-white {
|
||||
background-color: $color-white !important;
|
||||
|
|
|
@ -11,16 +11,37 @@
|
|||
}
|
||||
|
||||
.user-table {
|
||||
.name {
|
||||
font-size: 1rem;
|
||||
color: $color-off-black;
|
||||
margin: 0 0 0 30px;
|
||||
tbody tr td, thead tr th {
|
||||
border-top: none !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.email {
|
||||
font-size: 0.9rem;
|
||||
.name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: $color-link;
|
||||
margin: 0 0 0 10px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
> .email {
|
||||
font-size: 0.9rem;
|
||||
color: $color-off-black;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.groups {
|
||||
cursor: pointer;
|
||||
margin: 5px 0 0 10px;
|
||||
font-size: 1rem;
|
||||
color: $color-gray;
|
||||
margin: 0 0 0 30px;
|
||||
|
||||
&:hover {
|
||||
color: $color-link;
|
||||
}
|
||||
}
|
||||
|
||||
.inactive-user
|
||||
|
@ -55,4 +76,64 @@
|
|||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
> .groups-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
> .group {
|
||||
margin: 15px 0;
|
||||
|
||||
.name {
|
||||
font-size: 1.2rem;
|
||||
color: $color-off-black;
|
||||
|
||||
> .purpose {
|
||||
font-size: 1rem;
|
||||
color: $color-gray;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// used for group admin
|
||||
> .group-users-members {
|
||||
> .item {
|
||||
margin: 10px 0;
|
||||
|
||||
> .fullname {
|
||||
color: $color-primary;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// used for user admin
|
||||
> .group-membership {
|
||||
> .item {
|
||||
margin: 10px 0;
|
||||
|
||||
> .group-name {
|
||||
color: $color-primary;
|
||||
font-size: 1.2rem;
|
||||
|
||||
> .group-purpose {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .smtp-failure {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: $color-red;
|
||||
}
|
||||
|
||||
> .smtp-success {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: $color-green;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.wysiwyg {
|
||||
font-size: 17px;
|
||||
line-height: 30px;
|
||||
line-height: 25px;
|
||||
// line-height: 30px;
|
||||
color: $color-off-black;
|
||||
|
||||
table {
|
||||
|
|
|
@ -10,40 +10,82 @@
|
|||
<div class="view-customize">
|
||||
<form class="mt-5">
|
||||
<div class="form-group row">
|
||||
<label for="smtp-host" class="col-sm-2 col-form-label">Host</label>
|
||||
<div class="col-sm-10">
|
||||
<label for="smtp-host" class="col-sm-4 col-form-label">Host</label>
|
||||
<div class="col-sm-8">
|
||||
{{focus-input id="smtp-host" type="text" value=model.smtp.host class=(if SMTPHostEmptyError 'form-control is-invalid' 'form-control')}}
|
||||
<small class="form-text text-muted">e.g. my.host.com</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="smtp-port" class="col-sm-2 col-form-label">Port</label>
|
||||
<div class="col-sm-10">
|
||||
<label for="smtp-port" class="col-sm-4 col-form-label">Port</label>
|
||||
<div class="col-sm-8">
|
||||
{{input id="smtp-port" type="text" value=model.smtp.port class=(if SMTPPortEmptyError 'form-control is-invalid' 'form-control')}}
|
||||
<small class="form-text text-muted">e.g. 587</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="smtp-sender" class="col-sm-2 col-form-label">Sender</label>
|
||||
<div class="col-sm-10">
|
||||
{{input id="smtp-sender" type="text" value=model.smtp.sender class=(if SMTPSenderEmptyError 'form-control is-invalid' 'form-control')}}
|
||||
<small class="form-text text-muted">e.g. user@some-domain.com</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="smtp-userid" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-10">
|
||||
{{input id="smtp-userid" type="text" value=model.smtp.userid class=(if SMTPUserIdEmptyError 'form-control is-invalid' 'form-control')}}
|
||||
<label for="smtp-userid" class="col-sm-4 col-form-label">Username</label>
|
||||
<div class="col-sm-8">
|
||||
{{input id="smtp-userid" type="text" value=model.smtp.userid class='form-control'}}
|
||||
<small class="form-text text-muted">e.g. Login username for SMTP server</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="smtp-password" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
{{input id="smtp-password" type="password" value=model.smtp.password class=(if SMTPPasswordEmptyError 'form-control is-invalid' 'form-control')}}
|
||||
<label for="smtp-password" class="col-sm-4 col-form-label">Password</label>
|
||||
<div class="col-sm-8">
|
||||
{{input id="smtp-password" type="password" value=model.smtp.password class='form-control'}}
|
||||
<small class="form-text text-muted">e.g. Login password for SMTP server</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-success mt-4" {{action 'saveSMTP'}}>Save</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="smtp-sender" class="col-sm-4 col-form-label">Sender Email</label>
|
||||
<div class="col-sm-8">
|
||||
{{input id="smtp-sender" type="email" value=model.smtp.sender class=(if SMTPSenderEmptyError 'form-control is-invalid' 'form-control')}}
|
||||
<small class="form-text text-muted">e.g. user@some-domain.com</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="smtp-senderName" class="col-sm-4 col-form-label">Sender Name</label>
|
||||
<div class="col-sm-8">
|
||||
{{input id="smtp-senderName" type="text" value=model.smtp.senderName class=(if senderNameError 'form-control is-invalid' 'form-control')}}
|
||||
<small class="form-text text-muted">e.g. Documize</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Anonymous</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check">
|
||||
{{input id="smtp-anonymous" type="checkbox" checked=model.smtp.anonymous class='form-check-input'}}
|
||||
<label class="form-check-label" for="smtp-anonymous">Anonymous authentication, ignore Username and Password fields</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Base64</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check">
|
||||
{{input id="smtp-base64creds" type="checkbox" checked=model.smtp.base64creds class='form-check-input'}}
|
||||
<label class="form-check-label" for="smtp-base64creds">Base64 encode Username and Password fields</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">SSL</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check">
|
||||
{{input id="smtp-usessl" type="checkbox" checked=model.smtp.usessl class='form-check-input'}}
|
||||
<label class="form-check-label" for="smtp-usessl">Use SSL</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn btn-success mt-4" {{action 'saveSMTP'}}>{{buttonText}}</div>
|
||||
</form>
|
||||
{{#if testSMTP.success}}
|
||||
<p class="smtp-success my-3">{{testSMTP.message}}</p>
|
||||
{{else}}
|
||||
<p class="smtp-failure my-3">{{testSMTP.message}}</p>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -1,153 +1,56 @@
|
|||
<div class="view-customize mb-5">
|
||||
<h3>Users</h3>
|
||||
<table class="table table-hover table-responsive user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{input type="text" class="form-control" placeholder="filter users" value=filter}}</th>
|
||||
<th class="no-width">Add Space</th>
|
||||
<th class="no-width">View Users</th>
|
||||
<th class="no-width">Admin</th>
|
||||
<th class="no-width">Active</th>
|
||||
<th class="no-width">
|
||||
{{#if hasSelectedUsers}}
|
||||
<button id="bulk-delete-users" type="button" class="btn btn-danger btn-sm" data-toggle="modal" data-target="#admin-user-delete-modal" data-backdrop="static">Delete</button>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="view-customize">
|
||||
<h1 class="admin-heading">Users</h1>
|
||||
<h2 class="sub-heading">Set basic information, passwords and permissions for {{users.length}} users</h2>
|
||||
|
||||
<div id="admin-user-delete-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Delete Users</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete selected users?</p>
|
||||
{{#if isAuthProviderDocumize}}
|
||||
<div class="btn btn-success mt-3 mb-3" {{action 'onOpenUserModal'}}>Add user</div>
|
||||
<div id="add-user-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Add User</div>
|
||||
<div class="modal-body">
|
||||
<form onsubmit={{action 'onAddUser'}}>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label for="newUserFirstname">Firstname</label>
|
||||
{{input id="newUserFirstname" type="text" class="form-control" placeholder="Firstname" value=newUser.firstname}}
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="newUserLastname">Lastname</label>
|
||||
{{input id="newUserLastname" type="text" class="form-control" placeholder="Lastname" value=newUser.lastname}}
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="newUserEmail">Lastname</label>
|
||||
{{input id="newUserEmail" type="email" class="form-control" placeholder="Email" value=newUser.email}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onBulkDelete'}}>Delete</button>
|
||||
<div class="form-row">
|
||||
<div class="col mt-3 text-right">
|
||||
<button type="submit" class="btn btn-success" {{action 'onAddUser'}}>Add user</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form onsubmit={{action 'onAddUser'}}>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-desc">Bulk create users</label>
|
||||
{{textarea id="bulkUsers" value=bulkUsers class="form-control" rows="5" placeholder="firstname, lastname, email"}}
|
||||
<small class="form-text text-muted">Comma-delimited list: firstname, lastname, email</small>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn btn-success" {{action 'onAddUsers'}}>Add users</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer mt-4">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each filteredUsers key="id" as |user|}}
|
||||
<tr>
|
||||
<td class="{{unless user.active 'inactive-user'}} {{if user.admin 'admin-user'}}">
|
||||
<div class="d-inline-block align-top">
|
||||
{{#if user.me}}
|
||||
<i class="material-icons color-gray">check_box_outline_blank</i>
|
||||
{{else if user.selected}}
|
||||
<i class="material-icons checkbox" {{action 'toggleSelect' user}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleSelect' user}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="d-inline-block">
|
||||
<div class="name d-inline-block">{{ user.fullname }}</div>
|
||||
<div class="email">{{ user.email }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.editor}}
|
||||
<i class="material-icons checkbox" {{action 'toggleEditor' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleEditor' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.viewUsers}}
|
||||
<i class="material-icons checkbox" {{action 'toggleUsers' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleUsers' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.me}}
|
||||
<i class="material-icons color-gray">check_box</i>
|
||||
{{else if user.admin}}
|
||||
<i class="material-icons checkbox" {{action 'toggleAdmin' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleAdmin' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.me}}
|
||||
<i class="material-icons color-gray">check_box</i>
|
||||
{{else if user.active}}
|
||||
<i class="material-icons checkbox" {{action 'toggleActive' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleActive' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.me}}
|
||||
<div class="edit-button-{{user.id}} button-icon-gray" title="Edit" {{action "onShowEdit" user.id}}>
|
||||
<i class="material-icons">edit</i>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="edit-button-{{user.id}} button-icon-gray" title="Edit" {{action "onShowEdit" user.id}}>
|
||||
<i class="material-icons">edit</i>
|
||||
</div>
|
||||
<div class="button-icon-gap"></div>
|
||||
<div class="delete-button-{{user.id}} button-icon-danger" title="Delete" {{action "onShowDelete" user.id}}>
|
||||
<i class="material-icons">delete</i>
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div id="edit-user-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">User {{editUser.firstname}} {{editUser.lastname}}</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="edit-firstname">Firstname</label>
|
||||
{{input id="edit-firstname" class="form-control" type="text" value=editUser.firstname}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-lastname">Lastname</label>
|
||||
{{input id="edit-lastname" type="text" class="form-control" value=editUser.lastname}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-email">Email</label>
|
||||
{{input id="edit-email" type="text" class="form-control" value=editUser.email}}
|
||||
</div>
|
||||
{{#if isAuthProviderDocumize}}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-password">Password</label>
|
||||
<div class="tip">Optional new password</div>
|
||||
{{input id="edit-password" type="password" class="form-control" value=password.password}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-confirmPassword">Confirm Password</label>
|
||||
<div class="tip">Confirm new password</div>
|
||||
{{input id="edit-confirmPassword" type="password" class="form-control" value=password.confirmation}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onUpdate'}}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#ui/ui-dialog title="Delete User" confirmCaption="Delete" buttonType="btn-danger" show=showDeleteDialog onAction=(action 'onDelete')}}
|
||||
<p>Are you sure you want to delete {{deleteUser.fullname}}?</p>
|
||||
{{/ui/ui-dialog}}
|
||||
|
||||
|
|
158
gui/app/templates/components/customize/user-groups.hbs
Normal file
158
gui/app/templates/components/customize/user-groups.hbs
Normal file
|
@ -0,0 +1,158 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="view-customize">
|
||||
|
||||
<h1 class="admin-heading">Groups</h1>
|
||||
<h2 class="sub-heading">Create groups for easier user management — assign users to groups</h2>
|
||||
<div class="btn btn-success mt-3 mb-3" {{action 'onOpenGroupModal'}}>Add group</div>
|
||||
|
||||
<div id="add-group-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Add Group</div>
|
||||
<div class="modal-body">
|
||||
<form onsubmit={{action 'onAddGroup'}}>
|
||||
<div class="form-group">
|
||||
<label for="new-group-name">Name</label>
|
||||
{{focus-input id="new-group-name" type="text" class="form-control mousetrap" placeholder="Enter group name" value=newGroup.name}}
|
||||
<small class="form-text text-muted">e.g. Managers, Developers, Acme Team</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-group-desc">Description (optional)</label>
|
||||
{{textarea id="new-group-desc" value=newGroup.purpose class="form-control" rows="3"}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onAddGroup'}}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="groups-list">
|
||||
{{#each groups as |group|}}
|
||||
<div class="row group">
|
||||
<div class="col-8">
|
||||
<div class="name">
|
||||
{{group.name}}
|
||||
{{#if group.purpose}}
|
||||
<div class="purpose"> — {{group.purpose}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-4 buttons text-right">
|
||||
<button class="btn btn-sm btn-secondary" {{action 'onShowMembersModal' group.id}}>{{group.members}} members</button>
|
||||
<div class="button-icon-gap" />
|
||||
<div class="button-icon-gray align-middle" data-toggle="tooltip" data-placement="top" title="Rename" {{action 'onShowEditModal' group.id}}>
|
||||
<i class="material-icons">edit</i>
|
||||
</div>
|
||||
<div class="button-icon-gap" />
|
||||
<div class="button-icon-danger align-middle" data-toggle="tooltip" data-placement="top" title="Delete" {{action 'onShowDeleteModal' group.id}}>
|
||||
<i class="material-icons">delete</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="margin-top-30"><i>No groups</i></div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div id="delete-group-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Delete Group</div>
|
||||
<div class="modal-body">
|
||||
<form onsubmit={{action 'onDeleteGroup'}}>
|
||||
<p>Are you sure you want to delete this group?</p>
|
||||
<div class="form-group">
|
||||
<label for="delete-group-name">Please type group name to confirm</label>
|
||||
{{input id="delete-group-name" type="text" class="form-control mousetrap" placeholder="Group name" value=deleteGroup.name}}
|
||||
<small class="form-text text-muted">This will remove group membership information and associated permissions!</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-danger" onclick={{action 'onDeleteGroup'}}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="edit-group-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Edit Group</div>
|
||||
<div class="modal-body">
|
||||
<form onsubmit={{action 'onEditGroup'}}>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-name">Name</label>
|
||||
{{input id="edit-group-name" type="text" class="form-control mousetrap" placeholder="Enter group name" value=editGroup.name}}
|
||||
<small class="form-text text-muted">e.g. Managers, Developers, Acme Team</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-desc">Description (optional)</label>
|
||||
{{textarea id="edit-group-desc" value=editGroup.purpose class="form-control" rows="3"}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onEditGroup'}}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="group-members-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">{{membersGroup.name}} ({{members.length}})</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="group-members-search">Search for group members, non-members</label>
|
||||
{{input id="group-members-search" type="text" class="form-control mousetrap" placeholder="Search members and users..." value=searchText key-up=(action 'onSearch')}}
|
||||
<small class="form-text text-muted">search firstname, lastname, email</small>
|
||||
</div>
|
||||
<div class="view-customize">
|
||||
<div class="group-users-members my-5">
|
||||
{{#if showMembers}}
|
||||
{{#each members as |member|}}
|
||||
<div class="row item">
|
||||
<div class="col-10 fullname">{{member.fullname}}</div>
|
||||
<div class="col-2 text-right">
|
||||
<button class="btn btn-danger" {{action 'onLeaveGroup' member.userId}}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if showUsers}}
|
||||
{{#each users as |user|}}
|
||||
<div class="row item">
|
||||
<div class="col-10 fullname">{{user.firstname}} {{user.lastname}}</div>
|
||||
<div class="col-2 text-right">
|
||||
{{#if user.isMember}}
|
||||
<button class="btn btn-danger" {{action 'onLeaveGroup' user.id}}>Remove</button>
|
||||
{{else}}
|
||||
<button class="btn btn-success" {{action 'onJoinGroup' user.id}}>Add</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
212
gui/app/templates/components/customize/user-list.hbs
Normal file
212
gui/app/templates/components/customize/user-list.hbs
Normal file
|
@ -0,0 +1,212 @@
|
|||
<div class="view-customize my-5">
|
||||
|
||||
<div class="my-2">
|
||||
<span class="font-weight-bold">Spaces</span>
|
||||
<span class="text-muted"> — can add spaces, both personal and shared with others</span>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<span class="font-weight-bold">Visible</span>
|
||||
<span class="text-muted"> — can see names of users and groups, can disable for external users like customers/partners</span>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<span class="font-weight-bold">Admin</span>
|
||||
<span class="text-muted"> — can manage all aspects of Documize, like this screen</span>
|
||||
</div>
|
||||
<div class="mt-2 mb-4">
|
||||
<span class="font-weight-bold">Active</span>
|
||||
<span class="text-muted"> — can login and use Documize</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-5 mb-3">
|
||||
{{focus-input type="text" class="form-control" placeholder="filter users" value=filter}}
|
||||
<small class="form-text text-muted">search firstname, lastname, email</small>
|
||||
</div>
|
||||
|
||||
<table class="table table-responsive table-borderless user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-muted">
|
||||
{{#if hasSelectedUsers}}
|
||||
<button id="bulk-delete-users" type="button" class="btn btn-danger" data-toggle="modal" data-target="#admin-user-delete-modal" data-backdrop="static">Delete selected users</button>
|
||||
{{/if}}
|
||||
</th>
|
||||
<th class="no-width">Spaces</th>
|
||||
<th class="no-width">Visible</th>
|
||||
<th class="no-width">Admin</th>
|
||||
<th class="no-width">Active</th>
|
||||
<th class="no-width">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each users key="id" as |user|}}
|
||||
<tr>
|
||||
<td class="{{unless user.active 'inactive-user'}} {{if user.admin 'admin-user'}}">
|
||||
<div class="d-inline-block align-top">
|
||||
{{#if user.me}}
|
||||
<i class="material-icons color-gray">check_box_outline_blank</i>
|
||||
{{else if user.selected}}
|
||||
<i class="material-icons checkbox" {{action 'toggleSelect' user}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleSelect' user}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="d-inline-block">
|
||||
<div class="name" {{action "onShowEdit" user.id}}>{{user.fullname}}<div class="email"> ({{user.email}})</div></div>
|
||||
<div class="groups" {{action "onShowGroupsModal" user.id}}>
|
||||
{{#each user.groups as |group|}}
|
||||
<span class="group">
|
||||
{{group.name}}{{#if (not-eq group user.groups.lastObject)}}, {{/if}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="group"><no groups></span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.editor}}
|
||||
<i class="material-icons checkbox" {{action 'toggleEditor' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleEditor' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.viewUsers}}
|
||||
<i class="material-icons checkbox" {{action 'toggleUsers' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleUsers' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.me}}
|
||||
<i class="material-icons color-gray">check_box</i>
|
||||
{{else if user.admin}}
|
||||
<i class="material-icons checkbox" {{action 'toggleAdmin' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleAdmin' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
{{#if user.me}}
|
||||
<i class="material-icons color-gray">check_box</i>
|
||||
{{else if user.active}}
|
||||
<i class="material-icons checkbox" {{action 'toggleActive' user.id}}>check_box</i>
|
||||
{{else}}
|
||||
<i class="material-icons checkbox" {{action 'toggleActive' user.id}}>check_box_outline_blank</i>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="no-width text-center">
|
||||
<div class="user-button-{{user.id}} button-icon-gray button-icon-small" title="Edit" {{action "onShowEdit" user.id}}>
|
||||
<i class="material-icons">mode_edit</i>
|
||||
</div>
|
||||
{{#unless user.me}}
|
||||
<div class="button-icon-gap" />
|
||||
<div class="delete-button-{{user.id}} button-icon-red button-icon-small" title="Delete" {{action "onShowDelete" user.id}}>
|
||||
<i class="material-icons">delete</i>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="edit-user-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">User {{editUser.firstname}} {{editUser.lastname}}</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="edit-firstname">Firstname</label>
|
||||
{{input id="edit-firstname" class="form-control" type="text" value=editUser.firstname}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-lastname">Lastname</label>
|
||||
{{input id="edit-lastname" type="text" class="form-control" value=editUser.lastname}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-email">Email</label>
|
||||
{{input id="edit-email" type="text" class="form-control" value=editUser.email}}
|
||||
</div>
|
||||
{{#if isAuthProviderDocumize}}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-password">Password</label>
|
||||
{{input id="edit-password" type="password" class="form-control" value=password.password}}
|
||||
<small class="form-text text-muted">Optional new password</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="edit-confirmPassword">Confirm Password</label>
|
||||
{{input id="edit-confirmPassword" type="password" class="form-control" value=password.confirmation}}
|
||||
<small class="form-text text-muted">Confirm new password</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onUpdate'}}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#ui/ui-dialog title="Delete User" confirmCaption="Delete" buttonType="btn-danger" show=showDeleteDialog onAction=(action 'onDelete')}}
|
||||
<p>Are you sure you want to delete {{deleteUser.fullname}}?</p>
|
||||
{{/ui/ui-dialog}}
|
||||
|
||||
<div id="admin-user-delete-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Delete Users</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete selected users?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onBulkDelete'}}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="group-member-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">{{selectedUser.fullname}}</div>
|
||||
<div class="modal-body">
|
||||
<div class="view-customize">
|
||||
<div class="group-membership my-5">
|
||||
{{#each groups as |group|}}
|
||||
<div class="row item">
|
||||
<div class="col-10 group-name">{{group.name}}
|
||||
{{#if group.purpose}}
|
||||
<span class="text-muted group-purpose"> — {{group.purpose}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="col-2 text-right">
|
||||
{{#if group.isMember}}
|
||||
<button class="btn btn-danger" {{action 'onLeaveGroup' group.id}}>Leave</button>
|
||||
{{else}}
|
||||
<button class="btn btn-success" {{action 'onJoinGroup' group.id}}>Join</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,17 +0,0 @@
|
|||
{{#if isAuthProviderDocumize}}
|
||||
<div class="view-customize mt-5 mb-5">
|
||||
<h3>Add User</h3>
|
||||
<form class="form-inline">
|
||||
<label class="sr-only" for="newUserFirstname">Firstname</label>
|
||||
{{focus-input id="newUserFirstname" type="text" value=newUser.firstname placeholder="Firstname" class=(if hasFirstnameEmptyError 'form-control mb-2 mr-sm-4 mb-sm-0 is-invalid' 'form-control mb-2 mr-sm-4 mb-sm-0')}}
|
||||
|
||||
<label class="sr-only" for="newUserLastname">Lastname</label>
|
||||
{{input id="newUserLastname" type="text" value=newUser.lastname placeholder="Lastname" class=(if hasLastnameEmptyError 'form-control mb-2 mr-sm-4 mb-sm-0 is-invalid' 'form-control mb-2 mr-sm-4 mb-sm-0')}}
|
||||
|
||||
<label class="sr-only" for="newEmail">Email</label>
|
||||
{{input id="newEmail" type="email" value=newUser.email placeholder="Email" class=(if hasEmailEmptyError 'form-control mb-2 mr-sm-4 mb-sm-0 is-invalid' 'form-control mb-2 mr-sm-4 mb-sm-0')}}
|
||||
|
||||
<button type="submit" class="btn btn-success" {{action 'add'}}>Add</button>
|
||||
</form>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -6,7 +6,7 @@
|
|||
<p class="sub-title">Sub-divide spaces into categories which can contain documents with restricted access.</p>
|
||||
<form class="form-inline" onsubmit={{action 'onAdd'}}>
|
||||
<div class="form-group mr-3">
|
||||
{{input id="new-category-name" type='text' class="form-control mousetrap" placeholder="Category name" value=newCategory}}
|
||||
{{focus-input id="new-category-name" type='text' class="form-control mousetrap" placeholder="Category name" value=newCategory}}
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onAdd'}}>Add</button>
|
||||
</form>
|
||||
|
@ -27,7 +27,7 @@
|
|||
{{else}}
|
||||
<div class="category col-8">
|
||||
<div class="name">{{cat.category}}</div>
|
||||
<div class="info">{{cat.documents}} {{if (eq cat.documents 1) 'document' 'documents' }}, {{cat.users}} {{if (eq cat.users 1) 'person' 'people' }}</div>
|
||||
<div class="info">{{cat.documents}} {{if (eq cat.documents 1) 'document' 'documents' }} · {{cat.users}} users/groups</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="col-4 buttons text-right">
|
||||
|
@ -72,8 +72,40 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{#ui/ui-dialog title="Set Cateogory Access" confirmCaption="Save" buttonType="btn-success" show=showCategoryAccess onAction=(action 'onGrantAccess')}}
|
||||
{{#ui/ui-dialog title="Set Category Access" confirmCaption="Save" buttonType="btn-success" show=showCategoryAccess onAction=(action 'onGrantAccess')}}
|
||||
<p>Select who can view documents within category</p>
|
||||
{{ui/ui-list-picker items=categoryUsers nameField='fullname' singleSelect=false}}
|
||||
|
||||
<div class="widget-list-picker">
|
||||
<ul class="options">
|
||||
{{#each categoryPermissions as |permission|}}
|
||||
<li class="option {{if permission.selected 'selected'}}" {{action 'onToggle' permission}}>
|
||||
<div class="text text-truncate">
|
||||
{{#if (eq permission.who "role")}}
|
||||
<span class="button-icon-gray button-icon-small align-middle">
|
||||
<i class="material-icons">people</i>
|
||||
</span>
|
||||
{{else}}
|
||||
{{#if (eq permission.whoId constants.EveryoneUserId)}}
|
||||
<span class="button-icon-gray button-icon-small align-middle">
|
||||
<i class="material-icons">language</i>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="button-icon-gray button-icon-small align-middle">
|
||||
<i class="material-icons">person</i>
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{permission.name}}
|
||||
{{#if (eq permission.whoId session.user.id)}}
|
||||
<small class="form-text text-muted d-inline-block">(you)</small>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if permission.selected}}
|
||||
<i class="material-icons">check</i>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/ui/ui-dialog}}
|
||||
</div>
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">Space Permissions</div>
|
||||
<div class="modal-body" style="overflow-x: auto;">
|
||||
|
||||
<div class="space-admin table-responsive">
|
||||
<table class="table table-hover permission-table">
|
||||
|
||||
<table class="table table-hover permission-table mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
|
@ -27,45 +27,89 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each permissions as |permission|}}
|
||||
{{#each spacePermissions as |permission|}}
|
||||
<tr>
|
||||
<td>{{permission.fullname}} {{if (eq permission.userId session.user.id) '(you)'}}</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'space-role-view-' permission.userId) checked=permission.spaceView}}
|
||||
{{#if (eq permission.who "role")}}
|
||||
<span class="button-icon-blue button-icon-small align-middle">
|
||||
<i class="material-icons">people</i>
|
||||
</span>
|
||||
<span class="color-blue"> {{permission.name}}
|
||||
<small class="form-text text-muted d-inline-block">({{permission.members}})</small>
|
||||
</span>
|
||||
{{else}}
|
||||
{{#if (eq permission.whoId constants.EveryoneUserId)}}
|
||||
<span class="button-icon-green button-icon-small align-middle">
|
||||
<i class="material-icons">language</i>
|
||||
</span>
|
||||
<span class="color-green"> {{permission.name}}</span>
|
||||
{{else}}
|
||||
<span class="button-icon-gray button-icon-small align-middle">
|
||||
<i class="material-icons">person</i>
|
||||
</span>
|
||||
<span class=""> {{permission.name}}
|
||||
{{#if (eq permission.whoId session.user.id)}}
|
||||
<small class="form-text text-muted d-inline-block">(you)</small>
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'space-role-manage-' permission.userId) checked=permission.spaceManage}}
|
||||
{{input type="checkbox" id=(concat 'space-role-view-' permission.whoId) checked=permission.spaceView}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'space-role-owner-' permission.userId) checked=permission.spaceOwner}}
|
||||
{{input type="checkbox" id=(concat 'space-role-manage-' permission.whoId) checked=permission.spaceManage}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-add-' permission.userId) checked=permission.documentAdd}}
|
||||
{{input type="checkbox" id=(concat 'space-role-owner-' permission.whoId) checked=permission.spaceOwner}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-edit-' permission.userId) checked=permission.documentEdit}}
|
||||
{{input type="checkbox" id=(concat 'doc-role-add-' permission.whoId) checked=permission.documentAdd}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-delete-' permission.userId) checked=permission.documentDelete}}
|
||||
{{input type="checkbox" id=(concat 'doc-role-edit-' permission.whoId) checked=permission.documentEdit}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-move-' permission.userId) checked=permission.documentMove}}
|
||||
{{input type="checkbox" id=(concat 'doc-role-delete-' permission.whoId) checked=permission.documentDelete}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-copy-' permission.userId) checked=permission.documentCopy}}
|
||||
{{input type="checkbox" id=(concat 'doc-role-move-' permission.whoId) checked=permission.documentMove}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-template-' permission.userId) checked=permission.documentTemplate}}
|
||||
{{input type="checkbox" id=(concat 'doc-role-copy-' permission.whoId) checked=permission.documentCopy}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-approve-' permission.userId) checked=permission.documentApprove}}
|
||||
{{input type="checkbox" id=(concat 'doc-role-template-' permission.whoId) checked=permission.documentTemplate}}
|
||||
</td>
|
||||
<td>
|
||||
{{input type="checkbox" id=(concat 'doc-role-approve-' permission.whoId) checked=permission.documentApprove}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
{{focus-input id="user-search" type="text" class="form-control mousetrap" placeholder="Search users..." value=searchText key-up=(action 'onSearch')}}
|
||||
<small class="form-text text-muted">firstname, lastname, email</small>
|
||||
</div>
|
||||
{{#each filteredUsers as |user|}}
|
||||
<div class="row my-3">
|
||||
<div class="col-10">{{user.fullname}}</div>
|
||||
<div class="col-2 text-right">
|
||||
<button class="btn btn-primary" {{action 'onAdd' user}}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<div class="tip">Gemini Username</div>
|
||||
{{input id="gemini-username" type="text" value=config.username class="form-control"}}
|
||||
<small class="form-text text-muted">Gemini Username</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gemini-apikey">API Key</label>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "documize",
|
||||
"version": "1.57.3",
|
||||
"version": "1.58.0",
|
||||
"description": "The Document IDE",
|
||||
"private": true,
|
||||
"repository": "",
|
||||
|
|
12
meta.json
12
meta.json
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"community":
|
||||
{
|
||||
"version": "1.57.3",
|
||||
"version": "1.58.0",
|
||||
"major": 1,
|
||||
"minor": 57,
|
||||
"patch": 3
|
||||
"minor": 58,
|
||||
"patch": 0
|
||||
},
|
||||
"enterprise":
|
||||
{
|
||||
"version": "1.59.3",
|
||||
"version": "1.60.0",
|
||||
"major": 1,
|
||||
"minor": 59,
|
||||
"patch": 3
|
||||
"minor": 60,
|
||||
"patch": 0
|
||||
}
|
||||
}
|
|
@ -74,4 +74,9 @@ const (
|
|||
EventTypeCategoryUpdate EventType = "updated-category"
|
||||
EventTypeCategoryLink EventType = "linked-category"
|
||||
EventTypeCategoryUnlink EventType = "unlinked-category"
|
||||
EventTypeGroupAdd EventType = "added-group"
|
||||
EventTypeGroupDelete EventType = "removed-group"
|
||||
EventTypeGroupUpdate EventType = "updated-group"
|
||||
EventTypeGroupJoin EventType = "joined-group"
|
||||
EventTypeGroupLeave EventType = "left-group"
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue