| default=5, | default=5, | ||||
| ) | ) | ||||
| EDUCATION_ENABLED: bool = Field( | |||||
| description="whether to enable education identity", | |||||
| default=False, | |||||
| ) | |||||
| class FeatureConfig( | class FeatureConfig( | ||||
| # place the configs in alphabet order | # place the configs in alphabet order |
| ) | ) | ||||
| 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): | class CompilanceRateLimitError(BaseHTTPException): | ||||
| error_code = "compilance_rate_limit" | error_code = "compilance_rate_limit" | ||||
| description = "Rate limit exceeded for downloading compliance report." | description = "Rate limit exceeded for downloading compliance report." |
| InvalidInvitationCodeError, | InvalidInvitationCodeError, | ||||
| RepeatPasswordNotMatchError, | 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 extensions.ext_database import db | ||||
| from fields.member_fields import account_fields | from fields.member_fields import account_fields | ||||
| from libs.helper import TimestampField, timezone | from libs.helper import TimestampField, timezone | ||||
| return {"result": "success"} | 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 | # Register API resources | ||||
| api.add_resource(AccountInitApi, "/account/init") | api.add_resource(AccountInitApi, "/account/init") | ||||
| api.add_resource(AccountProfileApi, "/account/profile") | api.add_resource(AccountProfileApi, "/account/profile") | ||||
| api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") | api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") | ||||
| api.add_resource(AccountDeleteApi, "/account/delete") | api.add_resource(AccountDeleteApi, "/account/delete") | ||||
| api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback") | 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(AccountEmailApi, '/account/email') | ||||
| # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') | # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') |
| return decorated | 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 cloud_edition_billing_resource_check(resource: str): | ||||
| def interceptor(view): | def interceptor(view): | ||||
| @wraps(view) | @wraps(view) |
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from libs.helper import RateLimiter | from libs.helper import RateLimiter | ||||
| from models.account import TenantAccountJoin, TenantAccountRole | |||||
| from models.account import Account, TenantAccountJoin, TenantAccountRole | |||||
| class BillingService: | class BillingService: | ||||
| json = {"email": email, "feedback": feedback} | json = {"email": email, "feedback": feedback} | ||||
| return cls._send_request("POST", "/account/delete-feedback", json=json) | 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 | @classmethod | ||||
| def get_compliance_download_link( | def get_compliance_download_link( | ||||
| cls, | cls, |
| subscription: SubscriptionModel = SubscriptionModel() | subscription: SubscriptionModel = SubscriptionModel() | ||||
| class EducationModel(BaseModel): | |||||
| enabled: bool = False | |||||
| activated: bool = False | |||||
| class LimitationModel(BaseModel): | class LimitationModel(BaseModel): | ||||
| size: int = 0 | size: int = 0 | ||||
| limit: int = 0 | limit: int = 0 | ||||
| class FeatureModel(BaseModel): | class FeatureModel(BaseModel): | ||||
| billing: BillingModel = BillingModel() | billing: BillingModel = BillingModel() | ||||
| education: EducationModel = EducationModel() | |||||
| members: LimitationModel = LimitationModel(size=0, limit=1) | members: LimitationModel = LimitationModel(size=0, limit=1) | ||||
| apps: LimitationModel = LimitationModel(size=0, limit=10) | apps: LimitationModel = LimitationModel(size=0, limit=10) | ||||
| vector_space: LimitationModel = LimitationModel(size=0, limit=5) | vector_space: LimitationModel = LimitationModel(size=0, limit=5) | ||||
| features.can_replace_logo = dify_config.CAN_REPLACE_LOGO | features.can_replace_logo = dify_config.CAN_REPLACE_LOGO | ||||
| features.model_load_balancing_enabled = dify_config.MODEL_LB_ENABLED | features.model_load_balancing_enabled = dify_config.MODEL_LB_ENABLED | ||||
| features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED | features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED | ||||
| features.education.enabled = dify_config.EDUCATION_ENABLED | |||||
| @classmethod | @classmethod | ||||
| def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str): | def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str): | ||||
| features.billing.enabled = billing_info["enabled"] | features.billing.enabled = billing_info["enabled"] | ||||
| features.billing.subscription.plan = billing_info["subscription"]["plan"] | features.billing.subscription.plan = billing_info["subscription"]["plan"] | ||||
| features.billing.subscription.interval = billing_info["subscription"]["interval"] | features.billing.subscription.interval = billing_info["subscription"]["interval"] | ||||
| features.education.activated = billing_info["subscription"].get("education", False) | |||||
| if "members" in billing_info: | if "members" in billing_info: | ||||
| features.members.size = billing_info["members"]["size"] | features.members.size = billing_info["members"]["size"] |