Skip to content

Commit f917d03

Browse files
authored
Feature/reset password (#289)
* WIP register - not finished * register fixed errors * register fixed flake-8 * register fixed tests flake8 * register fixed tests issues * register flake8 fix * register flake8 more fixes * fixed CR suggestions * first commit * fixed check_jwt_token * Not Finshed * minor fix * starting dependancy * dependancies working * redirectin and user messages * async fixing * exception handler * quary parameters * Documentation added * fixed pep8 * pep8 more fixes * pep8 config fix * pep8 jwt fix * pep8 jwt final fix * pep8 final fix * CR fixes, tests added * flake8 fixes * flake8 fixes2 * updating requirements * test added * flake8 fix * CR fixing * flake8 fixes * flake8 fix2 * flake8 fixes3 * flake8 fixes4 * flake8 fixes5 * CR fix * Revert "CR fix" This reverts commit 8fbe36a. * CR fixes * works, before tests * testing are not done * tests not finished * first push * before pull from develop * docstring added * send_reset_password_mail function updated * tests fixed * CR and more fixes * CR fixes * upgraded security in dependencies * CR and get_jwt_token function fixes * CR fixes * CR small fix * html fix
1 parent 9ace889 commit f917d03

File tree

17 files changed

+888
-118
lines changed

17 files changed

+888
-118
lines changed

app/config.py.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ email_conf = ConnectionConfig(
6767
JWT_KEY = "JWT_KEY_PLACEHOLDER"
6868
JWT_ALGORITHM = "HS256"
6969
JWT_MIN_EXP = 60 * 24 * 7
70+
7071
templates = Jinja2Templates(directory=os.path.join("app", "templates"))
7172

7273
# application name

app/database/schemas.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def username_length(cls, username: str) -> Union[ValueError, str]:
7575
"""Validating username length is legal"""
7676
if not (MIN_FIELD_LENGTH < len(username) < MAX_FIELD_LENGTH):
7777
raise ValueError("must contain between 3 to 20 charactars")
78+
if username.startswith("@"):
79+
raise ValueError("username can not start with '@'")
7880
return username
7981

8082
@validator("password")

app/internal/email.py

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,26 @@
77
from pydantic.errors import EmailError
88
from sqlalchemy.orm.session import Session
99

10-
from app.config import (CALENDAR_HOME_PAGE, CALENDAR_REGISTRATION_PAGE,
11-
CALENDAR_SITE_NAME, email_conf, templates)
10+
from app.config import (
11+
CALENDAR_HOME_PAGE,
12+
CALENDAR_REGISTRATION_PAGE,
13+
CALENDAR_SITE_NAME,
14+
DOMAIN,
15+
email_conf,
16+
)
1217
from app.database.models import Event, User
18+
from app.dependencies import templates
19+
from app.internal.security.schema import ForgotPassword
1320

1421
mail = FastMail(email_conf)
1522

1623

1724
def send(
18-
session: Session, event_used: int, user_to_send: int,
19-
title: str, background_tasks: BackgroundTasks = BackgroundTasks
25+
session: Session,
26+
event_used: int,
27+
user_to_send: int,
28+
title: str,
29+
background_tasks: BackgroundTasks = BackgroundTasks,
2030
) -> bool:
2131
"""This function is being used to send emails in the background.
2232
It takes an event and a user and it sends the event to the user.
@@ -32,10 +42,8 @@ def send(
3242
Returns:
3343
bool: Returns True if the email was sent, else returns False.
3444
"""
35-
event_used = session.query(Event).filter(
36-
Event.id == event_used).first()
37-
user_to_send = session.query(User).filter(
38-
User.id == user_to_send).first()
45+
event_used = session.query(Event).filter(Event.id == event_used).first()
46+
user_to_send = session.query(User).filter(User.id == user_to_send).first()
3947
if not user_to_send or not event_used:
4048
return False
4149
if not verify_email_pattern(user_to_send.email):
@@ -45,18 +53,21 @@ def send(
4553
recipients = {"email": [user_to_send.email]}.get("email")
4654
body = f"begins at:{event_used.start} : {event_used.content}"
4755

48-
background_tasks.add_task(send_internal,
49-
subject=subject,
50-
recipients=recipients,
51-
body=body)
56+
background_tasks.add_task(
57+
send_internal,
58+
subject=subject,
59+
recipients=recipients,
60+
body=body,
61+
)
5262
return True
5363

5464

55-
def send_email_invitation(sender_name: str,
56-
recipient_name: str,
57-
recipient_mail: str,
58-
background_tasks: BackgroundTasks = BackgroundTasks
59-
) -> bool:
65+
def send_email_invitation(
66+
sender_name: str,
67+
recipient_name: str,
68+
recipient_mail: str,
69+
background_tasks: BackgroundTasks = BackgroundTasks,
70+
) -> bool:
6071
"""
6172
This function takes as parameters the sender's name,
6273
the recipient's name and his email address, configuration, and
@@ -81,28 +92,35 @@ def send_email_invitation(sender_name: str,
8192
return False
8293

8394
template = templates.get_template("invite_mail.html")
84-
html = template.render(recipient=recipient_name, sender=sender_name,
85-
site_name=CALENDAR_SITE_NAME,
86-
registration_link=CALENDAR_REGISTRATION_PAGE,
87-
home_link=CALENDAR_HOME_PAGE,
88-
addr_to=recipient_mail)
95+
html = template.render(
96+
recipient=recipient_name,
97+
sender=sender_name,
98+
site_name=CALENDAR_SITE_NAME,
99+
registration_link=CALENDAR_REGISTRATION_PAGE,
100+
home_link=CALENDAR_HOME_PAGE,
101+
addr_to=recipient_mail,
102+
)
89103

90104
subject = "Invitation"
91105
recipients = [recipient_mail]
92106
body = html
93107
subtype = "html"
94108

95-
background_tasks.add_task(send_internal,
96-
subject=subject,
97-
recipients=recipients,
98-
body=body,
99-
subtype=subtype)
109+
background_tasks.add_task(
110+
send_internal,
111+
subject=subject,
112+
recipients=recipients,
113+
body=body,
114+
subtype=subtype,
115+
)
100116
return True
101117

102118

103-
def send_email_file(file_path: str,
104-
recipient_mail: str,
105-
background_tasks: BackgroundTasks = BackgroundTasks):
119+
def send_email_file(
120+
file_path: str,
121+
recipient_mail: str,
122+
background_tasks: BackgroundTasks = BackgroundTasks,
123+
):
106124
"""
107125
his function takes as parameters the file's path,
108126
the recipient's email address, configuration, and
@@ -126,19 +144,23 @@ def send_email_file(file_path: str,
126144
body = "file"
127145
file_attachments = [file_path]
128146

129-
background_tasks.add_task(send_internal,
130-
subject=subject,
131-
recipients=recipients,
132-
body=body,
133-
file_attachments=file_attachments)
147+
background_tasks.add_task(
148+
send_internal,
149+
subject=subject,
150+
recipients=recipients,
151+
body=body,
152+
file_attachments=file_attachments,
153+
)
134154
return True
135155

136156

137-
async def send_internal(subject: str,
138-
recipients: List[str],
139-
body: str,
140-
subtype: Optional[str] = None,
141-
file_attachments: Optional[List[str]] = None):
157+
async def send_internal(
158+
subject: str,
159+
recipients: List[str],
160+
body: str,
161+
subtype: Optional[str] = None,
162+
file_attachments: Optional[List[str]] = None,
163+
):
142164
if file_attachments is None:
143165
file_attachments = []
144166

@@ -147,8 +169,10 @@ async def send_internal(subject: str,
147169
recipients=[EmailStr(recipient) for recipient in recipients],
148170
body=body,
149171
subtype=subtype,
150-
attachments=[UploadFile(file_attachment)
151-
for file_attachment in file_attachments])
172+
attachments=[
173+
UploadFile(file_attachment) for file_attachment in file_attachments
174+
],
175+
)
152176

153177
return await send_internal_internal(message)
154178

@@ -177,3 +201,32 @@ def verify_email_pattern(email: str) -> bool:
177201
return True
178202
except EmailError:
179203
return False
204+
205+
206+
async def send_reset_password_mail(
207+
user: ForgotPassword,
208+
background_tasks: BackgroundTasks,
209+
) -> bool:
210+
"""
211+
This function sends a reset password email to user.
212+
:param user: ForgotPassword schema.
213+
Contains user's email address, jwt verifying token.
214+
:param background_tasks: (BackgroundTasks): Function from fastapi that lets
215+
you apply tasks in the background.
216+
returns True
217+
"""
218+
params = f"?email_verification_token={user.email_verification_token}"
219+
template = templates.get_template("reset_password_mail.html")
220+
html = template.render(
221+
recipient=user.username.lstrip("@"),
222+
link=f"{DOMAIN}/reset-password{params}",
223+
email=user.email,
224+
)
225+
background_tasks.add_task(
226+
send_internal,
227+
subject="Calendar reset password",
228+
recipients=[user.email],
229+
body=html,
230+
subtype="html",
231+
)
232+
return True

app/internal/security/dependencies.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ async def is_logged_in(
2020
"""
2121
A dependency function protecting routes for only logged in user
2222
"""
23-
await get_jwt_token(db, jwt)
23+
jwt_payload = get_jwt_token(jwt)
24+
user_id = jwt_payload.get("user_id")
25+
if not user_id:
26+
raise HTTPException(
27+
status_code=HTTP_401_UNAUTHORIZED,
28+
detail="Your token is not valid. Please log in again",
29+
)
2430
return True
2531

2632

@@ -32,8 +38,9 @@ async def is_manager(
3238
"""
3339
A dependency function protecting routes for only logged in manager
3440
"""
35-
jwt_payload = await get_jwt_token(db, jwt)
36-
if jwt_payload.get("is_manager"):
41+
jwt_payload = get_jwt_token(jwt)
42+
user_id = jwt_payload.get("user_id")
43+
if jwt_payload.get("is_manager") and user_id:
3744
return True
3845
raise HTTPException(
3946
status_code=HTTP_401_UNAUTHORIZED,
@@ -51,7 +58,7 @@ async def current_user_from_db(
5158
Returns logged in User object.
5259
A dependency function protecting routes for only logged in user.
5360
"""
54-
jwt_payload = await get_jwt_token(db, jwt)
61+
jwt_payload = get_jwt_token(jwt)
5562
username = jwt_payload.get("sub")
5663
user_id = jwt_payload.get("user_id")
5764
db_user = await User.get_by_username(db, username=username)
@@ -74,7 +81,12 @@ async def current_user(
7481
Returns logged in User object.
7582
A dependency function protecting routes for only logged in user.
7683
"""
77-
jwt_payload = await get_jwt_token(db, jwt)
84+
jwt_payload = get_jwt_token(jwt)
7885
username = jwt_payload.get("sub")
7986
user_id = jwt_payload.get("user_id")
87+
if not user_id:
88+
raise HTTPException(
89+
status_code=HTTP_401_UNAUTHORIZED,
90+
detail="Your token is not valid. Please log in again",
91+
)
8092
return schema.CurrentUser(user_id=user_id, username=username)

app/internal/security/ouath2.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy.orm import Session
1010
from starlette.requests import Request
1111
from starlette.responses import RedirectResponse
12-
from starlette.status import HTTP_401_UNAUTHORIZED
12+
from starlette.status import HTTP_302_FOUND, HTTP_401_UNAUTHORIZED
1313

1414
from app.config import JWT_ALGORITHM, JWT_KEY, JWT_MIN_EXP
1515
from app.database.models import User
@@ -20,7 +20,20 @@
2020
oauth_schema = OAuth2PasswordBearer(tokenUrl="/login")
2121

2222

23-
def get_hashed_password(password: bytes) -> str:
23+
async def update_password(
24+
db: Session,
25+
username: str,
26+
user_password: str,
27+
) -> None:
28+
"""Updating User password in database"""
29+
db_user = await User.get_by_username(db=db, username=username)
30+
hashed_password = get_hashed_password(user_password)
31+
db_user.password = hashed_password
32+
db.commit()
33+
return
34+
35+
36+
def get_hashed_password(password: str) -> str:
2437
"""Hashing user password"""
2538
return pwd_context.hash(password)
2639

@@ -30,17 +43,47 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
3043
return pwd_context.verify(plain_password, hashed_password)
3144

3245

46+
async def is_email_compatible_to_username(
47+
db: Session,
48+
user: schema.ForgotPassword,
49+
email: bool = False,
50+
) -> Union[schema.ForgotPassword, bool]:
51+
"""
52+
Verifying database record by username.
53+
Comparing given email to database record,
54+
"""
55+
db_user = await User.get_by_username(
56+
db=db,
57+
username=user.username.lstrip("@"),
58+
)
59+
if not db_user:
60+
return False
61+
if db_user.email == user.email:
62+
return schema.ForgotPassword(
63+
username=user.username,
64+
user_id=db_user.id,
65+
email=db_user.email,
66+
)
67+
return False
68+
69+
3370
async def authenticate_user(
3471
db: Session,
35-
new_user: schema.LoginUser,
72+
user: schema.LoginUser,
3673
) -> Union[schema.LoginUser, bool]:
37-
"""Verifying user is in database and password is correct"""
38-
db_user = await User.get_by_username(db=db, username=new_user.username)
39-
if db_user and verify_password(new_user.password, db_user.password):
74+
"""
75+
Verifying database record by username.
76+
Comparing given password to database record,
77+
varies with which function called this action.
78+
"""
79+
db_user = await User.get_by_username(db=db, username=user.username)
80+
if not db_user:
81+
return False
82+
elif verify_password(user.password, db_user.password):
4083
return schema.LoginUser(
4184
user_id=db_user.id,
4285
is_manager=db_user.is_manager,
43-
username=new_user.username,
86+
username=user.username,
4487
password=db_user.password,
4588
)
4689
return False
@@ -63,8 +106,7 @@ def create_jwt_token(
63106
return jwt_token
64107

65108

66-
async def get_jwt_token(
67-
db: Session,
109+
def get_jwt_token(
68110
token: str = Depends(oauth_schema),
69111
path: Union[bool, str] = None,
70112
) -> User:
@@ -121,6 +163,6 @@ async def auth_exception_handler(
121163
"""
122164
paramas = f"?next={exc.headers}&message={exc.detail}"
123165
url = f"/login{paramas}"
124-
response = RedirectResponse(url=url)
166+
response = RedirectResponse(url=url, status_code=HTTP_302_FOUND)
125167
response.delete_cookie("Authorization")
126168
return response

0 commit comments

Comments
 (0)