✨ Added password-change and forgotten password
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import smtplib
|
||||
import ssl
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
@@ -5,7 +6,7 @@ from email.mime.text import MIMEText
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
from classquiz.config import settings
|
||||
from classquiz.config import settings, redis
|
||||
from classquiz.db.models import User
|
||||
|
||||
settings = settings()
|
||||
@@ -41,3 +42,18 @@ async def send_register_email(email: str):
|
||||
token=user.verify_key
|
||||
)
|
||||
_sendMail(template=template, to=email, subject='Verify your email')
|
||||
|
||||
|
||||
async def send_forgotten_password_email(email: str):
|
||||
user = await User.objects.get_or_none(email=email)
|
||||
if user is None:
|
||||
raise ValueError('User not found')
|
||||
template = jinja.get_template('forgotten_password.jinja2')
|
||||
token = os.urandom(32).hex()
|
||||
template = await template.render_async(
|
||||
base_url=settings.root_address,
|
||||
token=token
|
||||
)
|
||||
await redis.set(f"reset_passwd:{token}", str(user.id), ex=3600)
|
||||
_sendMail(template=template, to=email, subject='Reset your password')
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<div style='box-sizing: border-box;display: block;max-width: 600px;margin: 0 auto;padding: 10px'><span
|
||||
style='color: transparent;height: 0;max-height: 0;max-width: 0;opacity: 0;overflow: hidden;mso-hide: all;visibility: hidden;width: 0'>Let's confirm your email address.</span>
|
||||
<div style='box-sizing: border-box;width: 100%;margin-bottom: 30px;margin-top: 15px'>
|
||||
<table
|
||||
style='box-sizing: border-box;width: 100%;border-spacing: 0;mso-table-rspace: 0pt;mso-table-lspace: 0pt;border-collapse: separate !important'
|
||||
width='100%'>
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style='box-sizing: border-box;width: 100%;margin-bottom: 30px;background: #ffffff;border: 1px solid #f0f0f0'>
|
||||
<table
|
||||
style='box-sizing: border-box;width: 100%;border-spacing: 0;mso-table-rspace: 0pt;mso-table-lspace: 0pt;border-collapse: separate !important'
|
||||
width='100%'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="box-sizing: border-box;font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size: 16px;vertical-align: top;padding: 30px"
|
||||
valign='top'>
|
||||
<table
|
||||
style='box-sizing: border-box;width: 100%;border-spacing: 0;mso-table-rspace: 0pt;mso-table-lspace: 0pt;border-collapse: separate !important'
|
||||
width='100%'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="box-sizing: border-box;padding: 0;font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size: 16px;vertical-align: top"
|
||||
valign='top'><h2
|
||||
style="margin: 0;margin-bottom: 30px;font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-weight: 300;line-height: 1.5;font-size: 24px;color: #294661 !important">
|
||||
You're on your way!<br> Let's reset your password.</h2>
|
||||
<p style="margin: 0;margin-bottom: 30px;color: #294661;font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size: 16px;font-weight: 300">
|
||||
By clicking on the following link, you are going to reset your password.</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="box-sizing: border-box;padding: 0;font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size: 16px;vertical-align: top"
|
||||
valign='top'>
|
||||
<table
|
||||
style='box-sizing: border-box;border-spacing: 0;mso-table-rspace: 0pt;mso-table-lspace: 0pt;width: 100%;border-collapse: separate !important'
|
||||
width='100%' cellspacing='0' cellpadding='0'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="box-sizing: border-box;padding: 0;font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size: 16px;vertical-align: top;padding-bottom: 15px"
|
||||
valign='top' align='center'>
|
||||
<table
|
||||
style='box-sizing: border-box;border-spacing: 0;mso-table-rspace: 0pt;mso-table-lspace: 0pt;width: auto;border-collapse: separate !important'
|
||||
cellspacing='0' cellpadding='0'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="box-sizing: border-box;padding: 0;font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size: 16px;vertical-align: top;background-color: #348eda;border-radius: 2px;text-align: center;background-color: #348eda"
|
||||
valign='top' align='center'><a
|
||||
href='{{ base_url }}/account/password-reset?token={{ token }}'
|
||||
style='box-sizing: border-box;font-weight: 400;text-decoration: none;display: inline-block;margin: 0;color: #ffffff;background: linear-gradient(90deg, #009444, #39b54a, #8dc63f);border: solid 1px black;border-radius: 2px;font-size: 14px;padding: 12px 45px'
|
||||
rel='external nofollow noopener noreferrer' target='_blank'
|
||||
tabindex='-1'>Confirm Password Reset</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'footer.jinja2' %}
|
||||
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@ from classquiz.auth import *
|
||||
from classquiz.config import redis
|
||||
from classquiz.cache import clear_cache_for_account
|
||||
from classquiz.db.models import *
|
||||
from classquiz.emails import send_register_email
|
||||
from classquiz.emails import send_register_email, send_forgotten_password_email
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -18,6 +18,11 @@ route_user = User.get_pydantic(
|
||||
exclude={"id": ..., "verified": ..., "verify_key": ..., "created_at": ..., "usersessions": ...})
|
||||
|
||||
|
||||
async def _sign_out_everywhere(user: User) -> None:
|
||||
await UserSession.objects.filter(user=user).delete()
|
||||
await clear_cache_for_account(user)
|
||||
|
||||
|
||||
@router.post("/create", response_model=User,
|
||||
response_model_include={"id": ..., "verified": ..., "email": ...})
|
||||
async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse:
|
||||
@@ -135,8 +140,7 @@ async def change_password(password_data: UpdatePassword, response: Response, use
|
||||
|
||||
@router.delete("/signout-everywhere")
|
||||
async def signout_everywhere(response: Response, user: User = Depends(get_current_user)):
|
||||
await UserSession.objects.filter(user=user).delete()
|
||||
await clear_cache_for_account(user)
|
||||
await _sign_out_everywhere(user)
|
||||
response.delete_cookie("access_token")
|
||||
response.delete_cookie("expiry")
|
||||
response.delete_cookie("rememberme")
|
||||
@@ -147,3 +151,39 @@ async def signout_everywhere(response: Response, user: User = Depends(get_curren
|
||||
@router.get("/me", response_model_exclude={"password", "verify_key", "usersessions"})
|
||||
async def get_me(user: User = Depends(get_current_user)):
|
||||
return user.dict(exclude={"password", "verify_key", "usersessions"})
|
||||
|
||||
|
||||
class ForgotPassword(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgotten_password(forgot_password: ForgotPassword):
|
||||
user = await User.objects.filter(email=forgot_password.email, verified=True).get_or_none()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
await send_forgotten_password_email(user.email)
|
||||
return {"message": "Password reset email sent"}
|
||||
|
||||
|
||||
class ResetPassword(BaseModel):
|
||||
password: str
|
||||
token: str
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password_with_token(reset_password: ResetPassword, response: Response):
|
||||
redis_res = await redis.get(f"reset_passwd:{reset_password.token}")
|
||||
if redis_res is None:
|
||||
raise HTTPException(status_code=400, detail="Invalid token")
|
||||
user = await User.objects.filter(id=uuid.UUID(redis_res)).get_or_none()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=400, detail="Invalid token")
|
||||
user.password = get_password_hash(reset_password.password)
|
||||
await user.update()
|
||||
await _sign_out_everywhere(user)
|
||||
response.delete_cookie("access_token")
|
||||
response.delete_cookie("expiry")
|
||||
response.delete_cookie("rememberme")
|
||||
response.delete_cookie("rememberme_token")
|
||||
return {"message": "Password updated successfully"}
|
||||
|
||||
Reference in New Issue
Block a user