From db36e3edf9be76fe766a35b65911ddfe4cd5dfa8 Mon Sep 17 00:00:00 2001 From: ytqh Date: Sun, 2 Feb 2025 17:34:49 +0800 Subject: [PATCH] add admin login and optimize the swagger definition --- api/controllers/admin/__init__.py | 8 ++ api/controllers/admin/auth/login.py | 124 +++++++++++++++++++ api/controllers/service_api/auth/login.py | 140 +++++++++++++++------- api/extensions/ext_blueprints.py | 8 ++ api/extensions/ext_swagger.py | 10 +- 5 files changed, 248 insertions(+), 42 deletions(-) create mode 100644 api/controllers/admin/__init__.py create mode 100644 api/controllers/admin/auth/login.py diff --git a/api/controllers/admin/__init__.py b/api/controllers/admin/__init__.py new file mode 100644 index 0000000000..bae026fc1d --- /dev/null +++ b/api/controllers/admin/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + +from libs.external_api import ExternalApi + +bp = Blueprint("admin_api", __name__, url_prefix="/admin") +api = ExternalApi(bp) + +from .auth import login \ No newline at end of file diff --git a/api/controllers/admin/auth/login.py b/api/controllers/admin/auth/login.py new file mode 100644 index 0000000000..7602ee892b --- /dev/null +++ b/api/controllers/admin/auth/login.py @@ -0,0 +1,124 @@ +from flask import Blueprint +from flask_restful import Api, Resource # type: ignore + +from controllers.admin import api + +class SendVerificationCodeApi(Resource): + def post(self): + """Send verification code to admin's phone number. + --- + tags: + - admin + summary: Send Verification Code + description: Sends a verification code to the provided admin phone number for authentication + parameters: + - in: body + name: body + required: true + schema: + type: object + required: + - phone + properties: + phone: + type: string + description: Admin's phone number + example: "13800138000" + responses: + 200: + description: Code sent successfully + schema: + type: object + properties: + success: + type: boolean + message: + type: string + 400: + description: Invalid phone number format + 404: + description: Phone number not registered as admin + """ + pass + +class LoginApi(Resource): + def post(self): + """Admin login with phone number and verification code. + --- + tags: + - admin + summary: Admin Login + description: Authenticates an admin using phone number and verification code + parameters: + - in: body + name: body + required: true + schema: + type: object + required: + - phone + - code + properties: + phone: + type: string + description: Admin's phone number + example: "13800138000" + code: + type: string + description: Verification code + example: "123456" + responses: + 200: + description: Login successful + schema: + type: object + properties: + token: + type: string + description: JWT access token + user: + type: object + properties: + id: + type: string + phone: + type: string + name: + type: string + role: + type: string + enum: [admin, super_admin] + 400: + description: Invalid or expired verification code + 404: + description: Phone number not registered + """ + pass + +class LogoutApi(Resource): + def post(self): + """Admin logout. + --- + tags: + - admin + summary: Admin Logout + description: Logs out the authenticated admin and invalidates the JWT token + security: + - JWT: [] + responses: + 200: + description: Logout successful + schema: + type: object + properties: + success: + type: boolean + 401: + description: Missing or invalid token + """ + pass + +# Register the resources +api.add_resource(SendVerificationCodeApi, '/auth/send-code') +api.add_resource(LoginApi, '/auth/login') +api.add_resource(LogoutApi, '/auth/logout') \ No newline at end of file diff --git a/api/controllers/service_api/auth/login.py b/api/controllers/service_api/auth/login.py index 9ac6bf9e69..3ef4271766 100644 --- a/api/controllers/service_api/auth/login.py +++ b/api/controllers/service_api/auth/login.py @@ -4,8 +4,6 @@ import flask_login # type: ignore from flask import request from flask_restful import Resource, reqparse # type: ignore -import services -from configs import dify_config from constants.languages import languages from controllers.service_api import api from controllers.service_api.auth.error import ( @@ -21,10 +19,8 @@ from controllers.service_api.error import ( ) from events.tenant_event import tenant_was_created from libs.helper import email, extract_remote_ip -from libs.password import valid_password from models.account import Account -from services.account_service import AccountService, RegisterService, TenantService -from services.billing_service import BillingService +from services.account_service import AccountService, TenantService from services.errors.account import AccountRegisterError from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.feature_service import FeatureService @@ -34,11 +30,23 @@ class LogoutApi(Resource): def get(self): """Logout user. --- + tags: + - user-end summary: Logout User - description: Logs out the authenticated user and invalidates the session. + description: Logs out the authenticated user and invalidates the session + security: + - JWT: [] responses: 200: - description: Successfully logged out. + description: Successfully logged out + schema: + type: object + properties: + result: + type: string + example: "success" + 401: + description: Unauthorized, invalid or missing token """ account = cast(Account, flask_login.current_user) if isinstance(account, flask_login.AnonymousUserMixin): @@ -52,26 +60,42 @@ class EmailCodeLoginSendEmailApi(Resource): def post(self): """Send email code for login. --- + tags: + - user-end summary: Email Code Login Email Sending - description: Sends an email with a verification code for login. + description: Sends an email with a verification code for login parameters: - in: body - name: email + name: body required: true - type: string - description: The user's email. - - in: body - name: language - required: false - type: string - description: Preferred language for the email. + schema: + type: object + required: + - email + properties: + email: + type: string + description: The user's email + language: + type: string + description: Preferred language for the email + enum: ["en-US", "zh-Hans"] responses: 200: - description: Successfully sent the email code along with token data. + description: Successfully sent the email code + schema: + type: object + properties: + result: + type: string + example: "success" + data: + type: object + description: Token data 429: - description: Too many requests, IP limit reached. + description: Too many requests, IP limit reached 404: - description: Account not found. + description: Account not found """ parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") @@ -106,29 +130,44 @@ class EmailCodeLoginApi(Resource): def post(self): """Login using email code. --- + tags: + - user-end summary: Email Code Login - description: Allows the user to login using a verification code and token sent via email. + description: Allows the user to login using a verification code and token sent via email parameters: - in: body - name: email - required: true - type: string - description: The user's email. - - in: body - name: code - required: true - type: string - description: The verification code sent to the email. - - in: body - name: token + name: body required: true - type: string - description: The token associated with the email code login. + schema: + type: object + required: + - email + - code + - token + properties: + email: + type: string + description: The user's email + code: + type: string + description: The verification code sent to the email + token: + type: string + description: The token associated with the email code login responses: 200: - description: Successfully logged in, returns token pair data. + description: Successfully logged in + schema: + type: object + properties: + result: + type: string + example: "success" + data: + type: object + description: Token pair data 400: - description: Invalid token, email or code. + description: Invalid token, email or code """ parser = reqparse.RequestParser() parser.add_argument("email", type=str, required=True, location="json") @@ -182,19 +221,38 @@ class RefreshTokenApi(Resource): def post(self): """Refresh authentication token. --- + tags: + - user-end summary: Refresh Token - description: Refreshes an access token using a valid refresh token. + description: Refreshes an access token using a valid refresh token + security: + - JWT: [] parameters: - in: body - name: refresh_token + name: body required: true - type: string - description: The refresh token provided in the request. + schema: + type: object + required: + - refresh_token + properties: + refresh_token: + type: string + description: The refresh token provided in the request responses: 200: - description: Successfully refreshed token, returns new token pair. + description: Successfully refreshed token + schema: + type: object + properties: + result: + type: string + example: "success" + data: + type: object + description: New token pair data 401: - description: Failed to refresh the token due to invalid or expired refresh token. + description: Unauthorized, invalid or missing token """ parser = reqparse.RequestParser() parser.add_argument("refresh_token", type=str, required=True, location="json") diff --git a/api/extensions/ext_blueprints.py b/api/extensions/ext_blueprints.py index 316be12f5c..217cb5e39c 100644 --- a/api/extensions/ext_blueprints.py +++ b/api/extensions/ext_blueprints.py @@ -12,6 +12,7 @@ def init_app(app: DifyApp): from controllers.inner_api import bp as inner_api_bp from controllers.service_api import bp as service_api_bp from controllers.web import bp as web_bp + from controllers.admin import bp as admin_bp CORS( service_api_bp, @@ -46,3 +47,10 @@ def init_app(app: DifyApp): app.register_blueprint(files_bp) app.register_blueprint(inner_api_bp) + + CORS( + admin_bp, + resources={r"/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}}, + supports_credentials=True, + ) + app.register_blueprint(admin_bp) diff --git a/api/extensions/ext_swagger.py b/api/extensions/ext_swagger.py index 46efa522fb..3bff3fe948 100644 --- a/api/extensions/ext_swagger.py +++ b/api/extensions/ext_swagger.py @@ -7,7 +7,15 @@ def init_app(app: DifyApp): app.config['SWAGGER'] = { 'title': 'API Docs', - 'uiversion': 3 + 'uiversion': 3, + 'securityDefinitions': { + 'JWT': { + 'type': 'apiKey', + 'name': 'access-token', # name of the cookie + 'in': 'header', # specify that auth is in cookie + 'description': 'JWT Authorization cookie' + } + } } Swagger(app)