diff --git a/README.md b/README.md index f4888b6..e9cf3c2 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,9 @@ $ docker run -d \ First of all, you can get most tasks done through the web UI, available at http://localhost:3000. ### 1. Define a user -To get started with MailWhale, you need to create a **user** first. Currently, this is done through hard-coded config (see `seed_users` in [config.default.yml](config.default.yml)). Later on, once we have a web UI, there will be a way to easily sign up new users at runtime. +To get started with MailWhale, you need to create a **user** first. +To do so, you can either let the application initialize a default user by supplying `security.seed_users` in [config.default.yml](config.default.yml)). +Alternatively, you can also register new users at runtime via API or web UI. `security.allow_signup` needs to be set to `true`. ### 2. Create an API client It is good practice to not authenticate against the API as a user directly. Instead, create an **API client** with limited privileges, that could easily be revoked in the future. A client is identified by a **client ID** and a **client secret** (or token), very similar to what you might already be familiar with from AWS APIs. Usually, such a client corresponds to an individual client application of yours, which wants to access MailWhale's API. @@ -163,7 +165,7 @@ You can specify configuration options either via a config file (`config.yml`) or | `mail.domain` | `MW_MAIL_DOMAIN` | - | Default domain for sending mails | | `mail.spf_check` | `MW_MAIL_SPF_CHECK` | `false` | Whether to validate sender address domains' SPF records | | `web.listen_v4` | `MW_WEB_LISTEN_V4` | `127.0.0.1:3000` | IP and port for the web server to listen on | -| `web.cors_origin` | - | [`http://localhost:5000`] | List of URLs which to accept CORS requests for | +| `web.cors_origin` | - | [`http://localhost:5000`] | List of URLs which to accept CORS requests for | | `smtp.host` | `MW_SMTP_HOST` | - | SMTP relay host name or IP | | `smtp.port` | `MW_SMTP_PORT` | - | SMTP relay port | | `smtp.username` | `MW_SMTP_USER` | - | SMTP relay authentication user name | @@ -171,6 +173,7 @@ You can specify configuration options either via a config file (`config.yml`) or | `smtp.tls` | `MW_SMTP_TLS` | `false` | Whether to require full TLS (not to be confused with STARTTLS) for the SMTP relay | | `store.path` | `MW_STORE_PATH` | `./data.gob.db` | Target location of the database file | | `security.pepper` | `MW_SECURITY_PEPPER`| - | Pepper to use for hashing user passwords | +| `security.allow_signup` | `MW_SECURITY_ALLOW_SIGNUP` | `false` | Whether to allow the registration of new users | | `security.seed_users` | - | - | List of users to initially populate the database with (see above) | ### SPF Check diff --git a/config.default.yml b/config.default.yml index 183a043..b5488f2 100644 --- a/config.default.yml +++ b/config.default.yml @@ -20,7 +20,8 @@ store: path: 'data.gob.db' security: - pepper: 'sshhh' + pepper: 'sshhh' # Change this! + allow_signup: false seed_users: - email: 'admin@local.host' - password: 'admin' \ No newline at end of file + password: 'admin' # Change this! \ No newline at end of file diff --git a/config/config.go b/config/config.go index dbbd8e1..dc744fd 100644 --- a/config/config.go +++ b/config/config.go @@ -47,8 +47,9 @@ type storeConfig struct { } type securityConfig struct { - Pepper string `env:"MW_SECURITY_PEPPER"` - SeedUsers []EmailPasswordTuple `yaml:"seed_users"` + Pepper string `env:"MW_SECURITY_PEPPER"` + AllowSignup bool `env:"MW_SECURITY_ALLOW_SIGNUP" yaml:"allow_signup"` + SeedUsers []EmailPasswordTuple `yaml:"seed_users"` } type Config struct { diff --git a/main.go b/main.go index bc63707..3f8f4b7 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ func main() { api.NewHealthHandler().Register(router) api.NewMailHandler().Register(router) api.NewClientHandler().Register(router) + api.NewUserHandler().Register(router) api.NewTemplateHandler().Register(router) handler := corsHandler.Handler(router) diff --git a/types/client.go b/types/client.go index d472717..b3862ed 100644 --- a/types/client.go +++ b/types/client.go @@ -13,6 +13,7 @@ import ( const ( PermissionSendMail = "send_mail" PermissionManageClient = "manage_client" + PermissionManageUser = "manage_user" PermissionManageTemplate = "manage_template" ) @@ -20,6 +21,7 @@ func AllPermissions() []string { return []string{ PermissionSendMail, PermissionManageClient, + PermissionManageUser, PermissionManageTemplate, } } diff --git a/version.txt b/version.txt index d5cc44d..8adc70f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.7.2 \ No newline at end of file +0.8.0 \ No newline at end of file diff --git a/web/routes/api/user.go b/web/routes/api/user.go new file mode 100644 index 0000000..ff146ea --- /dev/null +++ b/web/routes/api/user.go @@ -0,0 +1,92 @@ +package api + +import ( + "encoding/json" + "errors" + "github.com/gorilla/mux" + conf "github.com/muety/mailwhale/config" + "github.com/muety/mailwhale/service" + "github.com/muety/mailwhale/types" + "github.com/muety/mailwhale/util" + "github.com/muety/mailwhale/web/handlers" + "net/http" +) + +const routeUser = "/api/user" + +type UserHandler struct { + config *conf.Config + clientService *service.ClientService + userService *service.UserService +} + +func NewUserHandler() *UserHandler { + return &UserHandler{ + config: conf.Get(), + clientService: service.NewClientService(), + userService: service.NewUserService(), + } +} + +func (h *UserHandler) Register(router *mux.Router) { + r := router.PathPrefix(routeUser).Subrouter() + r.Path("").Methods(http.MethodPost).HandlerFunc(h.post) + + auth := handlers.NewAuthMiddleware(h.clientService, h.userService, []string{types.PermissionManageUser}) + r2 := r.PathPrefix("").Subrouter() + r2.Use(auth) + + r2.Path("/{id}").Methods(http.MethodPut).HandlerFunc(h.update) +} + +func (h *UserHandler) post(w http.ResponseWriter, r *http.Request) { + if !h.config.Security.AllowSignup { + util.RespondErrorMessage(w, r, http.StatusMethodNotAllowed, errors.New("user registration is disabled on this server")) + return + } + + var payload types.Signup + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + util.RespondError(w, r, http.StatusBadRequest, err) + return + } + + user, err := h.userService.Create(&payload) + if err != nil { + util.RespondError(w, r, http.StatusBadRequest, err) + return + } + + util.RespondJson(w, http.StatusCreated, user) +} + +func (h *UserHandler) update(w http.ResponseWriter, r *http.Request) { + reqClient := r.Context().Value(conf.KeyClient).(*types.Client) + + var payload types.Signup + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + util.RespondError(w, r, http.StatusBadRequest, err) + return + } + + if payload.Email != reqClient.UserId { + util.RespondEmpty(w, r, http.StatusForbidden) + return + } + + user, err := h.userService.GetById(reqClient.UserId) + if err != nil { + util.RespondError(w, r, http.StatusNotFound, err) + return + } + + user.Password = payload.Password + + user, err = h.userService.Update(user) + if err != nil { + util.RespondError(w, r, http.StatusInternalServerError, err) + return + } + + util.RespondJson(w, http.StatusOK, user) +} diff --git a/webui/src/App.svelte b/webui/src/App.svelte index bebf660..f3e7c9f 100644 --- a/webui/src/App.svelte +++ b/webui/src/App.svelte @@ -1,6 +1,7 @@ -