diff --git a/app/__init__.py b/app/__init__.py index 097c496..fceecfd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -42,7 +42,7 @@ def create_app(): origins=["http://localhost:3000"], # Frontend URL supports_credentials=True, # Allow cookies allow_headers=["Content-Type", "Authorization"], - methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + methods=["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"], ) # Initialize JWT manager diff --git a/app/models/user.py b/app/models/user.py index ed5ab60..bb7300d 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -67,6 +67,13 @@ class User(db.Model): def to_dict(self) -> dict: """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 { "id": str(self.id), "email": self.email, @@ -76,7 +83,7 @@ class User(db.Model): "is_active": self.is_active, "api_token": self.api_token, "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, "credits": self.credits, "created_at": self.created_at.isoformat(), diff --git a/app/routes/auth.py b/app/routes/auth.py index 3dd42eb..999e9f2 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -272,3 +272,115 @@ def me(): """Get current user information.""" user = get_current_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 diff --git a/app/services/decorators.py b/app/services/decorators.py index a461ffd..0c0dcf6 100644 --- a/app/services/decorators.py +++ b/app/services/decorators.py @@ -56,6 +56,13 @@ def get_user_from_api_token() -> dict[str, Any] | None: user = User.find_by_api_token(api_token) 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 { "id": str(user.id), "email": user.email, @@ -64,8 +71,7 @@ def get_user_from_api_token() -> dict[str, Any] | None: "role": user.role, "is_active": user.is_active, "provider": "api_token", - "providers": [p.provider for p in user.oauth_providers] - + ["api_token"], + "providers": providers, "plan": user.plan.to_dict() if user.plan else None, "credits": user.credits, }