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 @@ -
+
{#if currentUser} - person {currentUser} + person {currentUser} | Logout {:else} - Sign Up +  |  + Login + class="font-semibold text-primary hover:text-primary-dark">Login {/if}
diff --git a/webui/src/components/SignupForm.svelte b/webui/src/components/SignupForm.svelte new file mode 100644 index 0000000..82cf7d6 --- /dev/null +++ b/webui/src/components/SignupForm.svelte @@ -0,0 +1,60 @@ + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ diff --git a/webui/src/layouts/Main.svelte b/webui/src/layouts/Main.svelte index 33e9cc3..a91e274 100644 --- a/webui/src/layouts/Main.svelte +++ b/webui/src/layouts/Main.svelte @@ -11,13 +11,13 @@ } -
+
+ class="absolute inset-x-0 top-0 flex flex-col items-center justify-center w-full py-8 space-y-2"> {#each $errors as m}
warning {m} @@ -25,7 +25,7 @@ {/each} {#each $infos as m}
info {m} @@ -33,7 +33,7 @@ {/each} {#each $successes as e}
check_circle {e} @@ -41,20 +41,20 @@ {/each}
-
- +
-
+
diff --git a/webui/src/views/Clients.svelte b/webui/src/views/Clients.svelte index 7262bfd..408a9a6 100644 --- a/webui/src/views/Clients.svelte +++ b/webui/src/views/Clients.svelte @@ -6,7 +6,7 @@ import Modal from '../components/Modal.svelte' import { getClients, createClient, deleteClient } from '../api/clients' - const availablePermissions = ['send_mail', 'manage_client', 'manage_template'] + const availablePermissions = ['send_mail', 'manage_client', 'manage_user', 'manage_template'] let clients = [] @@ -81,11 +81,11 @@
-
+

Manage API Clients

@@ -101,7 +101,7 @@ {#if newClient.api_key}
+ class="flex flex-col w-full px-4 py-2 mt-4 mb-12 space-y-2 text-sm text-white rounded bg-primary">
Success! A new client was created. Here is your client secret (aka. API @@ -119,7 +119,7 @@
#{i + 1} {client.id}
{client.description} @@ -142,7 +142,7 @@
@@ -150,7 +150,7 @@
{:else}
+ class="flex items-center justify-center w-full py-12 text-gray-500"> No clients available. Create your first one.
{/if} @@ -161,13 +161,13 @@

Add new client

-

Permissions

+

Permissions

{#each availablePermissions as perm} -
+
-

E-Mail Settings

-
+

E-Mail Settings

+
+ class="px-4 py-2 mt-4 text-sm text-white rounded bg-primary"> Please Note: You can set an optional sender address for this client (e.g. My App <noreply@example.org>), that will be used in the mail's "From" header. However, you need to make sure that SPF and DMARC records are properly set for your domain. You need to authorize MailWhale's servers to send mail on your behalf. If left blank, a default sender address like vldsbgfr+user@mailwhale.dev will be used.
@@ -205,7 +205,7 @@
@@ -215,7 +215,7 @@
+ class="px-4 py-2 text-white rounded-md bg-primary hover:bg-primary-dark">Create
diff --git a/webui/src/views/Signup.svelte b/webui/src/views/Signup.svelte new file mode 100644 index 0000000..cfb024b --- /dev/null +++ b/webui/src/views/Signup.svelte @@ -0,0 +1,22 @@ + + + +
+

Sign Up

+
+ +
+
+