From 809d22b5b7a172f84b288e4d6981c103ce003744 Mon Sep 17 00:00:00 2001 From: Sarasaidin Date: Thu, 13 Aug 2026 14:44:57 +0800 Subject: [PATCH] Improve WellAI email verification --- server/main.py | 6 + server/routers/authentication.py | 467 +++++++++++++++++++++++++-- server/static/images/wellai-logo.png | Bin 0 -> 16009 bytes 3 files changed, 446 insertions(+), 27 deletions(-) create mode 100644 server/static/images/wellai-logo.png diff --git a/server/main.py b/server/main.py index 14969994..7c795681 100644 --- a/server/main.py +++ b/server/main.py @@ -7,6 +7,7 @@ import os from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from .routers import health_prediction, authentication, users, admin @@ -26,6 +27,11 @@ ] app = FastAPI() +app.mount( + "/static", + StaticFiles(directory="server/static"), + name="static", +) app.add_middleware( CORSMiddleware, allow_origins=ORIGINS, diff --git a/server/routers/authentication.py b/server/routers/authentication.py index ab4b529a..e612e574 100644 --- a/server/routers/authentication.py +++ b/server/routers/authentication.py @@ -199,23 +199,154 @@ async def register(user_reg: UserRegistrationDetails, def _send_validation_email(user: UserAccount, token: str): - """Send an email-validation link to the newly registered user.""" + """Send a branded email-validation link to the newly registered user.""" BACKEND_URL = os.getenv( "BACKEND_URL", "https://shp-backend.onrender.com" ) validation_url = f"{BACKEND_URL}/validate-email?token={token}" - email_subject = "Validate your account" + + logo_url = f"{BACKEND_URL}/static/images/wellai-logo.png" + + email_subject = "Verify your WellAI account" + email_content = f""" - -

Welcome to Smart Health Predictive!

-

Please click the link below to validate your email address:

- {validation_url} + +
+ + +
+ WellAI +
+ + +
+

+ Verify your WellAI account +

+ +

+ Registration successful! +

+ +

+ Thank you for creating your WellAI account. + Please verify your email address to complete + your account setup. +

+ + +
+ + Verify my email + +
+ +

+ If you did not create a WellAI account, + you can safely ignore this email. +

+ +

+ For your security, please do not forward this + email or share your verification link. +

+
+ + +
+

+ Please do not reply to this email. +

+ +

+ + Privacy Notice + +

+ +

+ © 2026 WellAI Sdn. Bhd. All rights reserved. +

+
+ +
""" + send_email( recipient=user.Email, subject=email_subject, @@ -227,10 +358,152 @@ def _send_validation_email(user: UserAccount, token: str): @router.get("/validate-email") async def validate_email_address(token: str, db_conn: Session = Depends(get_db)): """Validate a user's email address via a signed token sent by email.""" - validation_failure_exception = HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Invalid or expired validation token." - ) + def validation_error_response(): + html_content = """ + + + + + + Verification Link Invalid - WellAI + + + + +
+ +
+
WellAI
+
Love Yourself
+
+ +
+
!
+ +

Verification Link Invalid

+ +

+ This email verification link is no longer valid. + It may have expired or already been used. +

+ +

+ Please request a new verification email to continue + setting up your WellAI account. +

+
+ + + +
+ + + """ + + return HTMLResponse( + content=html_content, + status_code=status.HTTP_400_BAD_REQUEST + ) # Find the token in the database validation_token_entry = db_conn.query( @@ -238,18 +511,18 @@ async def validate_email_address(token: str, db_conn: Session = Depends(get_db)) # Check if the token exists if not validation_token_entry: - raise validation_failure_exception + return validation_error_response() # Check if the token has expired if validation_token_entry.ExpiresAt < datetime.utcnow(): - raise validation_failure_exception + return validation_error_response() # Get the user associated with the token user = db_conn.query(UserAccount).filter_by( UserID=validation_token_entry.UserID).first() if not user: # This should not happen if database integrity is maintained - raise validation_failure_exception + return validation_error_response() # Update the user's validation status user.IsValidated = True @@ -266,21 +539,161 @@ async def validate_email_address(token: str, db_conn: Session = Depends(get_db)) description=f"Email successfully validated.") html_content = """ - - - Email Validation - - - -
-

Email Validated Successfully!

-

Your email has been successfully validated. You can now close this window and log in to your account.

+ + + + + + Email Verified - WellAI + + + + +
+ +
+
WellAI
+
Love Yourself
- + +
+
+ +

Email Verified Successfully!

+ +

+ Your email address has been successfully verified. +

+ +

+ Your WellAI account is now ready. You can close this window + and return to the WellAI application to log in. +

+
+ + + +
+ """ return HTMLResponse(content=html_content) diff --git a/server/static/images/wellai-logo.png b/server/static/images/wellai-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2b51fb2fb13e0a70a1d5f61475f0acd4dccddd26 GIT binary patch literal 16009 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

X$XVPWC_>7LA*moSvw81N#=?@6Ke~W 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