feat(auth): add profile update and password change endpoints; enhance provider list handling

This commit is contained in:
JSC
2025-06-29 22:00:59 +02:00
parent 91648a858e
commit a7210a8d50
4 changed files with 129 additions and 4 deletions

View File

@@ -42,7 +42,7 @@ def create_app():
origins=["http://localhost:3000"], # Frontend URL origins=["http://localhost:3000"], # Frontend URL
supports_credentials=True, # Allow cookies supports_credentials=True, # Allow cookies
allow_headers=["Content-Type", "Authorization"], allow_headers=["Content-Type", "Authorization"],
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], methods=["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
) )
# Initialize JWT manager # Initialize JWT manager

View File

@@ -67,6 +67,13 @@ class User(db.Model):
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert user to dictionary.""" """Convert user to dictionary."""
# Build comprehensive providers list
providers = [provider.provider for provider in self.oauth_providers]
if self.password_hash:
providers.append("password")
if self.api_token:
providers.append("api_token")
return { return {
"id": str(self.id), "id": str(self.id),
"email": self.email, "email": self.email,
@@ -76,7 +83,7 @@ class User(db.Model):
"is_active": self.is_active, "is_active": self.is_active,
"api_token": self.api_token, "api_token": self.api_token,
"api_token_expires_at": self.api_token_expires_at.isoformat() if self.api_token_expires_at else None, "api_token_expires_at": self.api_token_expires_at.isoformat() if self.api_token_expires_at else None,
"providers": [provider.provider for provider in self.oauth_providers], "providers": providers,
"plan": self.plan.to_dict() if self.plan else None, "plan": self.plan.to_dict() if self.plan else None,
"credits": self.credits, "credits": self.credits,
"created_at": self.created_at.isoformat(), "created_at": self.created_at.isoformat(),

View File

@@ -272,3 +272,115 @@ def me():
"""Get current user information.""" """Get current user information."""
user = get_current_user() user = get_current_user()
return {"user": user} return {"user": user}
@bp.route("/profile", methods=["PATCH"])
@require_auth
def update_profile():
"""Update current user profile information."""
from flask import request
from app.database import db
from app.models.user import User
data = request.get_json()
if not data:
return {"error": "No data provided"}, 400
user_data = get_current_user()
if not user_data:
return {"error": "User not authenticated"}, 401
user = User.query.get(int(user_data["id"]))
if not user:
return {"error": "User not found"}, 404
# Update allowed fields
if "name" in data:
name = data["name"].strip()
if not name:
return {"error": "Name cannot be empty"}, 400
if len(name) > 100:
return {"error": "Name too long (max 100 characters)"}, 400
user.name = name
try:
db.session.commit()
# Return fresh user data from database
updated_user = {
"id": str(user.id),
"email": user.email,
"name": user.name,
"picture": user.picture,
"role": user.role,
"is_active": user.is_active,
"provider": "password", # This endpoint is only for password users
"providers": [p.provider for p in user.oauth_providers],
"plan": user.plan.to_dict() if user.plan else None,
"credits": user.credits,
}
return {
"message": "Profile updated successfully",
"user": updated_user
}
except Exception as e:
db.session.rollback()
return {"error": f"Failed to update profile: {str(e)}"}, 500
@bp.route("/password", methods=["PUT"])
@require_auth
def change_password():
"""Change or set user password."""
from flask import request
from app.database import db
from app.models.user import User
from werkzeug.security import check_password_hash
data = request.get_json()
if not data:
return {"error": "No data provided"}, 400
user_data = get_current_user()
if not user_data:
return {"error": "User not authenticated"}, 401
user = User.query.get(int(user_data["id"]))
if not user:
return {"error": "User not found"}, 404
new_password = data.get("new_password")
current_password = data.get("current_password")
if not new_password:
return {"error": "New password is required"}, 400
# Password validation
if len(new_password) < 6:
return {"error": "Password must be at least 6 characters long"}, 400
# Check authentication method: if user logged in via password, require current password
# If user logged in via OAuth, they can change password without current password
current_auth_method = user_data.get("provider", "unknown")
if user.password_hash and current_auth_method == "password":
# User has a password AND logged in via password, require current password for verification
if not current_password:
return {"error": "Current password is required to change password"}, 400
if not check_password_hash(user.password_hash, current_password):
return {"error": "Current password is incorrect"}, 400
# If user logged in via OAuth (google, github, etc.), they can change password without current password
# Set the new password
try:
user.set_password(new_password)
db.session.commit()
return {
"message": "Password updated successfully"
}
except Exception as e:
db.session.rollback()
return {"error": f"Failed to update password: {str(e)}"}, 500

View File

@@ -56,6 +56,13 @@ def get_user_from_api_token() -> dict[str, Any] | None:
user = User.find_by_api_token(api_token) user = User.find_by_api_token(api_token)
if user and user.is_active: if user and user.is_active:
# Build comprehensive providers list
providers = [p.provider for p in user.oauth_providers]
if user.password_hash:
providers.append("password")
if user.api_token:
providers.append("api_token")
return { return {
"id": str(user.id), "id": str(user.id),
"email": user.email, "email": user.email,
@@ -64,8 +71,7 @@ def get_user_from_api_token() -> dict[str, Any] | None:
"role": user.role, "role": user.role,
"is_active": user.is_active, "is_active": user.is_active,
"provider": "api_token", "provider": "api_token",
"providers": [p.provider for p in user.oauth_providers] "providers": providers,
+ ["api_token"],
"plan": user.plan.to_dict() if user.plan else None, "plan": user.plan.to_dict() if user.plan else None,
"credits": user.credits, "credits": user.credits,
} }