Basic frontend
This commit is contained in:
@@ -3,12 +3,14 @@ from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
import uuid
|
||||
|
||||
from ...core.database import get_db
|
||||
from ...core.security import verify_password, get_password_hash, create_access_token
|
||||
from ...models.models import User, UserRole
|
||||
from ...models.models import User, UserRole, PasswordResetToken
|
||||
from ...schemas import (
|
||||
UserCreate, UserResponse, Token, LoginRequest, MessageResponse
|
||||
UserCreate, UserResponse, Token, LoginRequest, MessageResponse,
|
||||
ForgotPasswordRequest, ResetPasswordRequest
|
||||
)
|
||||
from ...services.email_service import email_service
|
||||
|
||||
@@ -126,3 +128,92 @@ async def login_json(
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/forgot-password", response_model=MessageResponse)
|
||||
async def forgot_password(
|
||||
request: ForgotPasswordRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Request password reset for a user"""
|
||||
# Find user by email
|
||||
user = db.query(User).filter(User.email == request.email).first()
|
||||
|
||||
if not user or not user.is_active:
|
||||
# Don't reveal if email exists or not for security
|
||||
return {"message": "If an account with this email exists, a password reset link has been sent."}
|
||||
|
||||
# Invalidate any existing reset tokens for this user
|
||||
db.query(PasswordResetToken).filter(
|
||||
PasswordResetToken.user_id == user.id,
|
||||
PasswordResetToken.used == False,
|
||||
PasswordResetToken.expires_at > datetime.utcnow()
|
||||
).update({"used": True})
|
||||
|
||||
# Generate new reset token
|
||||
reset_token = str(uuid.uuid4())
|
||||
expires_at = datetime.utcnow() + timedelta(hours=1) # Token expires in 1 hour
|
||||
|
||||
# Create password reset token
|
||||
db_token = PasswordResetToken(
|
||||
user_id=user.id,
|
||||
token=reset_token,
|
||||
expires_at=expires_at,
|
||||
used=False
|
||||
)
|
||||
|
||||
db.add(db_token)
|
||||
db.commit()
|
||||
|
||||
# Send password reset email (non-blocking, ignore errors)
|
||||
try:
|
||||
await email_service.send_password_reset_email(
|
||||
to_email=user.email,
|
||||
first_name=user.first_name,
|
||||
reset_token=reset_token
|
||||
)
|
||||
except Exception as e:
|
||||
# Log error but don't fail the request
|
||||
print(f"Failed to send password reset email: {e}")
|
||||
|
||||
return {"message": "If an account with this email exists, a password reset link has been sent."}
|
||||
|
||||
|
||||
@router.post("/reset-password", response_model=MessageResponse)
|
||||
async def reset_password(
|
||||
request: ResetPasswordRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Reset password using reset token"""
|
||||
# Find valid reset token
|
||||
reset_token = db.query(PasswordResetToken).filter(
|
||||
PasswordResetToken.token == request.token,
|
||||
PasswordResetToken.used == False,
|
||||
PasswordResetToken.expires_at > datetime.utcnow()
|
||||
).first()
|
||||
|
||||
if not reset_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid or expired reset token"
|
||||
)
|
||||
|
||||
# Get the user
|
||||
user = db.query(User).filter(User.id == reset_token.user_id).first()
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid reset token"
|
||||
)
|
||||
|
||||
# Update password
|
||||
hashed_password = get_password_hash(request.new_password)
|
||||
user.hashed_password = hashed_password
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
# Mark token as used
|
||||
reset_token.used = True
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"message": "Password has been reset successfully. You can now log in with your new password."}
|
||||
|
||||
@@ -37,6 +37,7 @@ class Settings(BaseSettings):
|
||||
SMTP2GO_API_URL: str = "https://api.smtp2go.com/v3/email/send"
|
||||
EMAIL_FROM: str
|
||||
EMAIL_FROM_NAME: str
|
||||
FRONTEND_URL: str = "http://localhost:3500"
|
||||
|
||||
# CORS
|
||||
BACKEND_CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8080"]
|
||||
|
||||
@@ -259,3 +259,17 @@ class Notification(Base):
|
||||
sent_at = Column(DateTime, nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
|
||||
class PasswordResetToken(Base):
|
||||
__tablename__ = "password_reset_tokens"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
token = Column(String(255), unique=True, nullable=False, index=True)
|
||||
expires_at = Column(DateTime, nullable=False)
|
||||
used = Column(Boolean, default=False, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="password_reset_tokens")
|
||||
|
||||
@@ -7,6 +7,8 @@ from .schemas import (
|
||||
Token,
|
||||
TokenData,
|
||||
LoginRequest,
|
||||
ForgotPasswordRequest,
|
||||
ResetPasswordRequest,
|
||||
MembershipTierBase,
|
||||
MembershipTierCreate,
|
||||
MembershipTierUpdate,
|
||||
@@ -31,6 +33,8 @@ __all__ = [
|
||||
"Token",
|
||||
"TokenData",
|
||||
"LoginRequest",
|
||||
"ForgotPasswordRequest",
|
||||
"ResetPasswordRequest",
|
||||
"MembershipTierBase",
|
||||
"MembershipTierCreate",
|
||||
"MembershipTierUpdate",
|
||||
|
||||
@@ -53,6 +53,16 @@ class LoginRequest(BaseModel):
|
||||
password: str
|
||||
|
||||
|
||||
# Password Reset Schemas
|
||||
class ForgotPasswordRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
token: str = Field(..., min_length=1)
|
||||
new_password: str = Field(..., min_length=8)
|
||||
|
||||
|
||||
# Membership Tier Schemas
|
||||
class MembershipTierBase(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
|
||||
@@ -191,6 +191,59 @@ class EmailService:
|
||||
"""
|
||||
|
||||
return await self.send_email(to_email, subject, html_body, text_body)
|
||||
|
||||
async def send_password_reset_email(
|
||||
self,
|
||||
to_email: str,
|
||||
first_name: str,
|
||||
reset_token: str
|
||||
) -> dict:
|
||||
"""Send password reset email with reset link"""
|
||||
subject = f"Password Reset - {settings.APP_NAME}"
|
||||
|
||||
reset_url = f"{settings.FRONTEND_URL}/reset-password?token={reset_token}"
|
||||
|
||||
html_body = f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<h2 style="color: #0066cc;">Password Reset Request</h2>
|
||||
<p>Hello {first_name},</p>
|
||||
<p>You have requested to reset your password for your {settings.APP_NAME} account.</p>
|
||||
<p>Please click the button below to reset your password:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="{reset_url}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">Reset Password</a>
|
||||
</div>
|
||||
<p>If the button doesn't work, you can copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; background-color: #f5f5f5; padding: 10px; border-radius: 3px;">{reset_url}</p>
|
||||
<p><strong>This link will expire in 1 hour.</strong></p>
|
||||
<p>If you didn't request this password reset, please ignore this email. Your password will remain unchanged.</p>
|
||||
<p>For security reasons, please don't share this email with anyone.</p>
|
||||
<p>Best regards,<br>
|
||||
<strong>{settings.APP_NAME}</strong></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
text_body = f"""
|
||||
Password Reset Request
|
||||
|
||||
Hello {first_name},
|
||||
|
||||
You have requested to reset your password for your {settings.APP_NAME} account.
|
||||
|
||||
Please use this link to reset your password: {reset_url}
|
||||
|
||||
This link will expire in 1 hour.
|
||||
|
||||
If you didn't request this password reset, please ignore this email. Your password will remain unchanged.
|
||||
|
||||
For security reasons, please don't share this email with anyone.
|
||||
|
||||
Best regards,
|
||||
{settings.APP_NAME}
|
||||
"""
|
||||
|
||||
return await self.send_email(to_email, subject, html_body, text_body)
|
||||
|
||||
|
||||
# Create a singleton instance
|
||||
|
||||
Reference in New Issue
Block a user