Email SMTP Fix

This commit is contained in:
Koncept Kit
2025-12-07 16:59:04 +07:00
parent 79b617904b
commit 005c56b43d
11 changed files with 526 additions and 28 deletions

175
server.py
View File

@@ -20,9 +20,18 @@ from auth import (
verify_password,
create_access_token,
get_current_user,
get_current_admin_user
get_current_admin_user,
get_active_member,
create_password_reset_token,
verify_reset_token
)
from email_service import (
send_verification_email,
send_approval_notification,
send_payment_prompt_email,
send_password_reset_email,
send_admin_password_reset_email
)
from email_service import send_verification_email, send_approval_notification, send_payment_prompt_email
from payment_service import create_checkout_session, verify_webhook_signature, get_subscription_end_date
# Load environment variables
@@ -125,6 +134,20 @@ class LoginResponse(BaseModel):
token_type: str
user: dict
class ForgotPasswordRequest(BaseModel):
email: EmailStr
class ResetPasswordRequest(BaseModel):
token: str
new_password: str = Field(min_length=6)
class ChangePasswordRequest(BaseModel):
current_password: str
new_password: str = Field(min_length=6)
class AdminPasswordUpdateRequest(BaseModel):
force_change: bool = True
class UserResponse(BaseModel):
id: str
email: str
@@ -314,9 +337,32 @@ async def verify_email(token: str, db: Session = Depends(get_db)):
db.refresh(user)
logger.info(f"Email verified for user: {user.email}")
return {"message": "Email verified successfully", "status": user.status.value}
@api_router.post("/auth/resend-verification-email")
async def resend_verification_email(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""User requests to resend their verification email"""
# Check if email already verified
if current_user.email_verified:
raise HTTPException(status_code=400, detail="Email is already verified")
# Generate new token
verification_token = secrets.token_urlsafe(32)
current_user.email_verification_token = verification_token
db.commit()
# Send verification email
await send_verification_email(current_user.email, verification_token)
logger.info(f"Verification email resent to: {current_user.email}")
return {"message": "Verification email has been resent. Please check your inbox."}
@api_router.post("/auth/login", response_model=LoginResponse)
async def login(request: LoginRequest, db: Session = Depends(get_db)):
user = db.query(User).filter(User.email == request.email).first()
@@ -338,10 +384,60 @@ async def login(request: LoginRequest, db: Session = Depends(get_db)):
"first_name": user.first_name,
"last_name": user.last_name,
"status": user.status.value,
"role": user.role.value
"role": user.role.value,
"force_password_change": user.force_password_change
}
}
@api_router.post("/auth/forgot-password")
async def forgot_password(request: ForgotPasswordRequest, db: Session = Depends(get_db)):
"""Request password reset - sends email with reset link"""
user = db.query(User).filter(User.email == request.email).first()
# Always return success (security: don't reveal if email exists)
if user:
token = create_password_reset_token(user, db)
reset_url = f"{os.getenv('FRONTEND_URL')}/reset-password?token={token}"
await send_password_reset_email(user.email, user.first_name, reset_url)
return {"message": "If email exists, reset link has been sent"}
@api_router.post("/auth/reset-password")
async def reset_password(request: ResetPasswordRequest, db: Session = Depends(get_db)):
"""Complete password reset using token"""
user = verify_reset_token(request.token, db)
if not user:
raise HTTPException(status_code=400, detail="Invalid or expired reset token")
# Update password
user.password_hash = get_password_hash(request.new_password)
user.password_reset_token = None
user.password_reset_expires = None
user.force_password_change = False # Reset flag if it was set
db.commit()
return {"message": "Password reset successful"}
@api_router.put("/users/change-password")
async def change_password(
request: ChangePasswordRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""User changes their own password"""
# Verify current password
if not verify_password(request.current_password, current_user.password_hash):
raise HTTPException(status_code=400, detail="Current password is incorrect")
# Update password
current_user.password_hash = get_password_hash(request.new_password)
current_user.force_password_change = False # Clear flag if set
db.commit()
return {"message": "Password changed successfully"}
@api_router.get("/auth/me", response_model=UserResponse)
async def get_me(current_user: User = Depends(get_current_user)):
return UserResponse(
@@ -412,6 +508,7 @@ async def update_profile(
# Event Routes
@api_router.get("/events", response_model=List[EventResponse])
async def get_events(
current_user: User = Depends(get_active_member),
db: Session = Depends(get_db)
):
# Get published events for all users
@@ -445,6 +542,7 @@ async def get_events(
@api_router.get("/events/{event_id}", response_model=EventResponse)
async def get_event(
event_id: str,
current_user: User = Depends(get_active_member),
db: Session = Depends(get_db)
):
event = db.query(Event).filter(Event.id == event_id).first()
@@ -478,7 +576,7 @@ async def get_event(
async def rsvp_to_event(
event_id: str,
request: RSVPRequest,
current_user: User = Depends(get_current_user),
current_user: User = Depends(get_active_member),
db: Session = Depends(get_db)
):
event = db.query(Event).filter(Event.id == event_id).first()
@@ -743,6 +841,73 @@ async def activate_payment_manually(
"subscription_id": str(subscription.id)
}
@api_router.put("/admin/users/{user_id}/reset-password")
async def admin_reset_user_password(
user_id: str,
request: AdminPasswordUpdateRequest,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Admin resets user password - generates temp password and emails it"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Generate random temporary password
temp_password = secrets.token_urlsafe(12)
# Update user
user.password_hash = get_password_hash(temp_password)
user.force_password_change = request.force_change
db.commit()
# Email user the temporary password
await send_admin_password_reset_email(
user.email,
user.first_name,
temp_password,
request.force_change
)
# Log admin action
logger.info(
f"Admin {current_user.email} reset password for user {user.email} "
f"(force_change={request.force_change})"
)
return {"message": f"Password reset for {user.email}. Temporary password emailed."}
@api_router.post("/admin/users/{user_id}/resend-verification")
async def admin_resend_verification(
user_id: str,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""Admin resends verification email for any user"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Check if email already verified
if user.email_verified:
raise HTTPException(status_code=400, detail="User's email is already verified")
# Generate new token
verification_token = secrets.token_urlsafe(32)
user.email_verification_token = verification_token
db.commit()
# Send verification email
await send_verification_email(user.email, verification_token)
# Log admin action
logger.info(
f"Admin {current_user.email} resent verification email to user {user.email}"
)
return {"message": f"Verification email resent to {user.email}"}
@api_router.post("/admin/events", response_model=EventResponse)
async def create_event(
request: EventCreate,