Compare commits

...

16 Commits

Author SHA1 Message Date
GareArc
00092e8645 fix: typo 2025-03-14 04:55:58 -04:00
GareArc
d45c8eda71 fix: renaming 2025-03-14 04:44:55 -04:00
GareArc
d43408dd7c fix: remove education field in subscription 2025-03-14 04:28:30 -04:00
GareArc
4b055595b3 fix: seperate education fields from billing 2025-03-14 04:25:31 -04:00
GareArc
31934b46d9 feat: add school role 2025-03-13 03:32:47 -04:00
GareArc
df119861c3 fix: update activcation route 2025-03-10 11:50:49 -04:00
GareArc
d8575a4537 fix: wrong params 2025-03-10 02:30:17 -04:00
GareArc
31fed12bbc feat: add autocomplete for institution search 2025-03-07 00:04:56 -05:00
GareArc
3f8c382561 fix: remove email from activation api 2025-03-06 14:20:53 -05:00
GareArc
73df389e28 fix: rename status field 2025-02-27 14:58:48 -05:00
GareArc
3a8cb8e1dd feat: add education indicator 2025-02-27 02:06:24 -05:00
GareArc
badedc70ce feat: add rate limiter 2025-02-27 00:49:37 -05:00
GareArc
89ef1f0835 fix: typo 2025-02-26 23:48:41 -05:00
GareArc
1be3ad93d2 chore: format code 2025-02-26 23:46:35 -05:00
GareArc
a4738d290e fix: add billing service health check 2025-02-26 23:45:50 -05:00
GareArc
10c40c4286 feat: add education identity support 2025-02-26 23:25:17 -05:00
7 changed files with 167 additions and 3 deletions

View File

@ -445,4 +445,7 @@ CREATE_TIDB_SERVICE_JOB_ENABLED=false
# Maximum number of submitted thread count in a ThreadPool for parallel node execution
MAX_SUBMIT_COUNT=100
# Lockout duration in seconds
LOGIN_LOCKOUT_DURATION=86400
LOGIN_LOCKOUT_DURATION=86400
# Education Identity
EDUCATION_ENABLED=false

View File

@ -844,6 +844,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

View File

@ -101,3 +101,15 @@ class AccountInFreezeError(BaseHTTPException):
"This email account has been deleted within the past 30 days"
"and is temporarily unavailable for new account registration."
)
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

View File

@ -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')

View File

@ -51,6 +51,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)

View File

@ -5,7 +5,8 @@ import httpx
from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fixed
from extensions.ext_database import db
from models.account import TenantAccountJoin, TenantAccountRole
from libs.helper import RateLimiter
from models.account import Account, TenantAccountJoin, TenantAccountRole
class BillingService:
@ -91,3 +92,45 @@ class BillingService:
"""Update account deletion feedback."""
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}
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)

View File

@ -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)
@ -111,6 +117,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):
@ -119,6 +126,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"]