Skip to content

Commit

Permalink
Add email verification
Browse files Browse the repository at this point in the history
- docker-compose
  - add postfix image
- backend
  - add SMTP_HOST to settings
  - send user an email with a verification link when they sign up
- frontend
  - add /verify/[[code]] route
    - shows message & login page if verification successful, or account is already verified
    - shows error message otherwise
- resolves #109
  • Loading branch information
lkeegan committed Dec 6, 2024
1 parent 016988c commit 3a6964c
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 24 deletions.
10 changes: 4 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ services:
volumes:
- ${MONDEY_SSL_CERT:-./cert.pem}:/mondey_ssl_cert.pem
- ${MONDEY_SSL_KEY:-./key.pem}:/mondey_ssl_key.pem
# email:
# image: "boky/postfix"
# environment:
# - ALLOW_EMPTY_SENDER_DOMAINS="true"
# networks:
# - mondey-network
email:
image: "boky/postfix"
environment:
- ALLOW_EMPTY_SENDER_DOMAINS="true"
4 changes: 3 additions & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ ARG MONDEY_API_URL

WORKDIR /app

COPY package*.json ./
COPY package.json ./

COPY pnpm-lock.yaml ./

RUN npm install -g pnpm && pnpm install

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@
"submitButtonLabel": "Absenden",
"selectPlaceholder": "Bitte auswählen",
"successMessage": "Bitte überprüfen sie ihr E-Mail Postfach",
"emailValidationMessage": "Ihre E-Mail-Adresse wurde bestätigt und Sie können sich jetzt anmelden.",
"emailValidationError": "Ungültiger oder abgelaufener E-Mail-Validierungslink",
"goHome": "Zur Hauptseite"
},
"login": {
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/routes/verify/[[code]]/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
import { page } from "$app/stores";
import { verifyVerify } from "$lib/client/services.gen";
import UserLogin from "$lib/components/UserLogin.svelte";
import {
CheckCircleOutline,
ExclamationCircleOutline,
} from "flowbite-svelte-icons";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
onMount(async () => {
const { data, error } = await verifyVerify({
body: { token: $page.params.code },
});
if ((!error && data) || error?.detail === "VERIFY_USER_ALREADY_VERIFIED") {
success = true;
return;
}
console.log(error);
success = false;
});
let success: boolean = $state(false);
</script>

<div class="m-2 mx-auto flex flex-col w-full items-center justify-center p-2 text-gray-700 dark:text-gray-400">
{#if success}
<div class="flex flex-row">
<CheckCircleOutline size="xl" color="green" class="m-2"/>
<div class="m-2 p-2">
{$_('registration.emailValidationMessage')}
</div>
</div>
<UserLogin/>
{:else}
<div class="flex flex-row">
<ExclamationCircleOutline size="xl" color="red" class="m-2"/>
<div class="m-2 p-2">
{$_('registration.emailValidationError')}
</div>
</div>
{/if}
</div>
1 change: 1 addition & 0 deletions mondey_backend/src/mondey_backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AppSettings(BaseSettings):
PRIVATE_FILES_PATH: str = "private"
ENABLE_CORS: bool = True
HOST: str = "localhost"
SMTP_HOST: str = "email:587"
PORT: int = 8000
RELOAD: bool = True
LOG_LEVEL: str = "debug"
Expand Down
38 changes: 21 additions & 17 deletions mondey_backend/src/mondey_backend/users.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# TODO: 17th Oct. 2024: remove the artificial verification setting again as soon as
# the email verification server has been implemented. See 'README' block @ line 33f

from __future__ import annotations

import logging
import smtplib
from email.message import EmailMessage
from typing import Annotated

from fastapi import Depends
Expand All @@ -18,29 +18,30 @@

from .databases.users import AccessToken
from .databases.users import User
from .databases.users import async_session_maker
from .databases.users import get_access_token_db
from .databases.users import get_user_db
from .settings import app_settings


def send_email_validation_link(email: str, token: str) -> None:
msg = EmailMessage()
msg["From"] = "no-reply@mondey.lkeegan.dev"
msg["To"] = email
msg["Subject"] = "MONDEY-Konto aktivieren"
msg.set_content(
f"Bitte klicken Sie hier, um Ihr MONDEY-Konto zu aktivieren:\n\nhttps://mondey.lkeegan.dev/verify/{token}"
)
with smtplib.SMTP(app_settings.SMTP_HOST) as s:
s.send_message(msg)


class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
reset_password_token_secret = app_settings.SECRET
verification_token_secret = app_settings.SECRET

async def on_after_register(self, user: User, request: Request | None = None):
# README: Sets the verified flag artificially to allow users to work without an
# actual verification process for now. this can go again as soon as we have an email server for verification.
async with async_session_maker() as session:
user_db = await session.get(User, user.id)
if user_db:
user_db.is_verified = True
await session.commit()
await session.refresh(user_db)

print(f"User {user_db.id} has registered.")
print(f"User is verified? {user_db.is_verified}")
# end README
logging.info(f"User {user.email} registered.")
await self.request_verify(user, request)

async def on_after_forgot_password(
self, user: User, token: str, request: Request | None = None
Expand All @@ -50,7 +51,10 @@ async def on_after_forgot_password(
async def on_after_request_verify(
self, user: User, token: str, request: Request | None = None
):
print(f"Verification requested for user {user.id}. Verification token: {token}")
logging.info(
f"Verification requested for user {user.id}. Verification token: {token}"
)
send_email_validation_link(user.email, token)


async def get_user_manager(
Expand Down
41 changes: 41 additions & 0 deletions mondey_backend/tests/routers/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import smtplib
from email.message import EmailMessage

import pytest
from fastapi.testclient import TestClient


class SMTPMock:
last_message: EmailMessage | None = None

def __init__(self, *args, **kwargs):
pass

def __enter__(self):
return self

def __exit__(self, *args):
pass

def send_message(self, msg: EmailMessage, **kwargs):
SMTPMock.last_message = msg


@pytest.fixture
def smtp_mock(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(smtplib, "SMTP", SMTPMock)
return SMTPMock


def test_register_new_user(public_client: TestClient, smtp_mock: SMTPMock):
assert smtp_mock.last_message is None
email = "u1@asdgdasf.com"
response = public_client.post(
"/auth/register", json={"email": email, "password": "p1"}
)
assert response.status_code == 201
msg = smtp_mock.last_message
assert msg is not None
assert "aktivieren" in msg.get("Subject").lower()
assert msg.get("To") == email
assert "/verify/" in msg.get_content()

0 comments on commit 3a6964c

Please sign in to comment.