Skip to content

Commit

Permalink
Fix forgot password functionality (#217)
Browse files Browse the repository at this point in the history
* adjust  forgot password page, add backend email sending function

* add backend connection and email sending

* finish implementation

* add tests for password reset

* remove some old debug output
  • Loading branch information
MaHaWo authored Jan 9, 2025
1 parent f2d9d14 commit 46598e0
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 36 deletions.
53 changes: 29 additions & 24 deletions frontend/src/lib/components/UserForgotPassword.svelte
Original file line number Diff line number Diff line change
@@ -1,49 +1,52 @@
<svelte:options runes={true} />
<script lang="ts">
import { goto } from "$app/navigation";
import { base } from "$app/paths";
import { type ResetForgotPasswordData } from "$lib/client";
import { resetForgotPassword } from "$lib/client/services.gen";
import { preventDefault } from "$lib/util";
import { type ResetForgotPasswordData, resetForgotPassword } from "$lib/client";
import AlertMessage from "$lib/components/AlertMessage.svelte";
import DataInput from "$lib/components/DataInput/DataInput.svelte";
import { preventDefault } from "$lib/util";
import { Button, Card, Heading, Input } from "flowbite-svelte";
import { _ } from "svelte-i18n";
const maildata = {
component: Input,
type: "email",
value: "",
props: {
placeholder: $_("forgotPw.placeholder"),
id: "email",
required: true,
},
};
let alertMessage: string = $_("forgotPw.formatError");
let showAlert: boolean;
let showSuccess = false;
let userEmail = $state("");
let confirmEmail = $state("");
let alertMessage: string = $state($_("forgotPw.formatError"));
let showAlert: boolean = $state(false);
let showSuccess = $state(false);
async function submitData(): Promise<void> {
if (userEmail !== confirmEmail) {
alertMessage = $_("forgotPw.confirmError");
showAlert = true;
return;
}
const data: ResetForgotPasswordData = {
body: {
email: maildata.value,
email: userEmail,
},
};
const response = await resetForgotPassword(data);
const result = await resetForgotPassword(data);
if (result.error) {
console.log("error: ", result.error);
showAlert = true;
if (response.error) {
console.log("error: ", response.error);
alertMessage = $_("forgotPw.sendError");
showAlert = true;
} else {
console.log(
"successful transmission, response status: ",
result.response.status,
);
console.log("successful transmission of forgot password email");
console.log("response: ", response);
showSuccess = true;
}
}
Expand All @@ -53,7 +56,7 @@ async function submitData(): Promise<void> {
<AlertMessage
title={$_('forgotPw.alertTitle')}
message={alertMessage}
lastpage={`${base}/userLand/lostPassword`}
lastpage={`${base}/forgotPassword`}
onclick={() => {
showAlert = false;
}}
Expand All @@ -71,7 +74,12 @@ async function submitData(): Promise<void> {
<div class="m-2 mx-auto w-full flex-col space-y-6 p-2">
<DataInput
component={maildata.component}
bind:value={maildata.value}
bind:value={userEmail}
{...maildata.props}
/>
<DataInput
component={maildata.component}
bind:value={confirmEmail}
{...maildata.props}
/>
</div>
Expand All @@ -81,9 +89,6 @@ async function submitData(): Promise<void> {
</div>
</form>
{:else}
<div class="m-2 flex w-full items-center justify-center p-2">
<p>{$_('forgotPw.mailSentMessage')}</p>
</div>
<div class="m-2 flex w-full items-center justify-center p-2">
<Button
class="dark:bg-primay-700 w-full bg-primary-700 text-center text-sm text-white hover:bg-primary-800 hover:text-white dark:hover:bg-primary-800"
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/components/UserLogin.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ let alertMessage: string = $state($_("login.badCredentials"));
placeholder={$_("login.passwordLabel")}
required
/>
<a href={`${base}/forgotPassword`} class="text-primary-700 dark:text-primary-500">
{$_("login.forgotPassword")}
</div>

<Button
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@
"profileButtonLabelLogout": "Logout",
"profileTitleDefault": "Willkommen!",
"profileAccess": "Ihr Profil",
"registerNew": "Als neuer Benutzer registrieren"
"registerNew": "Als neuer Benutzer registrieren",
"forgotPassword": "Passwort vergessen?"
},
"userData": {
"label": "Persönliche Daten",
Expand Down Expand Up @@ -195,13 +196,20 @@
},
"forgotPw": {
"heading": "Passwort vergessen?",
"resetHeading": "Passwort zurücksetzen",
"placeholder": "Bitte geben sie eine E-mail Adresse an um ihr Passwort zu erneuern",
"success": "Zurück zur Startseite",
"success": "Bitte überprüfen sie ihr E-Mail Postfach!",
"successReset": "Ihr Passwort wurde erfolgreich zurückgesetzt",
"goToLogin": "Zurück zum Login",
"pending": "Absenden",
"alertTitle": "Fehler",
"formatError": "Die angegebene email Adresse hat ein falsches Format",
"mailSentMessage": "Bitte überprüfen sie ihr E-Mail Postfach",
"sendError": "Beim Senden der Daten ist ein Fehler aufgetreten. Bitte versuchen sie es erneut"
"sendError": "Beim Senden der Daten ist ein Fehler aufgetreten. Bitte versuchen sie es erneut",
"confirmError": "Eingaben nicht identisch",
"Error": "Ein Fehler ist aufgetreten",
"codeError": "Ungültiger oder leerer reset code",
"inputlabelPw": "Neues Passwort",
"inputlabelPwConfirm": "Neues Passwort bestätigen"
},
"misc": {
"understood": "Verstanden",
Expand Down
86 changes: 86 additions & 0 deletions frontend/src/routes/resetPassword/[[code]]/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<svelte:options runes={true} />
<script lang="ts">
import { page } from "$app/stores";
import { resetResetPassword } from "$lib/client/services.gen";
import AlertMessage from "$lib/components/AlertMessage.svelte";
import DataInput from "$lib/components/DataInput/DataInput.svelte";
import { preventDefault } from "$lib/util";
import { Button, Card, Heading, Input } from "flowbite-svelte";
import { CheckCircleOutline } from "flowbite-svelte-icons";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
let pw = $state("");
let confirmPw = $state("");
let showAlert = $state(false);
let alertMessage = $state($_("forgotPw.confirmError"));
let success: boolean = $state(false);
onMount(() => {
if (
$page.params.code === undefined ||
$page.params.code === null ||
$page.params.code === ""
) {
alertMessage = $_("forgotPw.codeError");
showAlert = true;
}
});
async function submitData(): Promise<void> {
if (pw !== confirmPw) {
showAlert = true;
return;
}
const { data, error } = await resetResetPassword({
body: { token: $page.params.code, password: pw },
});
if ((!error && data) || error?.detail === "VERIFY_USER_ALREADY_VERIFIED") {
success = true;
return;
}
console.log(error);
alertMessage = $_("forgotPw.sendError");
showAlert = true;
success = false;
}
</script>

{#if showAlert === true}
<AlertMessage title={$_('forgotPw.Error')} message={alertMessage} onclick={() => {
showAlert = false;
}}/>
{:else}
{#if success === true}
<div class="flex flex-row">
<CheckCircleOutline size="xl" color="green" class="m-2"/>
<div class="m-2 p-2">
{$_('forgotPw.successReset')}
</div>
</div>
<Button href="/userLand/userLogin" size="md">{$_('forgotPw.goToLogin')}</Button>
{:else}
<Card class="container m-2 mx-auto w-full max-w-xl items-center justify-center p-2">

<Heading
tag="h3"
class="m-2 p-2 text-center font-bold tracking-tight text-gray-700 dark:text-gray-400"
>{$_('forgotPw.resetHeading')}</Heading>

<form onsubmit={preventDefault(submitData)} class = "space-y-4">
<div class="m-2 mx-auto w-full flex-col space-y-6 p-2">

<DataInput component = {Input} bind:value={pw} required={true} id="restPw" kwargs={{type: "password"}} label={$_("forgotPw.inputlabelPw")}/>

<DataInput component = {Input} bind:value={confirmPw} required={true} id="restConfirmPw" kwargs={{type: "password"}} label={$_("forgotPw.inputlabelPwConfirm")}/>
</div>
<div class="m-2 flex w-full items-center justify-center p-2">
<Button size="md" type="submit">{$_('forgotPw.pending')}</Button>
</div>
</form>
</Card>
{/if}
{/if}
15 changes: 14 additions & 1 deletion mondey_backend/src/mondey_backend/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ def send_email_validation_link(email: str, token: str) -> None:
s.send_message(msg)


def send_reset_password_link(email: str, token: str) -> None:
msg = EmailMessage()
msg["From"] = "no-reply@mondey.lkeegan.dev"
msg["To"] = email
msg["Subject"] = "MONDEY Passwort zurücksetzen"
msg.set_content(
f"Bitte klicken Sie hier, um Ihr MONDEY Passwort zurückzusetzen:\n\nhttps://mondey.lkeegan.dev/resetPassword/{token}\n\n-----\n\nPlease click here to reset your MONDEY password:\n\nhttps://mondey.lkeegan.dev/resetPassword/{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
Expand All @@ -60,7 +72,8 @@ async def on_after_register(self, user: User, request: Request | None = None):
async def on_after_forgot_password(
self, user: User, token: str, request: Request | None = None
):
print(f"User {user.id} has forgot their password. Reset token: {token}")
logging.info(f"User {user.id} has forgot their password. Reset token: {token}")
send_reset_password_link(user.email, token)

async def on_after_request_verify(
self, user: User, token: str, request: Request | None = None
Expand Down
59 changes: 59 additions & 0 deletions mondey_backend/tests/routers/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,62 @@ def test_register_new_user_valid_research_code(
new_user = admin_client.get("/admin/users/").json()[-1]
assert new_user["email"] == email
assert new_user["research_group_id"] == 123451


def test_user_reset_password(user_client: TestClient, smtp_mock: SMTPMock):
assert smtp_mock.last_message is None
email = "user@mondey.de"
response = user_client.post("/auth/forgot-password", json={"email": email})
assert response.status_code == 202

msg = smtp_mock.last_message
assert msg is not None
assert msg.get("To") == email
token = msg.get_content().split("\n\n")[1].rsplit("/")[-1]
new_password = "new_password"
response = user_client.post(
"/auth/reset-password", json={"token": token, "password": new_password}
)
assert response.status_code == 200


def test_user_reset_password_invalid_token(
user_client: TestClient, smtp_mock: SMTPMock
):
assert smtp_mock.last_message is None
email = "user@mondey.de"
response = user_client.post("/auth/forgot-password", json={"email": email})
assert response.status_code == 202

msg = smtp_mock.last_message
assert msg is not None
assert msg.get("To") == email
token = msg.get_content().split("\n\n")[1].rsplit("/")[-1] + "invalid"
new_password = "new_password"
response = user_client.post(
"/auth/reset-password", json={"token": token, "password": new_password}
)
assert response.status_code == 400


def test_user_forgot_password(
user_client: TestClient, active_user, smtp_mock: SMTPMock
):
assert smtp_mock.last_message is None
response = user_client.post(
"/auth/forgot-password", json={"email": active_user.email}
)
assert response.status_code == 202


def test_user_forgot_password_invalid_email(
user_client: TestClient, smtp_mock: SMTPMock
):
assert smtp_mock.last_message is None
email = "invalid-email"
response = user_client.post("/auth/forgot-password", json={"email": email})
assert (
response.json()["detail"][0]["msg"]
== "value is not a valid email address: An email address must have an @-sign."
)
assert response.json()["detail"][0]["type"] == "value_error"
7 changes: 0 additions & 7 deletions mondey_backend/tests/utils/test_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def test_online_statistics_computation_too_little_data():

def test_get_score_statistics_by_age(session):
answers = session.exec(select(MilestoneAnswer)).all()
print(answers)
# which answers we choose here is arbitrary for testing, we just need to make sure it's fixed and not empty
child_ages = {
1: 5,
Expand Down Expand Up @@ -213,12 +212,6 @@ def test_calculate_milestone_statistics_by_age(statistics_session):

# we have nothing new for everything else
for age in range(0, len(mscore.scores)):
print(
age,
mscore.scores[age].count,
mscore.scores[age].avg_score,
mscore.scores[age].stddev_score,
)
if age != 8:
assert mscore.scores[age].count == 12
avg = 0 if age < 5 else min(1 * age - 5, 3)
Expand Down

0 comments on commit 46598e0

Please sign in to comment.