diff --git a/docker-compose.yml b/docker-compose.yml index 893a81c6..fe3cff02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index d7191075..07727750 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -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 diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 13c06751..8c6b1501 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -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": { diff --git a/frontend/src/routes/verify/[[code]]/+page.svelte b/frontend/src/routes/verify/[[code]]/+page.svelte new file mode 100644 index 00000000..67f7553c --- /dev/null +++ b/frontend/src/routes/verify/[[code]]/+page.svelte @@ -0,0 +1,44 @@ + + +
+ {#if success} +
+ +
+ {$_('registration.emailValidationMessage')} +
+
+ + {:else} +
+ +
+ {$_('registration.emailValidationError')} +
+
+ {/if} +
diff --git a/mondey_backend/src/mondey_backend/settings.py b/mondey_backend/src/mondey_backend/settings.py index af2a7acd..df99559d 100644 --- a/mondey_backend/src/mondey_backend/settings.py +++ b/mondey_backend/src/mondey_backend/settings.py @@ -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" diff --git a/mondey_backend/src/mondey_backend/users.py b/mondey_backend/src/mondey_backend/users.py index da5a0823..f213105d 100644 --- a/mondey_backend/src/mondey_backend/users.py +++ b/mondey_backend/src/mondey_backend/users.py @@ -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 @@ -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}\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 @@ -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( diff --git a/mondey_backend/tests/routers/test_auth.py b/mondey_backend/tests/routers/test_auth.py new file mode 100644 index 00000000..938b6abf --- /dev/null +++ b/mondey_backend/tests/routers/test_auth.py @@ -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): + 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() + 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