diff --git a/media_manager/auth/config.py b/media_manager/auth/config.py
index 966eab3..0913c7c 100644
--- a/media_manager/auth/config.py
+++ b/media_manager/auth/config.py
@@ -10,11 +10,20 @@ class AuthConfig(BaseSettings):
token_secret: str = Field(default_factory=secrets.token_hex)
session_lifetime: int = 60 * 60 * 24
admin_email: list[str] = []
+ email_password_resets: bool = False
@property
def jwt_signing_key(self):
return self._jwt_signing_key
+class EmailConfig(BaseSettings):
+ model_config = SettingsConfigDict(env_prefix="EMAIL_")
+ smtp_host: str
+ smtp_port: int
+ smtp_user: str
+ smtp_password: str
+ from_email: str
+ use_tls: bool = False
class OpenIdConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="OPENID_")
diff --git a/media_manager/auth/users.py b/media_manager/auth/users.py
index f787398..66305a8 100644
--- a/media_manager/auth/users.py
+++ b/media_manager/auth/users.py
@@ -1,3 +1,4 @@
+import logging
import os
import uuid
from typing import Optional
@@ -13,13 +14,20 @@ from fastapi_users.authentication import (
from fastapi_users.db import SQLAlchemyUserDatabase
from httpx_oauth.clients.openid import OpenID
from fastapi.responses import RedirectResponse, Response
+from pydantic import AnyHttpUrl
from starlette import status
-from media_manager.auth.config import AuthConfig, OpenIdConfig
+from media_manager.auth.config import AuthConfig, OpenIdConfig, EmailConfig
from media_manager.auth.db import User, get_user_db
from media_manager.auth.schemas import UserUpdate
from media_manager.config import BasicConfig
+import smtplib
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+
+log = logging.getLogger(__name__)
+
config = AuthConfig()
SECRET = config.token_secret
LIFETIME = config.session_lifetime
@@ -47,7 +55,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
verification_token_secret = SECRET
async def on_after_register(self, user: User, request: Optional[Request] = None):
- print(f"User {user.id} has registered.")
+ log.info(f"User {user.id} has registered.")
if user.email in config.admin_email:
updated_user = UserUpdate(is_superuser=True, is_verified=True)
await self.update(user=user, user_update=updated_user)
@@ -55,20 +63,55 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
):
- print(f"User {user.id} has forgot their password. Reset token: {token}")
+ link = f"{BasicConfig().FRONTEND_URL}login/reset-password?token={token}"
+ log.info(f"User {user.id} has forgot their password. Reset Link: {link}")
+ if not config.email_password_resets:
+ log.info("Email password resets are disabled, not sending email.")
+ return
+
+ email_conf = EmailConfig()
+ subject = "MediaManager - Password Reset Request"
+ html = f"""\
+
+
+ Hi {user.email},
+
+
+ if you forgot your password, reset you password here .
+ If you did not request a password reset, you can ignore this email.
+
+
+ If the link does not work, copy the following link into your browser: {link}
+
+
+ """
+
+ message = MIMEMultipart()
+ message["From"] = email_conf.from_email
+ message["To"] = user.email
+ message["Subject"] = subject
+ message.attach(MIMEText(html, "html"))
+
+ with smtplib.SMTP(email_conf.smtp_host, email_conf.smtp_port) as server:
+ if email_conf.use_tls:
+ server.starttls()
+ server.login(email_conf.smtp_user, email_conf.smtp_password)
+ server.sendmail(email_conf.from_email,user.email, message.as_string())
+ log.info(f"Sent password reset email to {user.email}")
+
async def on_after_reset_password(
self, user: User, request: Optional[Request] = None
):
- print(f"User {user.id} has reset their password.")
+ log.info(f"User {user.id} has reset their password.")
async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
):
- print(f"Verification requested for user {user.id}. Verification token: {token}")
+ log.info(f"Verification requested for user {user.id}. Verification token: {token}")
async def on_after_verify(self, user: User, request: Optional[Request] = None):
- print(f"User {user.id} has been verified")
+ log.info(f"User {user.id} has been verified")
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
diff --git a/media_manager/config.py b/media_manager/config.py
index ecfb626..05f1913 100644
--- a/media_manager/config.py
+++ b/media_manager/config.py
@@ -9,6 +9,6 @@ class BasicConfig(BaseSettings):
tv_directory: Path = "/data/tv"
movie_directory: Path = "/data/movies"
torrent_directory: Path = "/data/torrents"
- FRONTEND_URL: AnyHttpUrl = "http://localhost:3000"
+ FRONTEND_URL: AnyHttpUrl = "http://localhost:3000/"
CORS_URLS: list[str] = []
DEVELOPMENT: bool = False
diff --git a/web/src/lib/components/login-form.svelte b/web/src/lib/components/login-form.svelte
index bdd9c76..194cb70 100644
--- a/web/src/lib/components/login-form.svelte
+++ b/web/src/lib/components/login-form.svelte
@@ -184,8 +184,7 @@
diff --git a/web/src/routes/login/forgot-password/+page.svelte b/web/src/routes/login/forgot-password/+page.svelte
new file mode 100644
index 0000000..fcc4d9d
--- /dev/null
+++ b/web/src/routes/login/forgot-password/+page.svelte
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+ Forgot Password
+
+ {#if isSuccess}
+ We've sent a password reset link to your email address if a SMTP server is configured. Check your inbox and follow the instructions to reset your password.
+ If you didn't receive an email, please contact an administrator, the reset link will be in the logs of MediaManager.
+ {:else}
+ Enter your email address and we'll send you a link to reset your password.
+ {/if}
+
+
+
+ {#if isSuccess}
+
+
+
+ Password reset email sent successfully!
+
+
+
+
Didn't receive the email? Check your spam folder or
+
{ isSuccess = false; email = ''; }}
+ >
+ try again
+
+
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/routes/login/reset-password/+page.svelte b/web/src/routes/login/reset-password/+page.svelte
new file mode 100644
index 0000000..44ab156
--- /dev/null
+++ b/web/src/routes/login/reset-password/+page.svelte
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+ Reset Password
+
+ Enter your new password below.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/routes/login/reset-password/+page.ts b/web/src/routes/login/reset-password/+page.ts
new file mode 100644
index 0000000..3c72626
--- /dev/null
+++ b/web/src/routes/login/reset-password/+page.ts
@@ -0,0 +1,6 @@
+import type {PageLoad} from './$types';
+
+export function load({ url }) {
+ console.log("got token: ",url.searchParams.get('token'));
+ return {token: url.searchParams.get('token') };
+}
\ No newline at end of file