Added password-change and forgotten password

This commit is contained in:
Mawoka
2022-04-08 19:40:43 +02:00
parent 256ef3a37e
commit 67851f0cca
11 changed files with 892 additions and 319 deletions
+17 -1
View File
@@ -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>
+43 -3
View File
@@ -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"}