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 typing import Literal
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
|
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
|
||||||
|
from pydantic import EmailStr
|
||||||
from sqlmodel import Session
|
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.services.orgs.orgs import get_org_join_mechanism
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
from src.core.events.database import get_db_session
|
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)
|
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"])
|
@router.delete("/user_id/{user_id}", tags=["users"])
|
||||||
async def api_delete_user(
|
async def api_delete_user(
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import json
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import uuid
|
import uuid
|
||||||
|
from pydantic import EmailStr
|
||||||
import redis
|
import redis
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
@ -139,7 +140,7 @@ async def get_invite_codes(
|
||||||
|
|
||||||
for invite_code in invite_codes:
|
for invite_code in invite_codes:
|
||||||
invite_code = r.get(invite_code)
|
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)
|
invite_codes_list.append(invite_code)
|
||||||
|
|
||||||
return invite_codes_list
|
return invite_codes_list
|
||||||
|
|
@ -258,7 +259,7 @@ def send_invite_email(
|
||||||
org: OrganizationRead,
|
org: OrganizationRead,
|
||||||
invite_code_uuid: str,
|
invite_code_uuid: str,
|
||||||
user: UserRead,
|
user: UserRead,
|
||||||
email: str,
|
email: EmailStr,
|
||||||
):
|
):
|
||||||
LH_CONFIG = get_learnhouse_config()
|
LH_CONFIG = get_learnhouse_config()
|
||||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||||
|
|
@ -284,7 +285,7 @@ def send_invite_email(
|
||||||
# Send email
|
# Send email
|
||||||
if invite:
|
if invite:
|
||||||
invite = r.get(invite[0])
|
invite = r.get(invite[0])
|
||||||
invite = json.loads(invite)
|
invite = json.loads(invite) # type: ignore
|
||||||
|
|
||||||
# send email
|
# send email
|
||||||
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 uuid import uuid4
|
||||||
from fastapi import HTTPException, Request, UploadFile, status
|
from fastapi import HTTPException, Request, UploadFile, status
|
||||||
from sqlmodel import Session, select
|
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.orgs.invites import get_invite_code
|
||||||
from src.services.users.avatars import upload_avatar
|
from src.services.users.avatars import upload_avatar
|
||||||
from src.db.roles import Role, RoleRead
|
from src.db.roles import Role, RoleRead
|
||||||
|
|
@ -102,6 +105,12 @@ async def create_user(
|
||||||
|
|
||||||
user = UserRead.from_orm(user)
|
user = UserRead.from_orm(user)
|
||||||
|
|
||||||
|
# Send Account creation email
|
||||||
|
send_account_creation_email(
|
||||||
|
user=user,
|
||||||
|
email=user.email,
|
||||||
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -182,6 +191,12 @@ async def create_user_without_org(
|
||||||
|
|
||||||
user = UserRead.from_orm(user)
|
user = UserRead.from_orm(user)
|
||||||
|
|
||||||
|
# Send Account creation email
|
||||||
|
send_account_creation_email(
|
||||||
|
user=user,
|
||||||
|
email=user.email,
|
||||||
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -331,7 +346,6 @@ async def update_user_password(
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
async def read_user_by_id(
|
async def read_user_by_id(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue