Skip to content
This repository has been archived by the owner on Dec 23, 2023. It is now read-only.

Commit

Permalink
feat: user registration (resolve #7)
Browse files Browse the repository at this point in the history
  • Loading branch information
muety committed Apr 4, 2021
1 parent c486601 commit 55700aa
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 39 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -163,14 +165,15 @@ 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 |
| `smtp.password` | `MW_SMTP_PASS` | - | SMTP relay authentication password |
| `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
Expand Down
5 changes: 3 additions & 2 deletions config.default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ store:
path: 'data.gob.db'

security:
pepper: 'sshhh'
pepper: 'sshhh' # Change this!
allow_signup: false
seed_users:
- email: '[email protected]'
password: 'admin'
password: 'admin' # Change this!
5 changes: 3 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions types/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import (
const (
PermissionSendMail = "send_mail"
PermissionManageClient = "manage_client"
PermissionManageUser = "manage_user"
PermissionManageTemplate = "manage_template"
)

func AllPermissions() []string {
return []string{
PermissionSendMail,
PermissionManageClient,
PermissionManageUser,
PermissionManageTemplate,
}
}
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.2
0.8.0
92 changes: 92 additions & 0 deletions web/routes/api/user.go
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 3 additions & 1 deletion webui/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script>
import router from 'page'
import Home from './views/Home.svelte'
import Signup from './views/Signup.svelte'
import Clients from './views/Clients.svelte'
import Mails from './views/Mails.svelte';
import Templates from './views/Templates.svelte';
Expand All @@ -18,6 +19,7 @@
router('/', () => (page = Home))
router('/login', () => (page = Home))
router('/signup', () => (page = Signup))
router('/clients', () => (page = Clients))
router('/mails', () => (page = Mails))
router('/templates', () => (page = Templates))
Expand All @@ -38,6 +40,6 @@
@tailwind utilities;
</style>

<div id="app-container" class="container mx-auto flex flex-col flex-grow">
<div id="app-container" class="container flex flex-col flex-grow mx-auto">
<svelte:component this={page} />
</div>
11 changes: 11 additions & 0 deletions webui/src/api/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { request } from './api'

async function createUser(signup) {
return (await request('/user', signup, { method: 'POST' })).data
}

async function updateUser(id, signup) {
return (await request(`/user/${id}`, signup, { method: 'PUT' })).data
}

export { createUser, updateUser }
14 changes: 9 additions & 5 deletions webui/src/components/AccountIndicator.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@
export let currentUser
</script>

<div class="flex space-x-2 items-center h-full">
<div class="flex items-center h-full space-x-2">
{#if currentUser}
<span class="text-sm flex items-center"><span class="material-icons mr-1">person</span> {currentUser}</span>
<span class="flex items-center text-sm"><span class="mr-1 material-icons">person</span> {currentUser}</span>
<span>|</span>
<a
class="text-sm text-primary hover:text-primary-dark cursor-pointer"
class="text-sm cursor-pointer text-primary hover:text-primary-dark"
on:click={logout}>Logout</a>
{:else}
<a
<a
href="/signup"
class="font-semibold text-primary hover:text-primary-dark">Sign Up</a>
<span>&nbsp;|&nbsp;</span>
<a
href="/login"
class="text-primary hover:text-primary-dark font-semibold">Login</a>
class="font-semibold text-primary hover:text-primary-dark">Login</a>
{/if}
</div>
60 changes: 60 additions & 0 deletions webui/src/components/SignupForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script>
import { createEventDispatcher } from 'svelte';
import { errors } from '../stores/alerts'
const dispatch = createEventDispatcher()
let username, password, passwordRepeat;
function signup() {
if (!username || !password || !passwordRepeat) return
if (password !== passwordRepeat) {
return errors.spawn('Passwords do not match')
}
dispatch('signup', { username, password })
}
</script>

<form class="flex flex-col w-full p-4 space-y-4" on:submit|preventDefault="{signup}">
<div class="flex flex-col w-full space-y-1">
<label for="email-input">E-Mail</label>
<input
type="email"
class="p-2 border-2 rounded-md border-primary"
name="email-input"
placeholder="[email protected]"
required
bind:value={username} />
</div>

<div class="flex flex-col w-full space-y-1">
<label for="password-input">Password</label>
<input
type="password"
class="p-2 border-2 rounded-md border-primary"
name="password-input"
placeholder="********"
required
bind:value={password} />
</div>

<div class="flex flex-col w-full space-y-1">
<label for="password-input">Password (repeat)</label>
<input
type="password"
class="p-2 border-2 rounded-md border-primary"
name="password-repeat-input"
placeholder="********"
required
bind:value={passwordRepeat} />
</div>

<div class="flex justify-between py-2">
<div />
<button
type="submit"
class="px-4 py-2 text-white rounded-md bg-primary hover:bg-primary-dark">Sign Up</button>
</div>
</form>
20 changes: 10 additions & 10 deletions webui/src/layouts/Main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,50 @@
}
</script>

<div id="app-container" class="container mx-auto my-8 flex flex-col flex-grow">
<div id="app-container" class="container flex flex-col flex-grow mx-auto my-8">
<div
id="alert-container"
class="w-full absolute inset-x-0 top-0 py-8 flex justify-center space-y-2 flex-col items-center">
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}
<div
class="flex space-x-2 mt-4 bg-red-500 px-4 py-2 rounded text-white text-sm"
class="flex px-4 py-2 mt-4 space-x-2 text-sm text-white bg-red-500 rounded"
transition:slide>
<span class="material-icons">warning</span>
<span>{m}</span>
</div>
{/each}
{#each $infos as m}
<div
class="flex space-x-2 mt-4 bg-primary px-4 py-2 rounded text-white text-sm"
class="flex px-4 py-2 mt-4 space-x-2 text-sm text-white rounded bg-primary"
transition:slide>
<span class="material-icons">info</span>
<span>{m}</span>
</div>
{/each}
{#each $successes as e}
<div
class="flex space-x-2 mt-4 bg-green-500 px-4 py-2 rounded text-white text-sm"
class="flex px-4 py-2 mt-4 space-x-2 text-sm text-white bg-green-500 rounded"
transition:slide>
<span class="material-icons">check_circle</span>
<span>{e}</span>
</div>
{/each}
</div>

<header class="flex w-full justify-between">
<div id="logo-container" class="flex space-x-4 items-center">
<header class="flex justify-between w-full">
<a id="logo-container" href="/" class="flex items-center space-x-4">
<img src="images/logo.svg" alt="Logo" style="max-height: 60px;" />
<div class="flex">
<span class="text-primary text-xl font-semibold">Mail</span>
<span class="text-xl font-semibold text-primary">Mail</span>
<span class="text-xl font-semibold">Whale</span>
</div>
</div>
</a>
<div>
<AccountIndicator currentUser={$user} on:logout={logout} />
</div>
</header>

<main class="mt-24 flex-grow">
<main class="flex-grow mt-24">
<slot name="content" />
</main>

Expand Down
Loading

0 comments on commit 55700aa

Please sign in to comment.