| @@ -848,6 +848,11 @@ class AccountConfig(BaseSettings): | |||
| default=5, | |||
| ) | |||
| EDUCATION_ENABLED: bool = Field( | |||
| description="whether to enable education identity", | |||
| default=False, | |||
| ) | |||
| class FeatureConfig( | |||
| # place the configs in alphabet order | |||
| @@ -103,6 +103,18 @@ class AccountInFreezeError(BaseHTTPException): | |||
| ) | |||
| class EducationVerifyLimitError(BaseHTTPException): | |||
| error_code = "education_verify_limit" | |||
| description = "Rate limit exceeded" | |||
| code = 429 | |||
| class EducationActivateLimitError(BaseHTTPException): | |||
| error_code = "education_activate_limit" | |||
| description = "Rate limit exceeded" | |||
| code = 429 | |||
| class CompilanceRateLimitError(BaseHTTPException): | |||
| error_code = "compilance_rate_limit" | |||
| description = "Rate limit exceeded for downloading compliance report." | |||
| @@ -15,7 +15,13 @@ from controllers.console.workspace.error import ( | |||
| InvalidInvitationCodeError, | |||
| RepeatPasswordNotMatchError, | |||
| ) | |||
| from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required | |||
| from controllers.console.wraps import ( | |||
| account_initialization_required, | |||
| cloud_edition_billing_enabled, | |||
| enterprise_license_required, | |||
| only_edition_cloud, | |||
| setup_required, | |||
| ) | |||
| from extensions.ext_database import db | |||
| from fields.member_fields import account_fields | |||
| from libs.helper import TimestampField, timezone | |||
| @@ -292,6 +298,79 @@ class AccountDeleteUpdateFeedbackApi(Resource): | |||
| return {"result": "success"} | |||
| class EducationVerifyApi(Resource): | |||
| verify_fields = { | |||
| "token": fields.String, | |||
| } | |||
| @setup_required | |||
| @login_required | |||
| @account_initialization_required | |||
| @only_edition_cloud | |||
| @cloud_edition_billing_enabled | |||
| @marshal_with(verify_fields) | |||
| def get(self): | |||
| account = current_user | |||
| return BillingService.EducationIdentity.verify(account.id, account.email) | |||
| class EducationApi(Resource): | |||
| status_fields = { | |||
| "result": fields.Boolean, | |||
| } | |||
| @setup_required | |||
| @login_required | |||
| @account_initialization_required | |||
| @only_edition_cloud | |||
| @cloud_edition_billing_enabled | |||
| def post(self): | |||
| account = current_user | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("token", type=str, required=True, location="json") | |||
| parser.add_argument("institution", type=str, required=True, location="json") | |||
| parser.add_argument("role", type=str, required=True, location="json") | |||
| args = parser.parse_args() | |||
| return BillingService.EducationIdentity.activate(account, args["token"], args["institution"], args["role"]) | |||
| @setup_required | |||
| @login_required | |||
| @account_initialization_required | |||
| @only_edition_cloud | |||
| @cloud_edition_billing_enabled | |||
| @marshal_with(status_fields) | |||
| def get(self): | |||
| account = current_user | |||
| return BillingService.EducationIdentity.is_active(account.id) | |||
| class EducationAutoCompleteApi(Resource): | |||
| data_fields = { | |||
| "data": fields.List(fields.String), | |||
| "curr_page": fields.Integer, | |||
| "has_next": fields.Boolean, | |||
| } | |||
| @setup_required | |||
| @login_required | |||
| @account_initialization_required | |||
| @only_edition_cloud | |||
| @cloud_edition_billing_enabled | |||
| @marshal_with(data_fields) | |||
| def get(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("keywords", type=str, required=True, location="args") | |||
| parser.add_argument("page", type=int, required=False, location="args", default=0) | |||
| parser.add_argument("limit", type=int, required=False, location="args", default=20) | |||
| args = parser.parse_args() | |||
| return BillingService.EducationIdentity.autocomplete(args["keywords"], args["page"], args["limit"]) | |||
| # Register API resources | |||
| api.add_resource(AccountInitApi, "/account/init") | |||
| api.add_resource(AccountProfileApi, "/account/profile") | |||
| @@ -305,5 +384,8 @@ api.add_resource(AccountIntegrateApi, "/account/integrates") | |||
| api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") | |||
| api.add_resource(AccountDeleteApi, "/account/delete") | |||
| api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback") | |||
| api.add_resource(EducationVerifyApi, "/account/education/verify") | |||
| api.add_resource(EducationApi, "/account/education") | |||
| api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete") | |||
| # api.add_resource(AccountEmailApi, '/account/email') | |||
| # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') | |||
| @@ -54,6 +54,17 @@ def only_edition_self_hosted(view): | |||
| return decorated | |||
| def cloud_edition_billing_enabled(view): | |||
| @wraps(view) | |||
| def decorated(*args, **kwargs): | |||
| features = FeatureService.get_features(current_user.current_tenant_id) | |||
| if not features.billing.enabled: | |||
| abort(403, "Billing feature is not enabled.") | |||
| return view(*args, **kwargs) | |||
| return decorated | |||
| def cloud_edition_billing_resource_check(resource: str): | |||
| def interceptor(view): | |||
| @wraps(view) | |||
| @@ -6,7 +6,7 @@ from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fix | |||
| from extensions.ext_database import db | |||
| from libs.helper import RateLimiter | |||
| from models.account import TenantAccountJoin, TenantAccountRole | |||
| from models.account import Account, TenantAccountJoin, TenantAccountRole | |||
| class BillingService: | |||
| @@ -106,6 +106,48 @@ class BillingService: | |||
| json = {"email": email, "feedback": feedback} | |||
| return cls._send_request("POST", "/account/delete-feedback", json=json) | |||
| class EducationIdentity: | |||
| verification_rate_limit = RateLimiter(prefix="edu_verification_rate_limit", max_attempts=10, time_window=60) | |||
| activation_rate_limit = RateLimiter(prefix="edu_activation_rate_limit", max_attempts=10, time_window=60) | |||
| @classmethod | |||
| def verify(cls, account_id: str, account_email: str): | |||
| if cls.verification_rate_limit.is_rate_limited(account_email): | |||
| from controllers.console.error import EducationVerifyLimitError | |||
| raise EducationVerifyLimitError() | |||
| cls.verification_rate_limit.increment_rate_limit(account_email) | |||
| params = {"account_id": account_id} | |||
| return BillingService._send_request("GET", "/education/verify", params=params) | |||
| @classmethod | |||
| def is_active(cls, account_id: str): | |||
| params = {"account_id": account_id} | |||
| return BillingService._send_request("GET", "/education/status", params=params) | |||
| @classmethod | |||
| def activate(cls, account: Account, token: str, institution: str, role: str): | |||
| if cls.activation_rate_limit.is_rate_limited(account.email): | |||
| from controllers.console.error import EducationActivateLimitError | |||
| raise EducationActivateLimitError() | |||
| cls.activation_rate_limit.increment_rate_limit(account.email) | |||
| params = {"account_id": account.id, "curr_tenant_id": account.current_tenant_id} | |||
| json = { | |||
| "institution": institution, | |||
| "token": token, | |||
| "role": role, | |||
| } | |||
| return BillingService._send_request("POST", "/education/", json=json, params=params) | |||
| @classmethod | |||
| def autocomplete(cls, keywords: str, page: int = 0, limit: int = 20): | |||
| params = {"keywords": keywords, "page": page, "limit": limit} | |||
| return BillingService._send_request("GET", "/education/autocomplete", params=params) | |||
| @classmethod | |||
| def get_compliance_download_link( | |||
| cls, | |||
| @@ -17,6 +17,11 @@ class BillingModel(BaseModel): | |||
| subscription: SubscriptionModel = SubscriptionModel() | |||
| class EducationModel(BaseModel): | |||
| enabled: bool = False | |||
| activated: bool = False | |||
| class LimitationModel(BaseModel): | |||
| size: int = 0 | |||
| limit: int = 0 | |||
| @@ -38,6 +43,7 @@ class LicenseModel(BaseModel): | |||
| class FeatureModel(BaseModel): | |||
| billing: BillingModel = BillingModel() | |||
| education: EducationModel = EducationModel() | |||
| members: LimitationModel = LimitationModel(size=0, limit=1) | |||
| apps: LimitationModel = LimitationModel(size=0, limit=10) | |||
| vector_space: LimitationModel = LimitationModel(size=0, limit=5) | |||
| @@ -128,6 +134,7 @@ class FeatureService: | |||
| features.can_replace_logo = dify_config.CAN_REPLACE_LOGO | |||
| features.model_load_balancing_enabled = dify_config.MODEL_LB_ENABLED | |||
| features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED | |||
| features.education.enabled = dify_config.EDUCATION_ENABLED | |||
| @classmethod | |||
| def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str): | |||
| @@ -136,6 +143,7 @@ class FeatureService: | |||
| features.billing.enabled = billing_info["enabled"] | |||
| features.billing.subscription.plan = billing_info["subscription"]["plan"] | |||
| features.billing.subscription.interval = billing_info["subscription"]["interval"] | |||
| features.education.activated = billing_info["subscription"].get("education", False) | |||
| if "members" in billing_info: | |||
| features.members.size = billing_info["members"]["size"] | |||