Skip to content

Commit fe7c83e

Browse files
UmairHundekarlucyqqirichieb21YashK2005
authored
Finished log in and sign up for volunteers and participants and linked it with the intake form (#46)
## Notion ticket link <!-- Please replace with your ticket's URL --> [Ticket Name](https://www.notion.so/uwblueprintexecs/Login-Page-1fd10f3fb1dc809686a7ea3b4f7c2ca0?source=copy_link) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * Added sign up and login pages * Added password reset and log in verification * Linked login with intake forms (french versions not available yet) * Added the admin pages (not functional yet) <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> ## Steps to test 1. Create users for volunteers and participants and try logging in 2. Reset password 3. Test the intake form depending on how completed that is 4. Try seeing if all the page transitions works <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> ## What should reviewers focus on? * Should the emails for verification and reset password look different ## Checklist - [ ] My PR name is descriptive and in imperative tense - [ ] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [ ] I have run the appropriate linter(s) - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR --------- Co-authored-by: Lucy Qi <[email protected]> Co-authored-by: richieb21 <[email protected]> Co-authored-by: YashK2005 <[email protected]>
1 parent b8c291d commit fe7c83e

37 files changed

+4348
-136
lines changed

backend/app/interfaces/auth_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,10 @@ def is_authorized_by_email(self, access_token, requested_email):
129129
:rtype: bool
130130
"""
131131
pass
132+
133+
@abstractmethod
134+
def verify_email(self, email):
135+
"""
136+
Verify the email address of the user with the given email
137+
"""
138+
pass

backend/app/middleware/auth_middleware.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ def __init__(self, app: ASGIApp, public_paths: List[str] = None):
1717
self.logger = logging.getLogger(LOGGER_NAME("auth_middleware"))
1818

1919
def is_public_path(self, path: str) -> bool:
20-
return path in self.public_paths
20+
for public_path in self.public_paths:
21+
# Handle parameterized routes by checking if path starts with the pattern
22+
if public_path.endswith("{email}") and path.startswith(public_path.replace("{email}", "")):
23+
return True
24+
# Exact match for non-parameterized routes
25+
if path == public_path:
26+
return True
27+
return False
2128

2229
async def dispatch(self, request: Request, call_next):
2330
if self.is_public_path(request.url.path):

backend/app/models/User.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
class User(Base):
1212
__tablename__ = "users"
1313
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
14-
first_name = Column(String(80), nullable=False)
15-
last_name = Column(String(80), nullable=False)
14+
first_name = Column(String(80), nullable=True)
15+
last_name = Column(String(80), nullable=True)
1616
email = Column(String(120), unique=True, nullable=False)
1717
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
1818
auth_id = Column(String, nullable=False)

backend/app/routes/auth.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter, Depends, HTTPException, Request
1+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
22
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
33

44
from ..schemas.auth import AuthResponse, LoginRequest, RefreshRequest, Token
@@ -68,3 +68,31 @@ async def refresh(refresh_data: RefreshRequest, auth_service: AuthService = Depe
6868
return auth_service.renew_token(refresh_data.refresh_token)
6969
except Exception as e:
7070
raise HTTPException(status_code=401, detail=str(e))
71+
72+
73+
@router.post("/resetPassword/{email}")
74+
async def reset_password(email: str, auth_service: AuthService = Depends(get_auth_service)):
75+
try:
76+
auth_service.reset_password(email)
77+
# Return 204 No Content for successful password reset email sending
78+
return Response(status_code=204)
79+
except Exception:
80+
# Don't reveal if email exists or not for security reasons
81+
# Always return success even if email doesn't exist
82+
return Response(status_code=204)
83+
84+
85+
@router.post("/verify/{email}")
86+
async def verify_email(email: str, auth_service: AuthService = Depends(get_auth_service)):
87+
try:
88+
auth_service.verify_email(email)
89+
return Response(status_code=200)
90+
except ValueError as e:
91+
# Log the error for debugging but don't expose it to the client
92+
print(f"Email verification failed for {email}: {str(e)}")
93+
# Return 404 for user not found instead of 400
94+
return Response(status_code=404)
95+
except Exception as e:
96+
# Log unexpected errors
97+
print(f"Unexpected error during email verification for {email}: {str(e)}")
98+
return Response(status_code=500)

backend/app/schemas/user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ class UserBase(BaseModel):
4040
Base schema for user model with common attributes shared across schemas.
4141
"""
4242

43-
first_name: str = Field(..., min_length=1, max_length=50)
44-
last_name: str = Field(..., min_length=1, max_length=50)
43+
first_name: Optional[str] = Field(None, min_length=0, max_length=50)
44+
last_name: Optional[str] = Field(None, min_length=0, max_length=50)
4545
email: EmailStr
4646
role: UserRole
4747

backend/app/server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"/openapi.json",
2525
"/auth/login",
2626
"/auth/register",
27+
"/auth/resetPassword/{email}",
28+
"/auth/verify/{email}",
2729
"/health",
2830
"/test-middleware-public",
2931
"/email/send-test-email",
@@ -47,6 +49,7 @@ async def lifespan(_: FastAPI):
4749
CORSMiddleware,
4850
allow_origins=[
4951
"http://localhost:3000",
52+
"http://localhost:3002",
5053
"https://uw-blueprint-starter-code.firebaseapp.com",
5154
"https://uw-blueprint-starter-code.web.app",
5255
# TODO: create a separate middleware function to dynamically

backend/app/services/implementations/auth_service.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import logging
2+
import os
23

34
import firebase_admin.auth
5+
import requests
46
from fastapi import HTTPException
57

68
from app.utilities.constants import LOGGER_NAME
@@ -46,10 +48,29 @@ def renew_token(self, refresh_token: str) -> Token:
4648

4749
def reset_password(self, email: str) -> None:
4850
try:
49-
firebase_admin.auth.generate_password_reset_link(email)
51+
# Use Firebase REST API to send password reset email
52+
url = f"https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key={os.getenv('FIREBASE_WEB_API_KEY')}"
53+
data = {
54+
"requestType": "PASSWORD_RESET",
55+
"email": email,
56+
"continueUrl": "http://localhost:3000/set-new-password", # Custom action URL
57+
}
58+
59+
response = requests.post(url, json=data)
60+
response_json = response.json()
61+
62+
if response.status_code != 200:
63+
error_message = response_json.get("error", {}).get("message", "Unknown error")
64+
self.logger.error(f"Failed to send password reset email: {error_message}")
65+
# Don't raise exception for security reasons - don't reveal if email exists
66+
return
67+
68+
self.logger.info(f"Password reset email sent successfully to {email}")
69+
5070
except Exception as e:
5171
self.logger.error(f"Failed to reset password: {str(e)}")
52-
raise
72+
# Don't raise exception for security reasons - don't reveal if email exists
73+
return
5374

5475
def send_email_verification_link(self, email: str) -> None:
5576
try:
@@ -87,3 +108,27 @@ def is_authorized_by_email(self, access_token: str, requested_email: str) -> boo
87108
except Exception as e:
88109
print(f"Authorization error: {str(e)}")
89110
return False
111+
112+
def verify_email(self, email: str):
113+
try:
114+
user = self.user_service.get_user_by_email(email)
115+
if not user:
116+
self.logger.error(f"User not found for email: {email}")
117+
raise ValueError("User not found")
118+
119+
if not user.auth_id:
120+
self.logger.error(f"User {user.id} has no auth_id")
121+
raise ValueError("User has no auth_id")
122+
123+
self.logger.info(f"Updating email verification for user {user.id} with auth_id {user.auth_id}")
124+
firebase_admin.auth.update_user(user.auth_id, email_verified=True)
125+
self.logger.info(f"Successfully verified email for user {user.id}")
126+
127+
except ValueError as e:
128+
# User not found in database - this might happen if there's a timing issue
129+
# between Firebase user creation and database user creation
130+
self.logger.warning(f"User not found in database for email {email}: {str(e)}")
131+
raise
132+
except Exception as e:
133+
self.logger.error(f"Failed to verify email for {email}: {str(e)}")
134+
raise
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""merge heads
2+
3+
Revision ID: 88c4cf2a6bd2
4+
Revises: abcd1234active, d6d4e2e5af85, fef3717e0fc2
5+
Create Date: 2025-07-20 16:06:01.056373
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
# revision identifiers, used by Alembic.
12+
revision: str = "88c4cf2a6bd2"
13+
down_revision: Union[str, None] = ("abcd1234active", "d6d4e2e5af85", "fef3717e0fc2")
14+
branch_labels: Union[str, Sequence[str], None] = None
15+
depends_on: Union[str, Sequence[str], None] = None
16+
17+
18+
def upgrade() -> None:
19+
pass
20+
21+
22+
def downgrade() -> None:
23+
pass
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""set-first-last-name-to-nullable
2+
3+
Revision ID: d6d4e2e5af85
4+
Revises: c9bc2b4d1036
5+
Create Date: 2025-06-11 22:22:13.206761
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = "d6d4e2e5af85"
16+
down_revision: Union[str, None] = "c9bc2b4d1036"
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.alter_column("users", "first_name", existing_type=sa.VARCHAR(length=80), nullable=True)
24+
op.alter_column("users", "last_name", existing_type=sa.VARCHAR(length=80), nullable=True)
25+
# ### end Alembic commands ###
26+
27+
28+
def downgrade() -> None:
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
op.alter_column("users", "last_name", existing_type=sa.VARCHAR(length=80), nullable=False)
31+
op.alter_column("users", "first_name", existing_type=sa.VARCHAR(length=80), nullable=False)
32+
# ### end Alembic commands ###

frontend/APIClients/authAPIClient.js

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)