diff --git a/changelog/unreleased/siteacc-ext.md b/changelog/unreleased/siteacc-ext.md
new file mode 100644
index 0000000000..ca36b67f0e
--- /dev/null
+++ b/changelog/unreleased/siteacc-ext.md
@@ -0,0 +1,11 @@
+Enhancement: Site account extensions
+
+This PR heavily extends the site accounts service:
+* Extended the accounts information (not just email and name)
+* Accounts now have a password
+* Users can now "log in" to their accounts and edit it
+* Ability to grant access to the GOCDB
+
+Furthermore, these accounts can now be used to authenticate for logging in to our customized GOCDB. More use cases for these accounts are also planned.
+
+https://github.com/cs3org/reva/pull/2008
diff --git a/docs/content/en/docs/config/http/services/siteacc/_index.md b/docs/content/en/docs/config/http/services/siteacc/_index.md
index d56f901c0f..6de927d5c8 100644
--- a/docs/content/en/docs/config/http/services/siteacc/_index.md
+++ b/docs/content/en/docs/config/http/services/siteacc/_index.md
@@ -19,28 +19,45 @@ prefix = "/siteacc"
{{< /highlight >}}
{{% /dir %}}
-{{% dir name="enable_registration_form" type="string" default="false" %}}
-If set to true, the service will expose a simple form for account registration.
+## GOCDB settings
+{{% dir name="url" type="string" default="" %}}
+The external URL of the central GOCDB instance.
+{{< highlight toml >}}
+[http.services.siteacc.gocdb]
+url = "https://www.sciencemesh.eu/gocdb/"
+{{< /highlight >}}
+{{% /dir %}}
+{{% dir name="write_url" type="string" default="" %}}
+The external URL of the GOCDB Write API.
{{< highlight toml >}}
-[http.services.siteacc]
-enable_registration_form = true
+[http.services.siteacc.gocdb]
+write_url = "https://www.sciencemesh.eu/gocdbpi/"
+{{< /highlight >}}
+{{% /dir %}}
+
+{{% dir name="apikey" type="string" default="" %}}
+The API key for the GOCDB.
+{{< highlight toml >}}
+[http.services.siteacc.gocdb]
+apikey = "verysecret"
{{< /highlight >}}
{{% /dir %}}
+## Email settings
{{% dir name="notifications_mail" type="string" default="" %}}
An email address where all notifications are sent to.
{{< highlight toml >}}
-[http.services.siteacc]
+[http.services.siteacc.email]
notifications_mail = "notify@example.com"
{{< /highlight >}}
{{% /dir %}}
-## SMTP settings
+### SMTP settings
{{% dir name="sender_mail" type="string" default="" %}}
An email address from which all emails are sent.
{{< highlight toml >}}
-[http.services.siteacc.smtp]
+[http.services.siteacc.email.smtp]
sender_mail = "notify@example.com"
{{< /highlight >}}
{{% /dir %}}
@@ -48,7 +65,7 @@ sender_mail = "notify@example.com"
{{% dir name="sender_login" type="string" default="" %}}
The login name.
{{< highlight toml >}}
-[http.services.siteacc.smtp]
+[http.services.siteacc.email.smtp]
sender_login = "hans"
{{< /highlight >}}
{{% /dir %}}
@@ -56,7 +73,7 @@ sender_login = "hans"
{{% dir name="sender_password" type="string" default="" %}}
The password for the login.
{{< highlight toml >}}
-[http.services.siteacc.smtp]
+[http.services.siteacc.email.smtp]
password = "secret"
{{< /highlight >}}
{{% /dir %}}
@@ -64,7 +81,7 @@ password = "secret"
{{% dir name="smtp_server" type="string" default="" %}}
The SMTP server to use.
{{< highlight toml >}}
-[http.services.siteacc.smtp]
+[http.services.siteacc.email.smtp]
smtp_server = "smtp.example.com"
{{< /highlight >}}
{{% /dir %}}
@@ -72,7 +89,7 @@ smtp_server = "smtp.example.com"
{{% dir name="smtp_port" type="int" default="25" %}}
The SMTP server port to use.
{{< highlight toml >}}
-[http.services.siteacc.smtp]
+[http.services.siteacc.email.smtp]
smtp_port = 25
{{< /highlight >}}
{{% /dir %}}
@@ -80,7 +97,7 @@ smtp_port = 25
{{% dir name="disable_auth" type="bool" default="false" %}}
Whether to disable authentication.
{{< highlight toml >}}
-[http.services.siteacc.smtp]
+[http.services.siteacc.email.smtp]
disable_auth = true
{{< /highlight >}}
{{% /dir %}}
@@ -111,3 +128,36 @@ The registration service URL.
url = "https://iop.example.com/sitereg"
{{< /highlight >}}
{{% /dir %}}
+
+## Webserver settings
+{{% dir name="url" type="string" default="" %}}
+The external URL of the site accounts service.
+{{< highlight toml >}}
+[http.services.siteacc.webserver]
+url = "https://www.sciencemesh.eu/accounts/"
+{{< /highlight >}}
+{{% /dir %}}
+
+{{% dir name="session_timeout" type="int" default="300" %}}
+The session timeout in seconds.
+{{< highlight toml >}}
+[http.services.siteacc.webserver]
+session_timeout = 600
+{{< /highlight >}}
+{{% /dir %}}
+
+{{% dir name="verify_remote_address" type="bool" default="false" %}}
+If true, sessions are only valid if they belong to the same IP. This can cause problems behind proxy servers.
+{{< highlight toml >}}
+[http.services.siteacc.webserver]
+verify_remote_address = true
+{{< /highlight >}}
+{{% /dir %}}
+
+{{% dir name="log_sessions" type="bool" default="false" %}}
+If enabled, debug information about sessions will be printed.
+{{< highlight toml >}}
+[http.services.siteacc.webserver]
+log_sessions = true
+{{< /highlight >}}
+{{% /dir %}}
diff --git a/examples/siteacc/siteacc.toml b/examples/siteacc/siteacc.toml
index 093fe58f86..cf081e748e 100644
--- a/examples/siteacc/siteacc.toml
+++ b/examples/siteacc/siteacc.toml
@@ -1,11 +1,10 @@
[http]
address = "0.0.0.0:9600"
-[http.services.siteacc]
-# If this is set to true, the service will expose a simple form for account registration
-enable_registration_form = true
-# All notification emails are sent to this email
-notifications_mail = "science.mesh@example.com"
+[http.services.siteacc.gocdb]
+url = "https://sciencemesh-test.uni-muenster.de/gocdb/"
+write_url = "https://sciencemesh-test.uni-muenster.de/gocdbpi/"
+apikey = "verysecret"
# Set up the storage driver
[http.services.siteacc.storage]
@@ -13,9 +12,18 @@ driver = "file"
[http.services.siteacc.storage.file]
file = "/var/revad/accounts.json"
+# Email related settings
+[http.services.siteacc.email]
+notifications_mail = "science.mesh@example.com"
+
# The SMTP server used for sending emails
-[http.services.siteacc.smtp]
+[http.services.siteacc.email.smtp]
sender_mail = "science.mesh@example.com"
smtp_server = "mail.example.com"
smtp_port = 25
disable_auth = true
+
+# The webserver section defines various webserver-related settings
+[http.services.siteacc.webserver]
+url = "https://sciencemesh-test.uni-muenster.de/api/accounts/"
+session_timeout = 60
diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go
index adde74160f..9132811bb1 100644
--- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go
+++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go
@@ -80,7 +80,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
return c, nil
}
-// New creates a new Public Storage Provider service.
+// New creates a new IsPublic Storage Provider service.
func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
diff --git a/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go b/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go
index a153c79396..35b7324fb1 100644
--- a/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go
+++ b/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go
@@ -128,7 +128,7 @@ func (h *Handler) Init(c *config.Config) {
h.c.Capabilities.FilesSharing.Public = &data.CapabilitiesFilesSharingPublic{}
}
- // h.c.Capabilities.FilesSharing.Public.Enabled is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.Enabled is boolean
h.c.Capabilities.FilesSharing.Public.Enabled = true
if h.c.Capabilities.FilesSharing.Public.Password == nil {
@@ -139,22 +139,22 @@ func (h *Handler) Init(c *config.Config) {
h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &data.CapabilitiesFilesSharingPublicPasswordEnforcedFor{}
}
- // h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor.ReadOnly is boolean
- // h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor.ReadWrite is boolean
- // h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor.UploadOnly is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadOnly is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadWrite is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.UploadOnly is boolean
- // h.c.Capabilities.FilesSharing.Public.Password.Enforced is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.Password.Enforced is boolean
if h.c.Capabilities.FilesSharing.Public.ExpireDate == nil {
h.c.Capabilities.FilesSharing.Public.ExpireDate = &data.CapabilitiesFilesSharingPublicExpireDate{}
}
- // h.c.Capabilities.FilesSharing.Public.ExpireDate.Enabled is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.ExpireDate.Enabled is boolean
- // h.c.Capabilities.FilesSharing.Public.SendMail is boolean
- // h.c.Capabilities.FilesSharing.Public.SocialShare is boolean
- // h.c.Capabilities.FilesSharing.Public.Upload is boolean
- // h.c.Capabilities.FilesSharing.Public.Multiple is boolean
- // h.c.Capabilities.FilesSharing.Public.SupportsUploadOnly is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.SendMail is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.SocialShare is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.Upload is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.Multiple is boolean
+ // h.c.Capabilities.FilesSharing.IsPublic.SupportsUploadOnly is boolean
if h.c.Capabilities.FilesSharing.User == nil {
h.c.Capabilities.FilesSharing.User = &data.CapabilitiesFilesSharingUser{}
diff --git a/internal/http/services/siteacc/data/account.go b/internal/http/services/siteacc/data/account.go
deleted file mode 100644
index 914d5b7e12..0000000000
--- a/internal/http/services/siteacc/data/account.go
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright 2018-2020 CERN
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this filePath except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-// In applying this license, CERN does not waive the privileges and immunities
-// granted to it by virtue of its status as an Intergovernmental Organization
-// or submit itself to any jurisdiction.
-
-package data
-
-import (
- "time"
-
- "github.com/pkg/errors"
-
- "github.com/cs3org/reva/pkg/mentix/key"
- "github.com/cs3org/reva/pkg/utils"
-)
-
-// Account represents a single site account.
-type Account struct {
- Email string `json:"email"`
- FirstName string `json:"firstName"`
- LastName string `json:"lastName"`
-
- DateCreated time.Time `json:"dateCreated"`
- DateModified time.Time `json:"dateModified"`
-
- Data AccountData `json:"data"`
-}
-
-// AccountData holds additional data for a site account.
-type AccountData struct {
- APIKey key.APIKey `json:"apiKey"`
- Authorized bool `json:"authorized"`
-}
-
-// Accounts holds an array of site accounts.
-type Accounts = []*Account
-
-// GetSiteID returns the site ID (generated from the API key) for the given account.
-func (acc *Account) GetSiteID() key.SiteIdentifier {
- if id, err := key.CalculateSiteID(acc.Data.APIKey, key.SaltFromEmail(acc.Email)); err == nil {
- return id
- }
-
- return ""
-}
-
-// Copy copies the data of the given account to this account; if copyData is true, the account data is copied as well.
-func (acc *Account) Copy(other *Account, copyData bool) error {
- if err := other.verify(); err != nil {
- return errors.Wrap(err, "unable to update account data")
- }
-
- // Manually update fields
- acc.FirstName = other.FirstName
- acc.LastName = other.LastName
-
- if copyData {
- acc.Data = other.Data
- }
-
- return nil
-}
-
-func (acc *Account) verify() error {
- if acc.Email == "" {
- return errors.Errorf("no email address provided")
- } else if !utils.IsEmailValid(acc.Email) {
- return errors.Errorf("invalid email address: %v", acc.Email)
- }
-
- if acc.FirstName == "" || acc.LastName == "" {
- return errors.Errorf("no or incomplete name provided")
- }
-
- return nil
-}
-
-// NewAccount creates a new site account.
-func NewAccount(email string, firstName, lastName string) (*Account, error) {
- t := time.Now()
-
- acc := &Account{
- Email: email,
- FirstName: firstName,
- LastName: lastName,
- DateCreated: t,
- DateModified: t,
- Data: AccountData{
- APIKey: "",
- Authorized: false,
- },
- }
-
- if err := acc.verify(); err != nil {
- return nil, err
- }
-
- return acc, nil
-}
diff --git a/internal/http/services/siteacc/panel/panel.go b/internal/http/services/siteacc/panel/panel.go
deleted file mode 100644
index ce8d6cd916..0000000000
--- a/internal/http/services/siteacc/panel/panel.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2018-2020 CERN
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-// In applying this license, CERN does not waive the privileges and immunities
-// granted to it by virtue of its status as an Intergovernmental Organization
-// or submit itself to any jurisdiction.
-
-package panel
-
-import (
- "html/template"
- "net/http"
-
- "github.com/pkg/errors"
- "github.com/rs/zerolog"
-
- "github.com/cs3org/reva/internal/http/services/siteacc/config"
- "github.com/cs3org/reva/internal/http/services/siteacc/data"
-)
-
-// Panel represents the web interface panel of the accounts service.
-type Panel struct {
- conf *config.Configuration
- log *zerolog.Logger
-
- tpl *template.Template
-}
-
-func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error {
- if conf == nil {
- return errors.Errorf("no configuration provided")
- }
- panel.conf = conf
-
- if log == nil {
- return errors.Errorf("no logger provided")
- }
- panel.log = log
-
- // Create the panel template
- panel.tpl = template.New("panel")
- if _, err := panel.tpl.Parse(panelTemplate); err != nil {
- return errors.Wrap(err, "error while parsing panel template")
- }
-
- return nil
-}
-
-// Execute generates the HTTP output of the panel and writes it to the response writer.
-func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) error {
- type TemplateData struct {
- Accounts *data.Accounts
- }
-
- tplData := TemplateData{
- Accounts: accounts,
- }
-
- return panel.tpl.Execute(w, tplData)
-}
-
-// NewPanel creates a new web interface panel.
-func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
- panel := &Panel{}
- if err := panel.initialize(conf, log); err != nil {
- return nil, errors.Wrapf(err, "unable to initialize the panel")
- }
- return panel, nil
-}
diff --git a/internal/http/services/siteacc/registration/form.go b/internal/http/services/siteacc/registration/form.go
deleted file mode 100644
index e7649e891d..0000000000
--- a/internal/http/services/siteacc/registration/form.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2018-2020 CERN
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-// In applying this license, CERN does not waive the privileges and immunities
-// granted to it by virtue of its status as an Intergovernmental Organization
-// or submit itself to any jurisdiction.
-
-package registration
-
-import (
- "html/template"
- "net/http"
-
- "github.com/pkg/errors"
- "github.com/rs/zerolog"
-
- "github.com/cs3org/reva/internal/http/services/siteacc/config"
-)
-
-// Form represents the web interface form for user account registration.
-type Form struct {
- conf *config.Configuration
- log *zerolog.Logger
-
- tpl *template.Template
-}
-
-func (form *Form) initialize(conf *config.Configuration, log *zerolog.Logger) error {
- if conf == nil {
- return errors.Errorf("no configuration provided")
- }
- form.conf = conf
-
- if log == nil {
- return errors.Errorf("no logger provided")
- }
- form.log = log
-
- // Create the form template
- form.tpl = template.New("form")
- if _, err := form.tpl.Parse(formTemplate); err != nil {
- return errors.Wrap(err, "error while parsing form template")
- }
-
- return nil
-}
-
-// Execute generates the HTTP output of the form and writes it to the response writer.
-func (form *Form) Execute(w http.ResponseWriter) error {
- type TemplateData struct {
- }
-
- tplData := TemplateData{}
-
- return form.tpl.Execute(w, tplData)
-}
-
-// NewForm creates a new web interface form.
-func NewForm(conf *config.Configuration, log *zerolog.Logger) (*Form, error) {
- form := &Form{}
- if err := form.initialize(conf, log); err != nil {
- return nil, errors.Wrapf(err, "unable to initialize the form")
- }
- return form, nil
-}
diff --git a/internal/http/services/siteacc/registration/template.go b/internal/http/services/siteacc/registration/template.go
deleted file mode 100644
index 8da584177d..0000000000
--- a/internal/http/services/siteacc/registration/template.go
+++ /dev/null
@@ -1,216 +0,0 @@
-// Copyright 2018-2020 CERN
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-// In applying this license, CERN does not waive the privileges and immunities
-// granted to it by virtue of its status as an Intergovernmental Organization
-// or submit itself to any jurisdiction.
-
-package registration
-
-const formTemplate = `
-
-
-
-
-
- ScienceMesh Account Registration
-
-
-
-
-
Welcome to the ScienceMesh Account Registration!
-
-
Fill out the form below to register for a ScienceMesh account. A confirmation email will be sent to you shortly after registration.
-
- After inspection by a ScienceMesh administrator, you will also receive an API key which can then be used in the
- ownCloud or
- Nextcloud web application.
-
-
-
-
-
-
-
- Sending registration... this should only take a moment.
-
-
- Your registration was successful! Please check your inbox for a confirmation email.
-
-
-
-
-
-
-`
diff --git a/internal/http/services/siteacc/siteacc.go b/internal/http/services/siteacc/siteacc.go
index e3f5765d39..8684964e9d 100644
--- a/internal/http/services/siteacc/siteacc.go
+++ b/internal/http/services/siteacc/siteacc.go
@@ -19,20 +19,14 @@
package siteacc
import (
- "encoding/json"
- "fmt"
- "io/ioutil"
"net/http"
- "net/url"
- "strings"
+ "github.com/cs3org/reva/pkg/siteacc"
+ "github.com/cs3org/reva/pkg/siteacc/config"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
- "github.com/cs3org/reva/internal/http/services/siteacc/config"
- "github.com/cs3org/reva/internal/http/services/siteacc/data"
- "github.com/cs3org/reva/pkg/mentix/key"
"github.com/cs3org/reva/pkg/rhttp/global"
)
@@ -44,7 +38,7 @@ type svc struct {
conf *config.Configuration
log *zerolog.Logger
- manager *Manager
+ siteacc *siteacc.SiteAccounts
}
const (
@@ -63,297 +57,12 @@ func (s *svc) Prefix() string {
// Unprotected returns all endpoints that can be queried without prior authorization.
func (s *svc) Unprotected() []string {
- // The account creation endpoint is always unprotected
- endpoints := []string{config.EndpointCreate}
-
- // If enabled, the registration registrationForm endpoint is also unprotected
- if s.conf.EnableRegistrationForm {
- endpoints = append(endpoints, config.EndpointRegistration)
- }
-
- return endpoints
+ return s.siteacc.GetPublicEndpoints()
}
// Handler serves all HTTP requests.
func (s *svc) Handler() http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- defer r.Body.Close()
-
- switch r.URL.Path {
- case config.EndpointPanel:
- s.handlePanelEndpoint(w, r)
-
- case config.EndpointRegistration:
- if s.conf.EnableRegistrationForm {
- s.handleRegistrationEndpoint(w, r)
- }
-
- default:
- s.handleRequestEndpoints(w, r)
- }
- })
-}
-
-func (s *svc) handlePanelEndpoint(w http.ResponseWriter, r *http.Request) {
- if err := s.manager.ShowPanel(w); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface panel: %v", err)))
- }
-}
-
-func (s *svc) handleRegistrationEndpoint(w http.ResponseWriter, r *http.Request) {
- if err := s.manager.ShowRegistrationForm(w); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface registration registrationForm: %v", err)))
- }
-}
-
-func (s *svc) handleRequestEndpoints(w http.ResponseWriter, r *http.Request) {
- // Allow definition of endpoints in a flexible and easy way
- type Endpoint struct {
- Path string
- Method string
- Handler func(url.Values, []byte) (interface{}, error)
- }
-
- // Every request to the accounts service results in a standardized JSON response
- type Response struct {
- Success bool `json:"success"`
- Error string `json:"error,omitempty"`
- Data interface{} `json:"data,omitempty"`
- }
-
- endpoints := []Endpoint{
- {config.EndpointGenerateAPIKey, http.MethodGet, s.handleGenerateAPIKey},
- {config.EndpointVerifyAPIKey, http.MethodGet, s.handleVerifyAPIKey},
- {config.EndpointAssignAPIKey, http.MethodPost, s.handleAssignAPIKey},
- {config.EndpointList, http.MethodGet, s.handleList},
- {config.EndpointFind, http.MethodGet, s.handleFind},
- {config.EndpointCreate, http.MethodPost, s.handleCreate},
- {config.EndpointUpdate, http.MethodPost, s.handleUpdate},
- {config.EndpointRemove, http.MethodPost, s.handleRemove},
- {config.EndpointAuthorize, http.MethodPost, s.handleAuthorize},
- {config.EndpointIsAuthorized, http.MethodGet, s.handleIsAuthorized},
- {config.EndpointUnregisterSite, http.MethodPost, s.handleUnregisterSite},
- }
-
- // The default response is an unknown endpoint (for the specified method)
- resp := Response{
- Success: false,
- Error: fmt.Sprintf("unknown endpoint %v for method %v", r.URL.Path, r.Method),
- Data: nil,
- }
-
- // Check each endpoint if it can handle the request
- for _, endpoint := range endpoints {
- if r.URL.Path == endpoint.Path && r.Method == endpoint.Method {
- body, _ := ioutil.ReadAll(r.Body)
-
- if data, err := endpoint.Handler(r.URL.Query(), body); err == nil {
- resp.Success = true
- resp.Error = ""
- resp.Data = data
- } else {
- resp.Success = false
- resp.Error = fmt.Sprintf("%v", err)
- resp.Data = nil
- }
-
- break
- }
- }
-
- // Any failure during query handling results in a bad request
- if !resp.Success {
- w.WriteHeader(http.StatusBadRequest)
- }
-
- jsonData, _ := json.MarshalIndent(&resp, "", "\t")
- _, _ = w.Write(jsonData)
-}
-
-func (s *svc) handleGenerateAPIKey(values url.Values, body []byte) (interface{}, error) {
- email := values.Get("email")
- flags := key.FlagDefault
-
- if strings.EqualFold(values.Get("isScienceMesh"), "true") {
- flags |= key.FlagScienceMesh
- }
-
- if len(email) == 0 {
- return nil, errors.Errorf("no email provided")
- }
-
- apiKey, err := key.GenerateAPIKey(key.SaltFromEmail(email), flags)
- if err != nil {
- return nil, errors.Wrap(err, "unable to generate API key")
- }
- return map[string]string{"apiKey": apiKey}, nil
-}
-
-func (s *svc) handleVerifyAPIKey(values url.Values, body []byte) (interface{}, error) {
- apiKey := values.Get("apiKey")
- email := values.Get("email")
-
- if len(apiKey) == 0 {
- return nil, errors.Errorf("no API key provided")
- }
-
- if len(email) == 0 {
- return nil, errors.Errorf("no email provided")
- }
-
- err := key.VerifyAPIKey(apiKey, key.SaltFromEmail(email))
- if err != nil {
- return nil, errors.Wrap(err, "invalid API key")
- }
- return nil, nil
-}
-
-func (s *svc) handleAssignAPIKey(values url.Values, body []byte) (interface{}, error) {
- account, err := s.unmarshalRequestData(body)
- if err != nil {
- return nil, err
- }
-
- flags := key.FlagDefault
- if _, ok := values["isScienceMesh"]; ok {
- flags |= key.FlagScienceMesh
- }
-
- // Assign a new API key to the account through the account manager
- if err := s.manager.AssignAPIKeyToAccount(account, flags); err != nil {
- return nil, errors.Wrap(err, "unable to assign API key")
- }
-
- return nil, nil
-}
-
-func (s *svc) handleList(values url.Values, body []byte) (interface{}, error) {
- return s.manager.CloneAccounts(), nil
-}
-
-func (s *svc) handleFind(values url.Values, body []byte) (interface{}, error) {
- account, err := s.findAccount(values.Get("by"), values.Get("value"))
- if err != nil {
- return nil, err
- }
- return map[string]interface{}{"account": account}, nil
-}
-
-func (s *svc) handleCreate(values url.Values, body []byte) (interface{}, error) {
- account, err := s.unmarshalRequestData(body)
- if err != nil {
- return nil, err
- }
-
- // Create a new account through the account manager
- if err := s.manager.CreateAccount(account); err != nil {
- return nil, errors.Wrap(err, "unable to create account")
- }
-
- return nil, nil
-}
-
-func (s *svc) handleUpdate(values url.Values, body []byte) (interface{}, error) {
- account, err := s.unmarshalRequestData(body)
- if err != nil {
- return nil, err
- }
-
- // Update the account through the account manager; only the basic data of an account can be updated through this endpoint
- if err := s.manager.UpdateAccount(account, false); err != nil {
- return nil, errors.Wrap(err, "unable to update account")
- }
-
- return nil, nil
-}
-
-func (s *svc) handleRemove(values url.Values, body []byte) (interface{}, error) {
- account, err := s.unmarshalRequestData(body)
- if err != nil {
- return nil, err
- }
-
- // Remove the account through the account manager
- if err := s.manager.RemoveAccount(account); err != nil {
- return nil, errors.Wrap(err, "unable to remove account")
- }
-
- return nil, nil
-}
-
-func (s *svc) handleIsAuthorized(values url.Values, body []byte) (interface{}, error) {
- account, err := s.findAccount(values.Get("by"), values.Get("value"))
- if err != nil {
- return nil, err
- }
- return account.Data.Authorized, nil
-}
-
-func (s *svc) handleUnregisterSite(values url.Values, body []byte) (interface{}, error) {
- account, err := s.unmarshalRequestData(body)
- if err != nil {
- return nil, err
- }
-
- // Unregister the account's site through the account manager
- if err := s.manager.UnregisterAccountSite(account); err != nil {
- return nil, errors.Wrap(err, "unable to unregister the site of the given account")
- }
-
- return nil, nil
-}
-
-func (s *svc) handleAuthorize(values url.Values, body []byte) (interface{}, error) {
- account, err := s.unmarshalRequestData(body)
- if err != nil {
- return nil, err
- }
-
- if val := values.Get("status"); len(val) > 0 {
- var authorize bool
- switch strings.ToLower(val) {
- case "true":
- authorize = true
-
- case "false":
- authorize = false
-
- default:
- return nil, errors.Errorf("unsupported authorization status %v", val[0])
- }
-
- // Authorize the account through the account manager
- if err := s.manager.AuthorizeAccount(account, authorize); err != nil {
- return nil, errors.Wrap(err, "unable to (un)authorize account")
- }
- } else {
- return nil, errors.Errorf("no authorization status provided")
- }
-
- return nil, nil
-}
-
-func (s *svc) unmarshalRequestData(body []byte) (*data.Account, error) {
- account := &data.Account{}
- if err := json.Unmarshal(body, account); err != nil {
- return nil, errors.Wrap(err, "invalid account data")
- }
- return account, nil
-}
-
-func (s *svc) findAccount(by string, value string) (*data.Account, error) {
- if len(by) == 0 && len(value) == 0 {
- return nil, errors.Errorf("missing search criteria")
- }
-
- // Find the account using the account manager
- account, err := s.manager.FindAccount(by, value)
- if err != nil {
- return nil, errors.Wrap(err, "user not found")
- }
- return account, nil
+ return s.siteacc.RequestHandler()
}
func parseConfig(m map[string]interface{}) (*config.Configuration, error) {
@@ -362,6 +71,12 @@ func parseConfig(m map[string]interface{}) (*config.Configuration, error) {
return nil, errors.Wrap(err, "error decoding configuration")
}
applyDefaultConfig(conf)
+ conf.Cleanup()
+
+ if conf.Webserver.URL == "" {
+ return nil, errors.Errorf("no webserver URL specified")
+ }
+
return conf, nil
}
@@ -373,6 +88,11 @@ func applyDefaultConfig(conf *config.Configuration) {
if conf.Storage.Driver == "" {
conf.Storage.Driver = "file"
}
+
+ // Enforce a minimum session timeout of 1 minute (and default to 5 minutes)
+ if conf.Webserver.SessionTimeout < 60 {
+ conf.Webserver.SessionTimeout = 5 * 60
+ }
}
// New returns a new Site Accounts service.
@@ -383,8 +103,8 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error)
return nil, err
}
- // Create the accounts manager instance
- mngr, err := newManager(conf, log)
+ // Create the site accounts instance
+ siteacc, err := siteacc.New(conf, log)
if err != nil {
return nil, errors.Wrap(err, "error creating the site accounts service")
}
@@ -393,7 +113,7 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error)
s := &svc{
conf: conf,
log: log,
- manager: mngr,
+ siteacc: siteacc,
}
return s, nil
}
diff --git a/pkg/mentix/connectors/gocdb/query.go b/pkg/mentix/connectors/gocdb/query.go
index 9e597d5576..9ce86958c5 100755
--- a/pkg/mentix/connectors/gocdb/query.go
+++ b/pkg/mentix/connectors/gocdb/query.go
@@ -52,7 +52,6 @@ func QueryGOCDB(address string, method string, isPrivate bool, scope string, api
return nil, fmt.Errorf("unable to generate the GOCDB URL: %v", err)
}
- fmt.Println(endpointURL.String())
data, err := network.ReadEndpoint(endpointURL, nil, true)
if err != nil {
return nil, fmt.Errorf("unable to read GOCDB endpoint: %v", err)
diff --git a/pkg/mentix/utils/network/network.go b/pkg/mentix/utils/network/network.go
index 6185ce8fde..23b8ca02ed 100644
--- a/pkg/mentix/utils/network/network.go
+++ b/pkg/mentix/utils/network/network.go
@@ -54,11 +54,13 @@ func GenerateURL(host string, path string, params URLParams) (*url.URL, error) {
fullURL.Path = p.Join(fullURL.Path, path)
- query := make(url.Values)
- for key, value := range params {
- query.Set(key, value)
+ if len(params) > 0 {
+ query := make(url.Values)
+ for key, value := range params {
+ query.Set(key, value)
+ }
+ fullURL.RawQuery = query.Encode()
}
- fullURL.RawQuery = query.Encode()
return fullURL, nil
}
@@ -79,11 +81,12 @@ func queryEndpoint(method string, endpointURL *url.URL, auth *BasicAuth, checkSt
if err != nil {
return nil, fmt.Errorf("unable to get data from endpoint: %v", err)
}
+ defer resp.Body.Close()
+
if checkStatus && resp.StatusCode >= 400 {
return nil, fmt.Errorf("invalid response received: %v", resp.Status)
}
- defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
return body, nil
}
diff --git a/pkg/siteacc/account/contact/contact.go b/pkg/siteacc/account/contact/contact.go
new file mode 100644
index 0000000000..038db6a5a8
--- /dev/null
+++ b/pkg/siteacc/account/contact/contact.go
@@ -0,0 +1,51 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package contact
+
+import "github.com/cs3org/reva/pkg/siteacc/html"
+
+// PanelTemplate is the content provider for the contact form.
+type PanelTemplate struct {
+ html.ContentProvider
+}
+
+// GetTitle returns the title of the panel.
+func (template *PanelTemplate) GetTitle() string {
+ return "ScienceMesh Contact Form"
+}
+
+// GetCaption returns the caption which is displayed on the panel.
+func (template *PanelTemplate) GetCaption() string {
+ return "Contact the ScienceMesh administration"
+}
+
+// GetContentJavaScript delivers additional JavaScript code.
+func (template *PanelTemplate) GetContentJavaScript() string {
+ return tplJavaScript
+}
+
+// GetContentStyleSheet delivers additional stylesheet code.
+func (template *PanelTemplate) GetContentStyleSheet() string {
+ return tplStyleSheet
+}
+
+// GetContentBody delivers the actual body content.
+func (template *PanelTemplate) GetContentBody() string {
+ return tplBody
+}
diff --git a/pkg/siteacc/account/contact/template.go b/pkg/siteacc/account/contact/template.go
new file mode 100644
index 0000000000..9a5b76e1be
--- /dev/null
+++ b/pkg/siteacc/account/contact/template.go
@@ -0,0 +1,111 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package contact
+
+const tplJavaScript = `
+function verifyForm(formData) {
+ if (formData.getTrimmed("subject") == "") {
+ setState(STATE_ERROR, "Please enter a subject.", "form", "subject", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("message") == "") {
+ setState(STATE_ERROR, "Please enter a message.", "form", "message", true);
+ return false;
+ }
+
+ return true;
+}
+
+function handleAction(action) {
+ const formData = new FormData(document.querySelector("form"));
+ if (!verifyForm(formData)) {
+ return;
+ }
+
+ setState(STATE_STATUS, "Sending message... this should only take a moment.", "form", null, false);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "{{getServerAddress}}/" + action);
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ setState(STATE_SUCCESS, "Your message was successfully sent! A copy of the message has been sent to your email address.");
+ } else {
+ var resp = JSON.parse(this.responseText);
+ setState(STATE_ERROR, "An error occurred while trying to send your message: " + resp.error + "", "form", null, true);
+ }
+ }
+
+ var postData = {
+ "subject": formData.getTrimmed("subject"),
+ "message": formData.getTrimmed("message")
+ };
+
+ xhr.send(JSON.stringify(postData));
+}
+`
+
+const tplStyleSheet = `
+html * {
+ font-family: arial !important;
+}
+
+.mandatory {
+ color: red;
+ font-weight: bold;
+}
+`
+
+const tplBody = `
+
+
Contact the ScienceMesh administration using the form below.
+
Please include as much information as possible in your request, especially:
+
+
The site your request refers to (if not obvious from your account information)
+
Your role within the ScienceMesh site (e.g., administrator, operational team member, etc.)
+
Any specific reasons for your request
+
Anything else that might help to process your request
+`
diff --git a/pkg/siteacc/account/edit/edit.go b/pkg/siteacc/account/edit/edit.go
new file mode 100644
index 0000000000..a4d40f257c
--- /dev/null
+++ b/pkg/siteacc/account/edit/edit.go
@@ -0,0 +1,51 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package edit
+
+import "github.com/cs3org/reva/pkg/siteacc/html"
+
+// PanelTemplate is the content provider for the edit form.
+type PanelTemplate struct {
+ html.ContentProvider
+}
+
+// GetTitle returns the title of the panel.
+func (template *PanelTemplate) GetTitle() string {
+ return "ScienceMesh Account"
+}
+
+// GetCaption returns the caption which is displayed on the panel.
+func (template *PanelTemplate) GetCaption() string {
+ return "Edit your ScienceMesh Account!"
+}
+
+// GetContentJavaScript delivers additional JavaScript code.
+func (template *PanelTemplate) GetContentJavaScript() string {
+ return tplJavaScript
+}
+
+// GetContentStyleSheet delivers additional stylesheet code.
+func (template *PanelTemplate) GetContentStyleSheet() string {
+ return tplStyleSheet
+}
+
+// GetContentBody delivers the actual body content.
+func (template *PanelTemplate) GetContentBody() string {
+ return tplBody
+}
diff --git a/pkg/siteacc/account/edit/template.go b/pkg/siteacc/account/edit/template.go
new file mode 100644
index 0000000000..9cde9f4009
--- /dev/null
+++ b/pkg/siteacc/account/edit/template.go
@@ -0,0 +1,170 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package edit
+
+const tplJavaScript = `
+function verifyForm(formData) {
+ if (formData.getTrimmed("fname") == "") {
+ setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("lname") == "") {
+ setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("organization") == "") {
+ setState(STATE_ERROR, "Please specify your organization/company.", "form", "organization", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("role") == "") {
+ setState(STATE_ERROR, "Please specify your role within the organization/company.", "form", "role", true);
+ return false;
+ }
+
+ if (formData.get("password") != "") {
+ if (formData.get("password2") == "") {
+ setState(STATE_ERROR, "Please confirm your new password.", "form", "password2", true);
+ return false;
+ }
+
+ if (formData.get("password") != formData.get("password2")) {
+ setState(STATE_ERROR, "The entered passwords do not match.", "form", "password2", true);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function handleAction(action) {
+ const formData = new FormData(document.querySelector("form"));
+ if (!verifyForm(formData)) {
+ return;
+ }
+
+ setState(STATE_STATUS, "Updating account... this should only take a moment.", "form", null, false);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "{{getServerAddress}}/" + action);
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ setState(STATE_SUCCESS, "Your account was successfully updated!", "form", null, true);
+ } else {
+ var resp = JSON.parse(this.responseText);
+ setState(STATE_ERROR, "An error occurred while trying to update your account: " + resp.error + "", "form", null, true);
+ }
+ }
+
+ var postData = {
+ "title": formData.getTrimmed("title"),
+ "firstName": formData.getTrimmed("fname"),
+ "lastName": formData.getTrimmed("lname"),
+ "organization": formData.getTrimmed("organization"),
+ "website": formData.getTrimmed("website"),
+ "role": formData.getTrimmed("role"),
+ "phoneNumber": formData.getTrimmed("phone"),
+ "password": {
+ "value": formData.get("password")
+ }
+ };
+
+ xhr.send(JSON.stringify(postData));
+}
+`
+
+const tplStyleSheet = `
+html * {
+ font-family: arial !important;
+}
+
+.mandatory {
+ color: red;
+ font-weight: bold;
+}
+`
+
+const tplBody = `
+
+
Edit your ScienceMesh account information below.
+
Please note that you cannot modify your email address using this form.
+`
diff --git a/pkg/siteacc/account/login/login.go b/pkg/siteacc/account/login/login.go
new file mode 100644
index 0000000000..078a128b32
--- /dev/null
+++ b/pkg/siteacc/account/login/login.go
@@ -0,0 +1,51 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package login
+
+import "github.com/cs3org/reva/pkg/siteacc/html"
+
+// PanelTemplate is the content provider for the login form.
+type PanelTemplate struct {
+ html.ContentProvider
+}
+
+// GetTitle returns the title of the panel.
+func (template *PanelTemplate) GetTitle() string {
+ return "ScienceMesh Account Login"
+}
+
+// GetCaption returns the caption which is displayed on the panel.
+func (template *PanelTemplate) GetCaption() string {
+ return "Login to your ScienceMesh Account!"
+}
+
+// GetContentJavaScript delivers additional JavaScript code.
+func (template *PanelTemplate) GetContentJavaScript() string {
+ return tplJavaScript
+}
+
+// GetContentStyleSheet delivers additional stylesheet code.
+func (template *PanelTemplate) GetContentStyleSheet() string {
+ return tplStyleSheet
+}
+
+// GetContentBody delivers the actual body content.
+func (template *PanelTemplate) GetContentBody() string {
+ return tplBody
+}
diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go
new file mode 100644
index 0000000000..501ff5609d
--- /dev/null
+++ b/pkg/siteacc/account/login/template.go
@@ -0,0 +1,137 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package login
+
+const tplJavaScript = `
+function verifyForm(formData, requirePassword = true) {
+ if (formData.getTrimmed("email") == "") {
+ setState(STATE_ERROR, "Please enter your email address.", "form", "email", true);
+ return false;
+ }
+
+ if (requirePassword) {
+ if (formData.get("password") == "") {
+ setState(STATE_ERROR, "Please enter your password.", "form", "password", true);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function handleAction(action) {
+ const formData = new FormData(document.querySelector("form"));
+ if (!verifyForm(formData)) {
+ return;
+ }
+
+ setState(STATE_STATUS, "Logging in... this should only take a moment.", "form", null, false);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "{{getServerAddress}}/" + action);
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ setState(STATE_SUCCESS, "Your login was successful! Redirecting...");
+ window.location.replace("{{getServerAddress}}/account/?path=manage");
+ } else {
+ var resp = JSON.parse(this.responseText);
+ setState(STATE_ERROR, "An error occurred while trying to login your account: " + resp.error + "", "form", null, true);
+ }
+ }
+
+ var postData = {
+ "email": formData.getTrimmed("email"),
+ "password": {
+ "value": formData.get("password")
+ }
+ };
+
+ xhr.send(JSON.stringify(postData));
+}
+
+function handleResetPassword() {
+ const formData = new FormData(document.querySelector("form"));
+ if (!verifyForm(formData, false)) {
+ return;
+ }
+
+ setState(STATE_STATUS, "Resetting password... this should only take a moment.", "form", null, false);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "{{getServerAddress}}/reset-password");
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ setState(STATE_SUCCESS, "Your password was successfully reset! Please check your inbox for your new password.", "form", null, true);
+ } else {
+ var resp = JSON.parse(this.responseText);
+ setState(STATE_ERROR, "An error occurred while trying to reset your password: " + resp.error + "", "form", null, true);
+ }
+ }
+
+ var postData = {
+ "email": formData.get("email")
+ };
+
+ xhr.send(JSON.stringify(postData));
+}
+`
+
+const tplStyleSheet = `
+html * {
+ font-family: arial !important;
+}
+
+.mandatory {
+ color: red;
+ font-weight: bold;
+}
+`
+
+const tplBody = `
+
+
Login to your ScienceMesh account using the form below.
+`
diff --git a/pkg/siteacc/account/manage/manage.go b/pkg/siteacc/account/manage/manage.go
new file mode 100644
index 0000000000..eff0ff5af3
--- /dev/null
+++ b/pkg/siteacc/account/manage/manage.go
@@ -0,0 +1,51 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package manage
+
+import "github.com/cs3org/reva/pkg/siteacc/html"
+
+// PanelTemplate is the content provider for the mangement form.
+type PanelTemplate struct {
+ html.ContentProvider
+}
+
+// GetTitle returns the title of the panel.
+func (template *PanelTemplate) GetTitle() string {
+ return "ScienceMesh Account"
+}
+
+// GetCaption returns the caption which is displayed on the panel.
+func (template *PanelTemplate) GetCaption() string {
+ return "Welcome to your ScienceMesh Account!"
+}
+
+// GetContentJavaScript delivers additional JavaScript code.
+func (template *PanelTemplate) GetContentJavaScript() string {
+ return tplJavaScript
+}
+
+// GetContentStyleSheet delivers additional stylesheet code.
+func (template *PanelTemplate) GetContentStyleSheet() string {
+ return tplStyleSheet
+}
+
+// GetContentBody delivers the actual body content.
+func (template *PanelTemplate) GetContentBody() string {
+ return tplBody
+}
diff --git a/pkg/siteacc/account/manage/template.go b/pkg/siteacc/account/manage/template.go
new file mode 100644
index 0000000000..6fde24cc1e
--- /dev/null
+++ b/pkg/siteacc/account/manage/template.go
@@ -0,0 +1,105 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package manage
+
+const tplJavaScript = `
+function handleEditAccount() {
+ setState(STATE_STATUS, "Redirecting to the account editor...");
+ window.location.replace("{{getServerAddress}}/account/?path=edit");
+}
+
+function handleRequestAccess() {
+ setState(STATE_STATUS, "Redirecting to the contact form...");
+ window.location.replace("{{getServerAddress}}/account/?path=contact&subject=" + encodeURIComponent("Request GOCDB access"));
+}
+
+function handleRequestKey() {
+ setState(STATE_STATUS, "Redirecting to the contact form...");
+ window.location.replace("{{getServerAddress}}/account/?path=contact&subject=" + encodeURIComponent("Request API key"));
+}
+
+function handleLogout() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "{{getServerAddress}}/logout");
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+
+ setState(STATE_STATUS, "Logging out...");
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ setState(STATE_SUCCESS, "Done! Redirecting...");
+ window.location.replace("{{getServerAddress}}/account/?path=login");
+ } else {
+ setState(STATE_ERROR, "An error occurred while logging out: " + this.responseText);
+ }
+ }
+
+ xhr.send();
+}
+`
+
+const tplStyleSheet = `
+html * {
+ font-family: arial !important;
+}
+
+.apikey {
+ font-family: monospace !important;
+ background: antiquewhite;
+ border: 1px solid black;
+ padding: 2px;
+}
+`
+
+const tplBody = `
+
On this page, you can manage your ScienceMesh user account. This includes editing your personal information, requesting an API key or access to the GOCDB and more.
+`
diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go
new file mode 100644
index 0000000000..5309783824
--- /dev/null
+++ b/pkg/siteacc/account/panel.go
@@ -0,0 +1,169 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package account
+
+import (
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/cs3org/reva/pkg/siteacc/account/contact"
+ "github.com/cs3org/reva/pkg/siteacc/account/edit"
+ "github.com/cs3org/reva/pkg/siteacc/account/login"
+ "github.com/cs3org/reva/pkg/siteacc/account/manage"
+ "github.com/cs3org/reva/pkg/siteacc/account/registration"
+ "github.com/cs3org/reva/pkg/siteacc/config"
+ "github.com/cs3org/reva/pkg/siteacc/data"
+ "github.com/cs3org/reva/pkg/siteacc/html"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+// Panel represents the account panel.
+type Panel struct {
+ html.PanelProvider
+
+ htmlPanel *html.Panel
+}
+
+const (
+ templateLogin = "login"
+ templateManage = "manage"
+ templateEdit = "edit"
+ templateContact = "contact"
+ templateRegistration = "register"
+)
+
+func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error {
+ // Create the internal HTML panel
+ htmlPanel, err := html.NewPanel("account-panel", panel, conf, log)
+ if err != nil {
+ return errors.Wrap(err, "unable to create the account panel")
+ }
+ panel.htmlPanel = htmlPanel
+
+ // Add all templates
+ if err := panel.htmlPanel.AddTemplate(templateLogin, &login.PanelTemplate{}); err != nil {
+ return errors.Wrap(err, "unable to create the login template")
+ }
+
+ if err := panel.htmlPanel.AddTemplate(templateManage, &manage.PanelTemplate{}); err != nil {
+ return errors.Wrap(err, "unable to create the account management template")
+ }
+
+ if err := panel.htmlPanel.AddTemplate(templateEdit, &edit.PanelTemplate{}); err != nil {
+ return errors.Wrap(err, "unable to create the account editing template")
+ }
+
+ if err := panel.htmlPanel.AddTemplate(templateContact, &contact.PanelTemplate{}); err != nil {
+ return errors.Wrap(err, "unable to create the contact template")
+ }
+
+ if err := panel.htmlPanel.AddTemplate(templateRegistration, ®istration.PanelTemplate{}); err != nil {
+ return errors.Wrap(err, "unable to create the registration template")
+ }
+
+ return nil
+}
+
+// GetActiveTemplate returns the name of the active template.
+func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string {
+ validPaths := []string{templateLogin, templateManage, templateEdit, templateContact, templateRegistration}
+ template := templateLogin
+
+ // Only allow valid template paths; redirect to the login page otherwise
+ for _, valid := range validPaths {
+ if valid == path {
+ template = path
+ break
+ }
+ }
+
+ return template
+}
+
+// PreExecute is called before the actual template is being executed.
+func (panel *Panel) PreExecute(session *html.Session, path string, w http.ResponseWriter, r *http.Request) (html.ExecutionResult, error) {
+ protectedPaths := []string{templateManage, templateEdit, templateContact}
+
+ if session.LoggedInUser == nil {
+ // If no user is logged in, redirect protected paths to the login page
+ for _, protected := range protectedPaths {
+ if protected == path {
+ return panel.redirect(templateLogin, w, r), nil
+ }
+ }
+ } else if path == templateLogin || path == templateRegistration {
+ // If a user is logged in and tries to login or register again, redirect to the main account page
+ return panel.redirect(templateManage, w, r), nil
+ }
+
+ return html.ContinueExecution, nil
+}
+
+// Execute generates the HTTP output of the form and writes it to the response writer.
+func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session) error {
+ dataProvider := func(*html.Session) interface{} {
+ flatValues := make(map[string]string, len(r.URL.Query()))
+ for k, v := range r.URL.Query() {
+ flatValues[strings.Title(k)] = v[0]
+ }
+
+ type TemplateData struct {
+ Account *data.Account
+ Params map[string]string
+
+ Titles []string
+ }
+
+ return TemplateData{
+ Account: session.LoggedInUser,
+ Params: flatValues,
+ Titles: []string{"Mr", "Mrs", "Ms", "Prof", "Dr"},
+ }
+ }
+ return panel.htmlPanel.Execute(w, r, session, dataProvider)
+}
+
+func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult {
+ // Check if the original (full) URI path is stored in the request header; if not, use the request URI to get the path
+ fullPath := r.Header.Get("X-Replaced-Path")
+ if fullPath == "" {
+ uri, _ := url.Parse(r.RequestURI)
+ fullPath = uri.Path
+ }
+
+ // Modify the original request URL by replacing the path parameter
+ newURL, _ := url.Parse(fullPath)
+ params := newURL.Query()
+ params.Del("path")
+ params.Add("path", path)
+ newURL.RawQuery = params.Encode()
+ http.Redirect(w, r, newURL.String(), http.StatusFound)
+ return html.AbortExecution
+}
+
+// NewPanel creates a new account panel.
+func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
+ form := &Panel{}
+ if err := form.initialize(conf, log); err != nil {
+ return nil, errors.Wrapf(err, "unable to initialize the account panel")
+ }
+ return form, nil
+}
diff --git a/pkg/siteacc/account/registration/registration.go b/pkg/siteacc/account/registration/registration.go
new file mode 100644
index 0000000000..cbc96dab57
--- /dev/null
+++ b/pkg/siteacc/account/registration/registration.go
@@ -0,0 +1,51 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package registration
+
+import "github.com/cs3org/reva/pkg/siteacc/html"
+
+// PanelTemplate is the content provider for the registration form.
+type PanelTemplate struct {
+ html.ContentProvider
+}
+
+// GetTitle returns the title of the panel.
+func (template *PanelTemplate) GetTitle() string {
+ return "ScienceMesh Account Registration"
+}
+
+// GetCaption returns the caption which is displayed on the panel.
+func (template *PanelTemplate) GetCaption() string {
+ return "Welcome to the ScienceMesh Account Registration!"
+}
+
+// GetContentJavaScript delivers additional JavaScript code.
+func (template *PanelTemplate) GetContentJavaScript() string {
+ return tplJavaScript
+}
+
+// GetContentStyleSheet delivers additional stylesheet code.
+func (template *PanelTemplate) GetContentStyleSheet() string {
+ return tplStyleSheet
+}
+
+// GetContentBody delivers the actual body content.
+func (template *PanelTemplate) GetContentBody() string {
+ return tplBody
+}
diff --git a/pkg/siteacc/account/registration/template.go b/pkg/siteacc/account/registration/template.go
new file mode 100644
index 0000000000..7bac7fbb0d
--- /dev/null
+++ b/pkg/siteacc/account/registration/template.go
@@ -0,0 +1,184 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package registration
+
+const tplJavaScript = `
+function verifyForm(formData) {
+ if (formData.getTrimmed("email") == "") {
+ setState(STATE_ERROR, "Please specify your email address.", "form", "email", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("fname") == "") {
+ setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("lname") == "") {
+ setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("organization") == "") {
+ setState(STATE_ERROR, "Please specify your organization/company.", "form", "organization", true);
+ return false;
+ }
+
+ if (formData.getTrimmed("role") == "") {
+ setState(STATE_ERROR, "Please specify your role within the organization/company.", "form", "role", true);
+ return false;
+ }
+
+ if (formData.get("password") == "") {
+ setState(STATE_ERROR, "Please set a password.", "form", "password", true);
+ return false;
+ }
+
+ if (formData.get("password2") == "") {
+ setState(STATE_ERROR, "Please confirm your password.", "form", "password2", true);
+ return false;
+ }
+
+ if (formData.get("password") != formData.get("password2")) {
+ setState(STATE_ERROR, "The entered passwords do not match.", "form", "password2", true);
+ return false;
+ }
+
+ return true;
+}
+
+function handleAction(action) {
+ const formData = new FormData(document.querySelector("form"));
+ if (!verifyForm(formData)) {
+ return;
+ }
+
+ setState(STATE_STATUS, "Sending registration... this should only take a moment.", "form", null, false);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "{{getServerAddress}}/" + action);
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ setState(STATE_SUCCESS, "Your registration was successful! Please check your inbox for a confirmation email. You will be redirected to the login page in a few seconds (if not, click here).");
+ window.setTimeout(function() {
+ window.location.replace("{{getServerAddress}}/account/?path=login");
+ }, 3000);
+ } else {
+ var resp = JSON.parse(this.responseText);
+ setState(STATE_ERROR, "An error occurred while trying to register your account: " + resp.error + "", "form", null, true);
+ }
+ }
+
+ var postData = {
+ "email": formData.getTrimmed("email"),
+ "title": formData.getTrimmed("title"),
+ "firstName": formData.getTrimmed("fname"),
+ "lastName": formData.getTrimmed("lname"),
+ "organization": formData.getTrimmed("organization"),
+ "website": formData.getTrimmed("website"),
+ "role": formData.getTrimmed("role"),
+ "phoneNumber": formData.getTrimmed("phone"),
+ "password": {
+ "value": formData.get("password")
+ }
+ };
+
+ xhr.send(JSON.stringify(postData));
+}
+`
+
+const tplStyleSheet = `
+html * {
+ font-family: arial !important;
+}
+
+.mandatory {
+ color: red;
+ font-weight: bold;
+}
+`
+
+const tplBody = `
+
+
Fill out the form below to register for a ScienceMesh account. A confirmation email will be sent to you shortly after registration.
+`
diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go
new file mode 100644
index 0000000000..7d274d8b76
--- /dev/null
+++ b/pkg/siteacc/admin/panel.go
@@ -0,0 +1,115 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package admin
+
+import (
+ "net/http"
+
+ "github.com/cs3org/reva/pkg/siteacc/config"
+ "github.com/cs3org/reva/pkg/siteacc/data"
+ "github.com/cs3org/reva/pkg/siteacc/html"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+// Panel represents the web interface panel of the accounts service administration.
+type Panel struct {
+ html.PanelProvider
+ html.ContentProvider
+
+ htmlPanel *html.Panel
+}
+
+const (
+ templateMain = "main"
+)
+
+func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error {
+ // Create the internal HTML panel
+ htmlPanel, err := html.NewPanel("admin-panel", panel, conf, log)
+ if err != nil {
+ return errors.Wrap(err, "unable to create the administration panel")
+ }
+ panel.htmlPanel = htmlPanel
+
+ // Add all templates
+ if err := panel.htmlPanel.AddTemplate(templateMain, panel); err != nil {
+ return errors.Wrap(err, "unable to create the main template")
+ }
+
+ return nil
+}
+
+// GetActiveTemplate returns the name of the active template.
+func (panel *Panel) GetActiveTemplate(*html.Session, string) string {
+ return templateMain
+}
+
+// GetTitle returns the title of the htmlPanel.
+func (panel *Panel) GetTitle() string {
+ return "ScienceMesh Administration Panel"
+}
+
+// GetCaption returns the caption which is displayed on the htmlPanel.
+func (panel *Panel) GetCaption() string {
+ return "Accounts ({{.Accounts | len}})"
+}
+
+// GetContentJavaScript delivers additional JavaScript code.
+func (panel *Panel) GetContentJavaScript() string {
+ return tplJavaScript
+}
+
+// GetContentStyleSheet delivers additional stylesheet code.
+func (panel *Panel) GetContentStyleSheet() string {
+ return tplStyleSheet
+}
+
+// GetContentBody delivers the actual body content.
+func (panel *Panel) GetContentBody() string {
+ return tplBody
+}
+
+// PreExecute is called before the actual template is being executed.
+func (panel *Panel) PreExecute(*html.Session, string, http.ResponseWriter, *http.Request) (html.ExecutionResult, error) {
+ return html.ContinueExecution, nil
+}
+
+// Execute generates the HTTP output of the htmlPanel and writes it to the response writer.
+func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session, accounts *data.Accounts) error {
+ dataProvider := func(*html.Session) interface{} {
+ type TemplateData struct {
+ Accounts *data.Accounts
+ }
+
+ return TemplateData{
+ Accounts: accounts,
+ }
+ }
+ return panel.htmlPanel.Execute(w, r, session, dataProvider)
+}
+
+// NewPanel creates a new administration panel.
+func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
+ panel := &Panel{}
+ if err := panel.initialize(conf, log); err != nil {
+ return nil, errors.Wrapf(err, "unable to initialize the administration panel")
+ }
+ return panel, nil
+}
diff --git a/internal/http/services/siteacc/panel/template.go b/pkg/siteacc/admin/template.go
similarity index 50%
rename from internal/http/services/siteacc/panel/template.go
rename to pkg/siteacc/admin/template.go
index df3fa7636f..cbe5e4354b 100644
--- a/internal/http/services/siteacc/panel/template.go
+++ b/pkg/siteacc/admin/template.go
@@ -16,77 +16,78 @@
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
-package panel
+package admin
-const panelTemplate = `
-
-
-
-
-
- Accounts panel
-
-
+ }
+
+ var postData = {
+ "email": email,
+ };
+
+ xhr.send(JSON.stringify(postData));
+}
+`
+
+const tplStyleSheet = `
+html * {
+ font-family: monospace !important;
+}
+`
-