Skip to content

Commit

Permalink
feat: implement email verification process; update user model and add…
Browse files Browse the repository at this point in the history
… verification email functionality
  • Loading branch information
hackerman70000 committed Feb 1, 2025
1 parent c11ef72 commit 3ce8ded
Show file tree
Hide file tree
Showing 13 changed files with 524 additions and 51 deletions.
3 changes: 2 additions & 1 deletion backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

db = SQLAlchemy()
migrate = Migrate()
CORS_ORIGIN_WHITELIST = ["http://localhost"]


def create_app(config_class=Config):
app = Flask(__name__, static_url_path="/static", static_folder="static")
CORS(app)
CORS(app, resources={r"/api/*": {"origins": "http://localhost"}})
app.config.from_object(config_class)

# Initialize database
Expand Down
16 changes: 13 additions & 3 deletions backend/app/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
import secrets
from datetime import datetime, timedelta, timezone

from app import db

Expand All @@ -8,8 +9,17 @@ class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(256), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
password = db.Column(db.String(255), nullable=False)
email_verified = db.Column(db.Boolean, default=False)
verification_token = db.Column(db.String(100), unique=True)
verification_token_expires = db.Column(db.DateTime(timezone=True))

def generate_verification_token(self):
self.verification_token = secrets.token_urlsafe(32)
self.verification_token_expires = datetime.now(timezone.utc) + timedelta(
hours=24
)
return self.verification_token


class Product(db.Model):
Expand Down
158 changes: 145 additions & 13 deletions backend/app/routes/auth.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import re
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from functools import wraps
from json import loads

from config import Config
from pyseto import Key, Paseto, VerifyError, DecryptError
from flask import Blueprint, jsonify, request
from pyseto import DecryptError, Key, Paseto, VerifyError
from werkzeug.security import check_password_hash, generate_password_hash
from json import loads

from app.models import User, db
from app.utils import send_verification_email

auth_bp = Blueprint("auth", __name__)

Expand Down Expand Up @@ -180,25 +181,142 @@ def register():
), 409

hashed_password = generate_password_hash(password)
new_user = User(username=username, email=email, password=hashed_password)
new_user = User(
username=username,
email=email,
password=hashed_password,
email_verified=False,
)

new_user.generate_verification_token()

db.session.add(new_user)
db.session.commit()

if send_verification_email(new_user):
return jsonify(
{
"message": "Registration successful",
"details": "Please check your email to verify your account",
"user": {
"id": new_user.id,
"username": new_user.username,
"email": new_user.email,
"email_verified": new_user.email_verified,
},
}
), 201
else:
return jsonify(
{
"message": "Registration successful but verification email failed",
"details": "Please contact support to verify your email",
}
), 201

except Exception as e:
db.session.rollback()
return jsonify({"message": "Registration failed", "details": str(e)}), 500


@auth_bp.route("/verify-email/<token>", methods=["GET"])
def verify_email(token):
try:
user = User.query.filter_by(verification_token=token).first()

if not user:
return jsonify(
{
"message": "Invalid verification token",
"details": "Token not found or already used",
}
), 404

if user.email_verified:
return jsonify(
{
"message": "Email already verified",
"details": "Your email has already been verified",
}
), 400

if user.verification_token_expires < datetime.now(timezone.utc):
return jsonify(
{
"message": "Verification token expired",
"details": "Please request a new verification email",
}
), 400

user.email_verified = True
user.verification_token = None
user.verification_token_expires = None
db.session.commit()

return jsonify(
{
"message": "Registration successful",
"user": {
"id": new_user.id,
"username": new_user.username,
"email": new_user.email,
},
"message": "Email verified successfully",
"details": "You can now log in to your account",
}
), 201
)

except Exception as e:
db.session.rollback()
return jsonify({"message": "Registration failed", "details": str(e)}), 500
return jsonify({"message": "Email verification failed", "details": str(e)}), 500


@auth_bp.route("/resend-verification", methods=["POST"])
def resend_verification():
try:
data = request.get_json()

if not data or "email" not in data:
return jsonify(
{"message": "Invalid request", "details": "Email is required"}
), 400

email = str(data["email"]).strip().lower()
user = User.query.filter_by(email=email).first()

if not user:
return jsonify(
{
"message": "User not found",
"details": "No account found with this email",
}
), 404

if user.email_verified:
return jsonify(
{
"message": "Email already verified",
"details": "Your email has already been verified",
}
), 400

user.generate_verification_token()
db.session.commit()

if send_verification_email(user):
return jsonify(
{
"message": "Verification email sent",
"details": "Please check your email to verify your account",
}
)
else:
return jsonify(
{
"message": "Failed to send verification email",
"details": "Please try again later",
}
), 500

except Exception as e:
db.session.rollback()
return jsonify(
{"message": "Failed to resend verification email", "details": str(e)}
), 500


@auth_bp.route("/login", methods=["POST"])
Expand All @@ -220,6 +338,14 @@ def login():
{"message": "Authentication failed", "details": "User not found"}
), 401

if not user.email_verified:
return jsonify(
{
"message": "Email not verified",
"details": "Please verify your email before logging in",
}
), 401

if not check_password_hash(user.password, password):
return jsonify(
{"message": "Authentication failed", "details": "Invalid password"}
Expand All @@ -236,7 +362,12 @@ def login():
{
"message": "Login successful",
"token": token.decode(),
"user": {"id": user.id, "username": user.username, "email": user.email},
"user": {
"id": user.id,
"username": user.username,
"email": user.email,
"email_verified": user.email_verified,
},
}
)

Expand All @@ -253,6 +384,7 @@ def get_current_user(current_user):
"id": current_user.id,
"username": current_user.username,
"email": current_user.email,
"email_verified": current_user.email_verified,
}
)
except Exception as e:
Expand Down
43 changes: 43 additions & 0 deletions backend/app/templates/email/verify_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { text-align: center; padding: 20px 0; }
.button {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}
.footer { text-align: center; margin-top: 30px; font-size: 0.9em; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Verify Your Email</h1>
</div>

<p>Hello {{ username }},</p>
<p>Thank you for registering. Please click the button below to verify your email address:</p>

<div style="text-align: center;">
<a href="{{ verification_url }}" class="button">Verify Email</a>
</div>

<p>Or copy and paste this link in your browser:</p>
<p>{{ verification_url }}</p>

<p>This link will expire in 24 hours.</p>

<div class="footer">
<p>If you didn't create an account, please ignore this email.</p>
</div>
</div>
</body>
</html>
54 changes: 46 additions & 8 deletions backend/app/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from flask import render_template
from flask import current_app, render_template
from flask_mail import Mail, Message
from flask import current_app

mail = Mail()


def send_order_confirmation_email(order):
"""Send order confirmation email with receipt to customer"""
try:
Expand All @@ -14,10 +14,10 @@ def send_order_confirmation_email(order):
)

msg.html = render_template(
"email/order_confirmation.html",
order=order,
user=order.user,
items=order.items
"email/order_confirmation.html",
order=order,
user=order.user,
items=order.items,
)

items_text = "\n".join(
Expand All @@ -33,7 +33,7 @@ def send_order_confirmation_email(order):
Thank you for your order!
Order Details:
Date: {order.created_at.strftime('%Y-%m-%d %H:%M:%S')}
Date: {order.created_at.strftime("%Y-%m-%d %H:%M:%S")}
Items:
{items_text}
Expand All @@ -47,4 +47,42 @@ def send_order_confirmation_email(order):
return True
except Exception as e:
current_app.logger.error(f"Failed to send order confirmation email: {str(e)}")
return False
return False


def send_verification_email(user):
"""Send email verification link to user"""
try:
current_app.logger.info(f"Attempting to send email to: {user.email}")

verification_url = f"{current_app.config['FRONTEND_URL']}/verify-email?token={user.verification_token}"

msg = Message(
"Verify Your Email",
sender=current_app.config["MAIL_DEFAULT_SENDER"],
recipients=[user.email],
)

msg.html = render_template(
"email/verify_email.html",
username=user.username,
verification_url=verification_url,
)

msg.body = f"""
Hello {user.username},
Thank you for registering. Please verify your email by clicking the link below:
{verification_url}
This link will expire in 24 hours.
If you didn't create an account, please ignore this email.
"""

mail.send(msg)
return True
except Exception as e:
current_app.logger.error(f"Failed to send verification email: {str(e)}")
return False
2 changes: 2 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from datetime import timedelta

from dotenv import load_dotenv

load_dotenv()
Expand All @@ -11,6 +12,7 @@ class Config:
SECRET_KEY = os.getenv("SECRET_KEY")
TOKEN_SECRET_KEY = os.getenv("TOKEN_SECRET_KEY")
TOKEN_ACCESS_TOKEN_EXPIRES = timedelta(hours=24)
FRONTEND_URL = os.getenv("FRONTEND_URL")

MAIL_SERVER = os.getenv("MAIL_SERVER")
MAIL_PORT = int(os.getenv("MAIL_PORT"))
Expand Down
Loading

0 comments on commit 3ce8ded

Please sign in to comment.