Skip to content

Commit

Permalink
Add new dependencies and email templates
Browse files Browse the repository at this point in the history
Update database initialization and security configuration

Fix import statement in application.py

Add new email templates for test email and new account

Update security configuration for access token and secret key

Add new email template for password recovery

Update login endpoint to use async function for password recovery

Update reset_password endpoint to use get_by_email function
  • Loading branch information
AndPuQing committed Jan 14, 2024
1 parent 7348fbf commit 87b30e6
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 20 deletions.
24 changes: 20 additions & 4 deletions bemore/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Settings(BaseSettings):
with environment variables.
"""

PROJECT_NAME: str = "BeMore"
API_STR: str = "/api"
SECRET_KEY: str = secrets.token_urlsafe(32)
host: str = "127.0.0.1"
Expand All @@ -36,15 +37,30 @@ class Settings(BaseSettings):

log_level: LogLevel = LogLevel.INFO

SQLALCHEMY_DATABASE_URI: str = "sqlite:///./bemore.db"
FIRST_SUPERUSER: str = "[email protected]"
FIRST_SUPERUSER_PASSWORD: str = "admin"
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8

EMAILS_ENABLED: bool = False
EMAILS_FROM_NAME: str = "BeMore"
EMAILS_FROM_EMAIL: str = ""
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48

SMTP_HOST: str = ""
SMTP_PORT: int = 0
SMTP_TLS: bool = True
SMTP_USER: str = ""
SMTP_PASSWORD: str = ""

EMAIL_TEMPLATES_DIR: str = "bemore/email-templates/build"

model_config = SettingsConfigDict(
env_file=".env",
env_prefix="BEMORE_",
env_file_encoding="utf-8",
)

SQLALCHEMY_DATABASE_URI: str = "sqlite:///./bemore.db"
FIRST_SUPERUSER: str = "admin@localhost"
FIRST_SUPERUSER_PASSWORD: str = "admin"


settings = Settings()
6 changes: 3 additions & 3 deletions bemore/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from jose import jwt
from passlib.context import CryptContext

from bemore.core import config
from bemore.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

Expand All @@ -19,10 +19,10 @@ def create_access_token(
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=ALGORITHM)
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


Expand Down
1 change: 1 addition & 0 deletions bemore/db/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def init_db(session: Session) -> None:
# But if you don't want to use migrations, create
# the tables un-commenting the next line
# Base.metadata.create_all(bind=engine)

user = session.exec(
select(User).where(User.email == settings.FIRST_SUPERUSER)
).first()
Expand Down
15 changes: 15 additions & 0 deletions bemore/email-templates/src/new_account.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<mjml>
<mj-body background-color="#fff">
<mj-section>
<mj-column>
<mj-divider border-color="#555"></mj-divider>
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - New Account</mj-text>
<mj-text font-size="16px" color="#555">You have a new account:</mj-text>
<mj-text font-size="16px" color="#555">Username: {{ username }}</mj-text>
<mj-text font-size="16px" color="#555">Password: {{ password }}</mj-text>
<mj-button padding="50px 0px" href="{{ link }}">Go to Dashboard</mj-button>
<mj-divider border-color="#555" border-width="2px"></mj-divider>
</mj-column>
</mj-section>
</mj-body>
</mjml>
19 changes: 19 additions & 0 deletions bemore/email-templates/src/reset_password.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<mjml>
<mj-body background-color="#fff">
<mj-section>
<mj-column>
<mj-divider border-color="#555"></mj-divider>
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - Password Recovery</mj-text>
<mj-text font-size="16px" color="#555">We received a request to recover the password for user {{ username }}
with email {{ email }}</mj-text>
<mj-text font-size="16px" color="#555">Reset your password by clicking the button below:</mj-text>
<mj-button padding="50px 0px" href="{{ link }}">Reset Password</mj-button>
<mj-text font-size="16px" color="#555">Or open the following link:</mj-text>
<mj-text font-size="16px" color="#555"><a href="{{ link }}">{{ link }}</a></mj-text>
<mj-divider border-color="#555" border-width="2px"></mj-divider>
<mj-text font-size="14px" color="#555">The reset password link / button will expire in {{ valid_hours }} hours.</mj-text>
<mj-text font-size="14px" color="#555">If you didn't request a password recovery you can disregard this email.</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
11 changes: 11 additions & 0 deletions bemore/email-templates/src/test_email.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<mjml>
<mj-body background-color="#fff">
<mj-section>
<mj-column>
<mj-divider border-color="#555"></mj-divider>
<mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }}</mj-text>
<mj-text font-size="16px" color="#555">Test email for: {{ email }}</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
3 changes: 2 additions & 1 deletion bemore/initial_data.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from sqlmodel import Session
from sqlmodel import SQLModel, Session

from bemore.db.engine import engine
from bemore.db.init_db import init_db


def init() -> None:
with Session(engine) as session:
SQLModel.metadata.create_all(engine)
init_db(session)


Expand Down
File renamed without changes.
10 changes: 5 additions & 5 deletions bemore/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# import logging
import logging
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, Optional
Expand Down Expand Up @@ -30,7 +30,7 @@ def send_email(
if settings.SMTP_PASSWORD:
smtp_options["password"] = settings.SMTP_PASSWORD
response = message.send(to=email_to, render=environment, smtp=smtp_options)
# logging.info(f"send email result: {response}")
logging.info(f"send email result: {response}")


def send_test_email(email_to: str) -> None:
Expand All @@ -46,12 +46,12 @@ def send_test_email(email_to: str) -> None:
)


def send_reset_password_email(email_to: str, email: str, token: str) -> None:
async def send_reset_password_email(email_to: str, email: str, token: str) -> None:
project_name = settings.PROJECT_NAME
subject = f"{project_name} - Password recovery for user {email}"
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
template_str = f.read()
server_host = settings.SERVER_HOST
server_host = settings.host + ":" + str(settings.port)
link = f"{server_host}/reset-password?token={token}"
send_email(
email_to=email_to,
Expand All @@ -72,7 +72,7 @@ def send_new_account_email(email_to: str, username: str, password: str) -> None:
subject = f"{project_name} - New account for user {username}"
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
template_str = f.read()
link = settings.SERVER_HOST
link = settings.host + ":" + str(settings.port)
send_email(
email_to=email_to,
subject_template=subject,
Expand Down
8 changes: 4 additions & 4 deletions bemore/web/api/endpoints/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@ def test_token(current_user: CurrentUser) -> Any:


@router.post("/password-recovery/{email}")
def recover_password(email: str, session: SessionDep) -> Message:
async def recover_password(email: str, session: SessionDep) -> Message:
"""
Password Recovery
"""
user = crud.get_user_by_email(session=session, email=email)
user = crud.get_by_email(db=session, email=email)

if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system.",
)
password_reset_token = generate_password_reset_token(email=email)
send_reset_password_email(
await send_reset_password_email(
email_to=user.email, email=email, token=password_reset_token
)
return Message(message="Password recovery email sent")
Expand All @@ -76,7 +76,7 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
email = verify_password_reset_token(token=body.token)
if not email:
raise HTTPException(status_code=400, detail="Invalid token")
user = crud.get_user_by_email(session=session, email=email)
user = crud.get_by_email(db=session, email=email)
if not user:
raise HTTPException(
status_code=404,
Expand Down
29 changes: 28 additions & 1 deletion bemore/web/api/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import select

from bemore.crud.crud_user import user as crud
from bemore.web.api.deps import get_current_active_superuser, SessionDep
from bemore.models import User, UserOut
from bemore.models import User, UserCreate, UserOut
from bemore.core.config import settings
from bemore.utils import (
send_new_account_email,
)

router = APIRouter()

Expand All @@ -21,3 +26,25 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
statement = select(User).offset(skip).limit(limit)
users = session.exec(statement).all()
return users


@router.post(
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UserOut
)
def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
"""
Create new user.
"""
user = crud.get_by_email(db=session, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this username already exists in the system.",
)

user = crud.create(db=session, user_create=user_in)
if settings.EMAILS_ENABLED and user_in.email:
send_new_account_email(
email_to=user_in.email, username=user_in.email, password=user_in.password
)
return user
2 changes: 1 addition & 1 deletion bemore/web/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi import FastAPI
from fastapi.responses import UJSONResponse

from bemore.logging import configure_logging
from bemore.log import configure_logging
from bemore.web.api.router import api_router
from bemore.web.lifetime import register_shutdown_event, register_startup_event
from bemore.core.config import settings
Expand Down
Loading

0 comments on commit 87b30e6

Please sign in to comment.