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

Fix forgot password functionality #217

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
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 @@ -123,7 +123,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 @@ -174,13 +175,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
Loading