Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add email verification #198

Merged
merged 2 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
MaHaWo marked this conversation as resolved.
Show resolved Hide resolved
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"] = "[email protected]"
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}\n\n-----\n\nPlease click here to activate your MONDEY account:\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
46 changes: 46 additions & 0 deletions mondey_backend/tests/routers/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need a test for failed verification?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes true, will add one

assert smtp_mock.last_message is None
email = "[email protected]"
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()
response = public_client.post("/auth/verify", json={"token": "invalid-token"})
assert response.status_code == 400
token = msg.get_content().split("\n\n")[1].rsplit("/")[-1]
response = public_client.post("/auth/verify", json={"token": token})
assert response.status_code == 200
Loading