feat: email resetting backend code

This commit is contained in:
swve 2024-03-19 12:09:28 +01:00
parent ae2367bdea
commit 15677a6946
5 changed files with 308 additions and 4 deletions

View file

@ -1,6 +1,11 @@
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
from pydantic import EmailStr
from sqlmodel import Session
from src.services.users.password_reset import (
change_password_with_reset_code,
send_reset_password_code,
)
from src.services.orgs.orgs import get_org_join_mechanism
from src.security.auth import get_current_user
from src.core.events.database import get_db_session
@ -210,6 +215,42 @@ async def api_update_user_password(
return await update_user_password(request, db_session, current_user, user_id, form)
@router.post("/reset_password/change_password/{email}", tags=["users"])
async def api_change_password_with_reset_code(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
new_password: str,
email: EmailStr,
org_id: int,
reset_code: str,
):
"""
Update User Password with reset code
"""
return await change_password_with_reset_code(
request, db_session, current_user, new_password, org_id, email, reset_code
)
@router.post("/reset_password/send_reset_code/{email}", tags=["users"])
async def api_send_password_reset_email(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
email: EmailStr,
org_id: int,
):
"""
Update User Password
"""
return await send_reset_password_code(
request, db_session, current_user, org_id, email
)
@router.delete("/user_id/{user_id}", tags=["users"])
async def api_delete_user(
*,

View file

@ -2,6 +2,7 @@ import json
import random
import string
import uuid
from pydantic import EmailStr
import redis
from datetime import datetime, timedelta
from sqlmodel import Session, select
@ -139,7 +140,7 @@ async def get_invite_codes(
for invite_code in invite_codes:
invite_code = r.get(invite_code)
invite_code = json.loads(invite_code)
invite_code = json.loads(invite_code) # type: ignore
invite_codes_list.append(invite_code)
return invite_codes_list
@ -258,7 +259,7 @@ def send_invite_email(
org: OrganizationRead,
invite_code_uuid: str,
user: UserRead,
email: str,
email: EmailStr,
):
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
@ -284,7 +285,7 @@ def send_invite_email(
# Send email
if invite:
invite = r.get(invite[0])
invite = json.loads(invite)
invite = json.loads(invite) # type: ignore
# send email
send_email(

View file

@ -0,0 +1,45 @@
from pydantic import EmailStr
from src.db.organizations import OrganizationRead
from src.db.users import UserRead
from src.services.email.utils import send_email
def send_account_creation_email(
user: UserRead,
email: EmailStr,
):
# send email
return send_email(
to=email,
subject=f"Welcome to LearnHouse, {user.username}!",
body=f"""
<html>
<body>
<p>Hello {user.username}</p>
<p>Welcome to LearnHouse! , get started by creating your own organization or join a one.</p>
<p>Need some help to get started ? <a href="https://learn.learnhouse.io">LearnHouse Academy</a></p>
</body>
</html>
""",
)
def send_password_reset_email(
reset_email_invite_uuid: str,
user: UserRead,
organization: OrganizationRead,
email: EmailStr,
):
# send email
return send_email(
to=email,
subject="Reset your password",
body=f"""
<html>
<body>
<p>Hello {user.username}</p>
<p>Click <a href="https://{organization.slug}.learnhouse.io/reset-password?resetEmailInviteUuid={reset_email_invite_uuid}">here</a> to reset your password.</p>
</body>
</html>
""",
)

View file

@ -0,0 +1,203 @@
from datetime import datetime, timedelta
import json
import random
import redis
import string
import uuid
from fastapi import HTTPException, Request
from pydantic import EmailStr
from sqlmodel import Session, select
from src.db.organizations import Organization, OrganizationRead
from src.security.security import security_hash_password
from config.config import get_learnhouse_config
from src.services.users.emails import (
send_password_reset_email,
)
from src.db.users import (
AnonymousUser,
PublicUser,
User,
UserRead,
)
async def send_reset_password_code(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
org_id: int,
email: EmailStr,
):
# Get user
statement = select(User).where(User.email == email)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# Get org
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(
status_code=400,
detail="Organization not found",
)
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Generate reset code
def generate_code(length=5):
letters_and_digits = string.ascii_letters + string.digits
return "".join(random.choice(letters_and_digits) for _ in range(length))
generated_reset_code = generate_code()
reset_email_invite_uuid = f"reset_email_invite_code_{uuid.uuid4()}"
ttl = int(datetime.now().timestamp()) + 60 * 60 * 1 # 1 hour
resetCodeObject = {
"reset_code": generated_reset_code,
"reset_email_invite_uuid": reset_email_invite_uuid,
"reset_code_expires": ttl,
"reset_code_type": "signup",
"created_at": datetime.now().isoformat(),
"created_by": user.user_uuid,
"org_uuid": org.org_uuid,
}
r.set(
f"{reset_email_invite_uuid}:user:{user.user_uuid}:org:{org.org_uuid}:code:{generated_reset_code}",
json.dumps(resetCodeObject),
ex=ttl,
)
user = UserRead.from_orm(user)
org = OrganizationRead.from_orm(org)
# Send reset code via email
isEmailSent = send_password_reset_email(
reset_email_invite_uuid=reset_email_invite_uuid,
user=user,
organization=org,
email=user.email,
)
if not isEmailSent:
raise HTTPException(
status_code=500,
detail="Issue with sending reset code",
)
return "Reset code sent"
async def change_password_with_reset_code(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
new_password: str,
org_id: int,
email: EmailStr,
reset_code: str,
):
# Get user
statement = select(User).where(User.email == email)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# Get org
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(
status_code=400,
detail="Organization not found",
)
# Redis init
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Get reset code
reset_code_key = f"*:user:{user.user_uuid}:org:{org.org_uuid}:code:{reset_code}"
keys = r.keys(reset_code_key)
if not keys:
raise HTTPException(
status_code=400,
detail="Reset code not found",
)
# Get reset code object
reset_code_value = r.get(keys[0])
if reset_code_value is None:
raise HTTPException(
status_code=400,
detail="Reset code value not found",
)
reset_code_object = json.loads(reset_code_value)
# Check if reset code is expired
if reset_code_object["reset_code_expires"] < int(datetime.now().timestamp()):
raise HTTPException(
status_code=400,
detail="Reset code expired",
)
# Change password
user.password = await security_hash_password(new_password)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Delete reset code
r.delete(keys[0])
return "Password changed"

View file

@ -3,6 +3,9 @@ from typing import Literal
from uuid import uuid4
from fastapi import HTTPException, Request, UploadFile, status
from sqlmodel import Session, select
from src.services.users.emails import (
send_account_creation_email,
)
from src.services.orgs.invites import get_invite_code
from src.services.users.avatars import upload_avatar
from src.db.roles import Role, RoleRead
@ -102,6 +105,12 @@ async def create_user(
user = UserRead.from_orm(user)
# Send Account creation email
send_account_creation_email(
user=user,
email=user.email,
)
return user
@ -182,6 +191,12 @@ async def create_user_without_org(
user = UserRead.from_orm(user)
# Send Account creation email
send_account_creation_email(
user=user,
email=user.email,
)
return user
@ -331,7 +346,6 @@ async def update_user_password(
return user
async def read_user_by_id(
request: Request,
db_session: Session,