mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: email resetting backend code
This commit is contained in:
parent
ae2367bdea
commit
15677a6946
5 changed files with 308 additions and 4 deletions
|
|
@ -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(
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
45
apps/api/src/services/users/emails.py
Normal file
45
apps/api/src/services/users/emails.py
Normal 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>
|
||||
""",
|
||||
)
|
||||
203
apps/api/src/services/users/password_reset.py
Normal file
203
apps/api/src/services/users/password_reset.py
Normal 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"
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue