Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>tags/1.8.1
| @@ -1,19 +1,20 @@ | |||
| from flask import Blueprint | |||
| from flask_restx import Namespace | |||
| from libs.external_api import ExternalApi | |||
| from .files import FileApi | |||
| from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi | |||
| bp = Blueprint("web", __name__, url_prefix="/api") | |||
| api = ExternalApi(bp) | |||
| # Files | |||
| api.add_resource(FileApi, "/files/upload") | |||
| api = ExternalApi( | |||
| bp, | |||
| version="1.0", | |||
| title="Web API", | |||
| description="Public APIs for web applications including file uploads, chat interactions, and app management", | |||
| doc="/docs", # Enable Swagger UI at /api/docs | |||
| ) | |||
| # Remote files | |||
| api.add_resource(RemoteFileInfoApi, "/remote-files/<path:url>") | |||
| api.add_resource(RemoteFileUploadApi, "/remote-files/upload") | |||
| # Create namespace | |||
| web_ns = Namespace("web", description="Web application API operations", path="/") | |||
| from . import ( | |||
| app, | |||
| @@ -21,11 +22,15 @@ from . import ( | |||
| completion, | |||
| conversation, | |||
| feature, | |||
| files, | |||
| forgot_password, | |||
| login, | |||
| message, | |||
| passport, | |||
| remote_files, | |||
| saved_message, | |||
| site, | |||
| workflow, | |||
| ) | |||
| api.add_namespace(web_ns) | |||
| @@ -1,12 +1,21 @@ | |||
| from flask_restx import Resource | |||
| from controllers.web import api | |||
| from controllers.web import web_ns | |||
| from services.feature_service import FeatureService | |||
| @web_ns.route("/system-features") | |||
| class SystemFeatureApi(Resource): | |||
| @web_ns.doc("get_system_features") | |||
| @web_ns.doc(description="Get system feature flags and configuration") | |||
| @web_ns.doc(responses={200: "System features retrieved successfully", 500: "Internal server error"}) | |||
| def get(self): | |||
| return FeatureService.get_system_features().model_dump() | |||
| """Get system feature flags and configuration. | |||
| Returns the current system feature flags and configuration | |||
| that control various functionalities across the platform. | |||
| api.add_resource(SystemFeatureApi, "/system-features") | |||
| Returns: | |||
| dict: System feature configuration object | |||
| """ | |||
| return FeatureService.get_system_features().model_dump() | |||
| @@ -9,14 +9,50 @@ from controllers.common.errors import ( | |||
| TooManyFilesError, | |||
| UnsupportedFileTypeError, | |||
| ) | |||
| from controllers.web import web_ns | |||
| from controllers.web.wraps import WebApiResource | |||
| from fields.file_fields import file_fields | |||
| from fields.file_fields import build_file_model | |||
| from services.file_service import FileService | |||
| @web_ns.route("/files/upload") | |||
| class FileApi(WebApiResource): | |||
| @marshal_with(file_fields) | |||
| @web_ns.doc("upload_file") | |||
| @web_ns.doc(description="Upload a file for use in web applications") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 201: "File uploaded successfully", | |||
| 400: "Bad request - invalid file or parameters", | |||
| 413: "File too large", | |||
| 415: "Unsupported file type", | |||
| } | |||
| ) | |||
| @marshal_with(build_file_model(web_ns)) | |||
| def post(self, app_model, end_user): | |||
| """Upload a file for use in web applications. | |||
| Accepts file uploads for use within web applications, supporting | |||
| multiple file types with automatic validation and storage. | |||
| Args: | |||
| app_model: The associated application model | |||
| end_user: The end user uploading the file | |||
| Form Parameters: | |||
| file: The file to upload (required) | |||
| source: Optional source type (datasets or None) | |||
| Returns: | |||
| dict: File information including ID, URL, and metadata | |||
| int: HTTP status code 201 for success | |||
| Raises: | |||
| NoFileUploadedError: No file provided in request | |||
| TooManyFilesError: Multiple files provided (only one allowed) | |||
| FilenameNotExistsError: File has no filename | |||
| FileTooLargeError: File exceeds size limit | |||
| UnsupportedFileTypeError: File type not supported | |||
| """ | |||
| if "file" not in request.files: | |||
| raise NoFileUploadedError() | |||
| @@ -16,7 +16,7 @@ from controllers.console.auth.error import ( | |||
| ) | |||
| from controllers.console.error import EmailSendIpLimitError | |||
| from controllers.console.wraps import email_password_login_enabled, only_edition_enterprise, setup_required | |||
| from controllers.web import api | |||
| from controllers.web import web_ns | |||
| from extensions.ext_database import db | |||
| from libs.helper import email, extract_remote_ip | |||
| from libs.password import hash_password, valid_password | |||
| @@ -24,10 +24,21 @@ from models.account import Account | |||
| from services.account_service import AccountService | |||
| @web_ns.route("/forgot-password") | |||
| class ForgotPasswordSendEmailApi(Resource): | |||
| @only_edition_enterprise | |||
| @setup_required | |||
| @email_password_login_enabled | |||
| @web_ns.doc("send_forgot_password_email") | |||
| @web_ns.doc(description="Send password reset email") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Password reset email sent successfully", | |||
| 400: "Bad request - invalid email format", | |||
| 404: "Account not found", | |||
| 429: "Too many requests - rate limit exceeded", | |||
| } | |||
| ) | |||
| def post(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("email", type=email, required=True, location="json") | |||
| @@ -54,10 +65,16 @@ class ForgotPasswordSendEmailApi(Resource): | |||
| return {"result": "success", "data": token} | |||
| @web_ns.route("/forgot-password/validity") | |||
| class ForgotPasswordCheckApi(Resource): | |||
| @only_edition_enterprise | |||
| @setup_required | |||
| @email_password_login_enabled | |||
| @web_ns.doc("check_forgot_password_token") | |||
| @web_ns.doc(description="Verify password reset token validity") | |||
| @web_ns.doc( | |||
| responses={200: "Token is valid", 400: "Bad request - invalid token format", 401: "Invalid or expired token"} | |||
| ) | |||
| def post(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("email", type=str, required=True, location="json") | |||
| @@ -94,10 +111,21 @@ class ForgotPasswordCheckApi(Resource): | |||
| return {"is_valid": True, "email": token_data.get("email"), "token": new_token} | |||
| @web_ns.route("/forgot-password/resets") | |||
| class ForgotPasswordResetApi(Resource): | |||
| @only_edition_enterprise | |||
| @setup_required | |||
| @email_password_login_enabled | |||
| @web_ns.doc("reset_password") | |||
| @web_ns.doc(description="Reset user password with verification token") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Password reset successfully", | |||
| 400: "Bad request - invalid parameters or password mismatch", | |||
| 401: "Invalid or expired token", | |||
| 404: "Account not found", | |||
| } | |||
| ) | |||
| def post(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("token", type=str, required=True, nullable=False, location="json") | |||
| @@ -141,8 +169,3 @@ class ForgotPasswordResetApi(Resource): | |||
| account.password = base64.b64encode(password_hashed).decode() | |||
| account.password_salt = base64.b64encode(salt).decode() | |||
| session.commit() | |||
| api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password") | |||
| api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity") | |||
| api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets") | |||
| @@ -9,18 +9,30 @@ from controllers.console.auth.error import ( | |||
| ) | |||
| from controllers.console.error import AccountBannedError | |||
| from controllers.console.wraps import only_edition_enterprise, setup_required | |||
| from controllers.web import api | |||
| from controllers.web import web_ns | |||
| from libs.helper import email | |||
| from libs.password import valid_password | |||
| from services.account_service import AccountService | |||
| from services.webapp_auth_service import WebAppAuthService | |||
| @web_ns.route("/login") | |||
| class LoginApi(Resource): | |||
| """Resource for web app email/password login.""" | |||
| @setup_required | |||
| @only_edition_enterprise | |||
| @web_ns.doc("web_app_login") | |||
| @web_ns.doc(description="Authenticate user for web application access") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Authentication successful", | |||
| 400: "Bad request - invalid email or password format", | |||
| 401: "Authentication failed - email or password mismatch", | |||
| 403: "Account banned or login disabled", | |||
| 404: "Account not found", | |||
| } | |||
| ) | |||
| def post(self): | |||
| """Authenticate user and login.""" | |||
| parser = reqparse.RequestParser() | |||
| @@ -51,9 +63,19 @@ class LoginApi(Resource): | |||
| # return {"result": "success"} | |||
| @web_ns.route("/email-code-login") | |||
| class EmailCodeLoginSendEmailApi(Resource): | |||
| @setup_required | |||
| @only_edition_enterprise | |||
| @web_ns.doc("send_email_code_login") | |||
| @web_ns.doc(description="Send email verification code for login") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Email code sent successfully", | |||
| 400: "Bad request - invalid email format", | |||
| 404: "Account not found", | |||
| } | |||
| ) | |||
| def post(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("email", type=email, required=True, location="json") | |||
| @@ -74,9 +96,20 @@ class EmailCodeLoginSendEmailApi(Resource): | |||
| return {"result": "success", "data": token} | |||
| @web_ns.route("/email-code-login/validity") | |||
| class EmailCodeLoginApi(Resource): | |||
| @setup_required | |||
| @only_edition_enterprise | |||
| @web_ns.doc("verify_email_code_login") | |||
| @web_ns.doc(description="Verify email code and complete login") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Email code verified and login successful", | |||
| 400: "Bad request - invalid code or token", | |||
| 401: "Invalid token or expired code", | |||
| 404: "Account not found", | |||
| } | |||
| ) | |||
| def post(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("email", type=str, required=True, location="json") | |||
| @@ -104,9 +137,3 @@ class EmailCodeLoginApi(Resource): | |||
| token = WebAppAuthService.login(account=account) | |||
| AccountService.reset_login_error_rate_limit(args["email"]) | |||
| return {"result": "success", "data": {"access_token": token}} | |||
| api.add_resource(LoginApi, "/login") | |||
| # api.add_resource(LogoutApi, "/logout") | |||
| api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") | |||
| api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") | |||
| @@ -7,7 +7,7 @@ from sqlalchemy import func, select | |||
| from werkzeug.exceptions import NotFound, Unauthorized | |||
| from configs import dify_config | |||
| from controllers.web import api | |||
| from controllers.web import web_ns | |||
| from controllers.web.error import WebAppAuthRequiredError | |||
| from extensions.ext_database import db | |||
| from libs.passport import PassportService | |||
| @@ -17,9 +17,19 @@ from services.feature_service import FeatureService | |||
| from services.webapp_auth_service import WebAppAuthService, WebAppAuthType | |||
| @web_ns.route("/passport") | |||
| class PassportResource(Resource): | |||
| """Base resource for passport.""" | |||
| @web_ns.doc("get_passport") | |||
| @web_ns.doc(description="Get authentication passport for web application access") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Passport retrieved successfully", | |||
| 401: "Unauthorized - missing app code or invalid authentication", | |||
| 404: "Application or user not found", | |||
| } | |||
| ) | |||
| def get(self): | |||
| system_features = FeatureService.get_system_features() | |||
| app_code = request.headers.get("X-App-Code") | |||
| @@ -94,9 +104,6 @@ class PassportResource(Resource): | |||
| } | |||
| api.add_resource(PassportResource, "/passport") | |||
| def decode_enterprise_webapp_user_id(jwt_token: str | None): | |||
| """ | |||
| Decode the enterprise user session from the Authorization header. | |||
| @@ -10,16 +10,44 @@ from controllers.common.errors import ( | |||
| RemoteFileUploadError, | |||
| UnsupportedFileTypeError, | |||
| ) | |||
| from controllers.web import web_ns | |||
| from controllers.web.wraps import WebApiResource | |||
| from core.file import helpers as file_helpers | |||
| from core.helper import ssrf_proxy | |||
| from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields | |||
| from fields.file_fields import build_file_with_signed_url_model, build_remote_file_info_model | |||
| from services.file_service import FileService | |||
| @web_ns.route("/remote-files/<path:url>") | |||
| class RemoteFileInfoApi(WebApiResource): | |||
| @marshal_with(remote_file_info_fields) | |||
| @web_ns.doc("get_remote_file_info") | |||
| @web_ns.doc(description="Get information about a remote file") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Remote file information retrieved successfully", | |||
| 400: "Bad request - invalid URL", | |||
| 404: "Remote file not found", | |||
| 500: "Failed to fetch remote file", | |||
| } | |||
| ) | |||
| @marshal_with(build_remote_file_info_model(web_ns)) | |||
| def get(self, app_model, end_user, url): | |||
| """Get information about a remote file. | |||
| Retrieves basic information about a file located at a remote URL, | |||
| including content type and content length. | |||
| Args: | |||
| app_model: The associated application model | |||
| end_user: The end user making the request | |||
| url: URL-encoded path to the remote file | |||
| Returns: | |||
| dict: Remote file information including type and length | |||
| Raises: | |||
| HTTPException: If the remote file cannot be accessed | |||
| """ | |||
| decoded_url = urllib.parse.unquote(url) | |||
| resp = ssrf_proxy.head(decoded_url) | |||
| if resp.status_code != httpx.codes.OK: | |||
| @@ -32,9 +60,42 @@ class RemoteFileInfoApi(WebApiResource): | |||
| } | |||
| @web_ns.route("/remote-files/upload") | |||
| class RemoteFileUploadApi(WebApiResource): | |||
| @marshal_with(file_fields_with_signed_url) | |||
| def post(self, app_model, end_user): # Add app_model and end_user parameters | |||
| @web_ns.doc("upload_remote_file") | |||
| @web_ns.doc(description="Upload a file from a remote URL") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 201: "Remote file uploaded successfully", | |||
| 400: "Bad request - invalid URL or parameters", | |||
| 413: "File too large", | |||
| 415: "Unsupported file type", | |||
| 500: "Failed to fetch remote file", | |||
| } | |||
| ) | |||
| @marshal_with(build_file_with_signed_url_model(web_ns)) | |||
| def post(self, app_model, end_user): | |||
| """Upload a file from a remote URL. | |||
| Downloads a file from the provided remote URL and uploads it | |||
| to the platform storage for use in web applications. | |||
| Args: | |||
| app_model: The associated application model | |||
| end_user: The end user making the request | |||
| JSON Parameters: | |||
| url: The remote URL to download the file from (required) | |||
| Returns: | |||
| dict: File information including ID, signed URL, and metadata | |||
| int: HTTP status code 201 for success | |||
| Raises: | |||
| RemoteFileUploadError: Failed to fetch file from remote URL | |||
| FileTooLargeError: File exceeds size limit | |||
| UnsupportedFileTypeError: File type not supported | |||
| """ | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("url", type=str, required=True, help="URL is required") | |||
| args = parser.parse_args() | |||