選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

account.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import pytz
  2. from flask import request
  3. from flask_login import current_user
  4. from flask_restful import Resource, fields, marshal_with, reqparse
  5. from sqlalchemy import select
  6. from sqlalchemy.orm import Session
  7. from configs import dify_config
  8. from constants.languages import supported_language
  9. from controllers.console import api
  10. from controllers.console.auth.error import (
  11. EmailAlreadyInUseError,
  12. EmailChangeLimitError,
  13. EmailCodeError,
  14. InvalidEmailError,
  15. InvalidTokenError,
  16. )
  17. from controllers.console.error import AccountNotFound, EmailSendIpLimitError
  18. from controllers.console.workspace.error import (
  19. AccountAlreadyInitedError,
  20. CurrentPasswordIncorrectError,
  21. InvalidAccountDeletionCodeError,
  22. InvalidInvitationCodeError,
  23. RepeatPasswordNotMatchError,
  24. )
  25. from controllers.console.wraps import (
  26. account_initialization_required,
  27. cloud_edition_billing_enabled,
  28. enable_change_email,
  29. enterprise_license_required,
  30. only_edition_cloud,
  31. setup_required,
  32. )
  33. from extensions.ext_database import db
  34. from fields.member_fields import account_fields
  35. from libs.datetime_utils import naive_utc_now
  36. from libs.helper import TimestampField, email, extract_remote_ip, timezone
  37. from libs.login import login_required
  38. from models import AccountIntegrate, InvitationCode
  39. from models.account import Account
  40. from services.account_service import AccountService
  41. from services.billing_service import BillingService
  42. from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError
  43. class AccountInitApi(Resource):
  44. @setup_required
  45. @login_required
  46. def post(self):
  47. account = current_user
  48. if account.status == "active":
  49. raise AccountAlreadyInitedError()
  50. parser = reqparse.RequestParser()
  51. if dify_config.EDITION == "CLOUD":
  52. parser.add_argument("invitation_code", type=str, location="json")
  53. parser.add_argument("interface_language", type=supported_language, required=True, location="json")
  54. parser.add_argument("timezone", type=timezone, required=True, location="json")
  55. args = parser.parse_args()
  56. if dify_config.EDITION == "CLOUD":
  57. if not args["invitation_code"]:
  58. raise ValueError("invitation_code is required")
  59. # check invitation code
  60. invitation_code = (
  61. db.session.query(InvitationCode)
  62. .filter(
  63. InvitationCode.code == args["invitation_code"],
  64. InvitationCode.status == "unused",
  65. )
  66. .first()
  67. )
  68. if not invitation_code:
  69. raise InvalidInvitationCodeError()
  70. invitation_code.status = "used"
  71. invitation_code.used_at = naive_utc_now()
  72. invitation_code.used_by_tenant_id = account.current_tenant_id
  73. invitation_code.used_by_account_id = account.id
  74. account.interface_language = args["interface_language"]
  75. account.timezone = args["timezone"]
  76. account.interface_theme = "light"
  77. account.status = "active"
  78. account.initialized_at = naive_utc_now()
  79. db.session.commit()
  80. return {"result": "success"}
  81. class AccountProfileApi(Resource):
  82. @setup_required
  83. @login_required
  84. @account_initialization_required
  85. @marshal_with(account_fields)
  86. @enterprise_license_required
  87. def get(self):
  88. return current_user
  89. class AccountNameApi(Resource):
  90. @setup_required
  91. @login_required
  92. @account_initialization_required
  93. @marshal_with(account_fields)
  94. def post(self):
  95. parser = reqparse.RequestParser()
  96. parser.add_argument("name", type=str, required=True, location="json")
  97. args = parser.parse_args()
  98. # Validate account name length
  99. if len(args["name"]) < 3 or len(args["name"]) > 30:
  100. raise ValueError("Account name must be between 3 and 30 characters.")
  101. updated_account = AccountService.update_account(current_user, name=args["name"])
  102. return updated_account
  103. class AccountAvatarApi(Resource):
  104. @setup_required
  105. @login_required
  106. @account_initialization_required
  107. @marshal_with(account_fields)
  108. def post(self):
  109. parser = reqparse.RequestParser()
  110. parser.add_argument("avatar", type=str, required=True, location="json")
  111. args = parser.parse_args()
  112. updated_account = AccountService.update_account(current_user, avatar=args["avatar"])
  113. return updated_account
  114. class AccountInterfaceLanguageApi(Resource):
  115. @setup_required
  116. @login_required
  117. @account_initialization_required
  118. @marshal_with(account_fields)
  119. def post(self):
  120. parser = reqparse.RequestParser()
  121. parser.add_argument("interface_language", type=supported_language, required=True, location="json")
  122. args = parser.parse_args()
  123. updated_account = AccountService.update_account(current_user, interface_language=args["interface_language"])
  124. return updated_account
  125. class AccountInterfaceThemeApi(Resource):
  126. @setup_required
  127. @login_required
  128. @account_initialization_required
  129. @marshal_with(account_fields)
  130. def post(self):
  131. parser = reqparse.RequestParser()
  132. parser.add_argument("interface_theme", type=str, choices=["light", "dark"], required=True, location="json")
  133. args = parser.parse_args()
  134. updated_account = AccountService.update_account(current_user, interface_theme=args["interface_theme"])
  135. return updated_account
  136. class AccountTimezoneApi(Resource):
  137. @setup_required
  138. @login_required
  139. @account_initialization_required
  140. @marshal_with(account_fields)
  141. def post(self):
  142. parser = reqparse.RequestParser()
  143. parser.add_argument("timezone", type=str, required=True, location="json")
  144. args = parser.parse_args()
  145. # Validate timezone string, e.g. America/New_York, Asia/Shanghai
  146. if args["timezone"] not in pytz.all_timezones:
  147. raise ValueError("Invalid timezone string.")
  148. updated_account = AccountService.update_account(current_user, timezone=args["timezone"])
  149. return updated_account
  150. class AccountPasswordApi(Resource):
  151. @setup_required
  152. @login_required
  153. @account_initialization_required
  154. @marshal_with(account_fields)
  155. def post(self):
  156. parser = reqparse.RequestParser()
  157. parser.add_argument("password", type=str, required=False, location="json")
  158. parser.add_argument("new_password", type=str, required=True, location="json")
  159. parser.add_argument("repeat_new_password", type=str, required=True, location="json")
  160. args = parser.parse_args()
  161. if args["new_password"] != args["repeat_new_password"]:
  162. raise RepeatPasswordNotMatchError()
  163. try:
  164. AccountService.update_account_password(current_user, args["password"], args["new_password"])
  165. except ServiceCurrentPasswordIncorrectError:
  166. raise CurrentPasswordIncorrectError()
  167. return {"result": "success"}
  168. class AccountIntegrateApi(Resource):
  169. integrate_fields = {
  170. "provider": fields.String,
  171. "created_at": TimestampField,
  172. "is_bound": fields.Boolean,
  173. "link": fields.String,
  174. }
  175. integrate_list_fields = {
  176. "data": fields.List(fields.Nested(integrate_fields)),
  177. }
  178. @setup_required
  179. @login_required
  180. @account_initialization_required
  181. @marshal_with(integrate_list_fields)
  182. def get(self):
  183. account = current_user
  184. account_integrates = db.session.query(AccountIntegrate).filter(AccountIntegrate.account_id == account.id).all()
  185. base_url = request.url_root.rstrip("/")
  186. oauth_base_path = "/console/api/oauth/login"
  187. providers = ["github", "google"]
  188. integrate_data = []
  189. for provider in providers:
  190. existing_integrate = next((ai for ai in account_integrates if ai.provider == provider), None)
  191. if existing_integrate:
  192. integrate_data.append(
  193. {
  194. "id": existing_integrate.id,
  195. "provider": provider,
  196. "created_at": existing_integrate.created_at,
  197. "is_bound": True,
  198. "link": None,
  199. }
  200. )
  201. else:
  202. integrate_data.append(
  203. {
  204. "id": None,
  205. "provider": provider,
  206. "created_at": None,
  207. "is_bound": False,
  208. "link": f"{base_url}{oauth_base_path}/{provider}",
  209. }
  210. )
  211. return {"data": integrate_data}
  212. class AccountDeleteVerifyApi(Resource):
  213. @setup_required
  214. @login_required
  215. @account_initialization_required
  216. def get(self):
  217. account = current_user
  218. token, code = AccountService.generate_account_deletion_verification_code(account)
  219. AccountService.send_account_deletion_verification_email(account, code)
  220. return {"result": "success", "data": token}
  221. class AccountDeleteApi(Resource):
  222. @setup_required
  223. @login_required
  224. @account_initialization_required
  225. def post(self):
  226. account = current_user
  227. parser = reqparse.RequestParser()
  228. parser.add_argument("token", type=str, required=True, location="json")
  229. parser.add_argument("code", type=str, required=True, location="json")
  230. args = parser.parse_args()
  231. if not AccountService.verify_account_deletion_code(args["token"], args["code"]):
  232. raise InvalidAccountDeletionCodeError()
  233. AccountService.delete_account(account)
  234. return {"result": "success"}
  235. class AccountDeleteUpdateFeedbackApi(Resource):
  236. @setup_required
  237. def post(self):
  238. parser = reqparse.RequestParser()
  239. parser.add_argument("email", type=str, required=True, location="json")
  240. parser.add_argument("feedback", type=str, required=True, location="json")
  241. args = parser.parse_args()
  242. BillingService.update_account_deletion_feedback(args["email"], args["feedback"])
  243. return {"result": "success"}
  244. class EducationVerifyApi(Resource):
  245. verify_fields = {
  246. "token": fields.String,
  247. }
  248. @setup_required
  249. @login_required
  250. @account_initialization_required
  251. @only_edition_cloud
  252. @cloud_edition_billing_enabled
  253. @marshal_with(verify_fields)
  254. def get(self):
  255. account = current_user
  256. return BillingService.EducationIdentity.verify(account.id, account.email)
  257. class EducationApi(Resource):
  258. status_fields = {
  259. "result": fields.Boolean,
  260. }
  261. @setup_required
  262. @login_required
  263. @account_initialization_required
  264. @only_edition_cloud
  265. @cloud_edition_billing_enabled
  266. def post(self):
  267. account = current_user
  268. parser = reqparse.RequestParser()
  269. parser.add_argument("token", type=str, required=True, location="json")
  270. parser.add_argument("institution", type=str, required=True, location="json")
  271. parser.add_argument("role", type=str, required=True, location="json")
  272. args = parser.parse_args()
  273. return BillingService.EducationIdentity.activate(account, args["token"], args["institution"], args["role"])
  274. @setup_required
  275. @login_required
  276. @account_initialization_required
  277. @only_edition_cloud
  278. @cloud_edition_billing_enabled
  279. @marshal_with(status_fields)
  280. def get(self):
  281. account = current_user
  282. return BillingService.EducationIdentity.is_active(account.id)
  283. class EducationAutoCompleteApi(Resource):
  284. data_fields = {
  285. "data": fields.List(fields.String),
  286. "curr_page": fields.Integer,
  287. "has_next": fields.Boolean,
  288. }
  289. @setup_required
  290. @login_required
  291. @account_initialization_required
  292. @only_edition_cloud
  293. @cloud_edition_billing_enabled
  294. @marshal_with(data_fields)
  295. def get(self):
  296. parser = reqparse.RequestParser()
  297. parser.add_argument("keywords", type=str, required=True, location="args")
  298. parser.add_argument("page", type=int, required=False, location="args", default=0)
  299. parser.add_argument("limit", type=int, required=False, location="args", default=20)
  300. args = parser.parse_args()
  301. return BillingService.EducationIdentity.autocomplete(args["keywords"], args["page"], args["limit"])
  302. class ChangeEmailSendEmailApi(Resource):
  303. @enable_change_email
  304. @setup_required
  305. @login_required
  306. @account_initialization_required
  307. def post(self):
  308. parser = reqparse.RequestParser()
  309. parser.add_argument("email", type=email, required=True, location="json")
  310. parser.add_argument("language", type=str, required=False, location="json")
  311. parser.add_argument("phase", type=str, required=False, location="json")
  312. parser.add_argument("token", type=str, required=False, location="json")
  313. args = parser.parse_args()
  314. ip_address = extract_remote_ip(request)
  315. if AccountService.is_email_send_ip_limit(ip_address):
  316. raise EmailSendIpLimitError()
  317. if args["language"] is not None and args["language"] == "zh-Hans":
  318. language = "zh-Hans"
  319. else:
  320. language = "en-US"
  321. account = None
  322. user_email = args["email"]
  323. if args["phase"] is not None and args["phase"] == "new_email":
  324. if args["token"] is None:
  325. raise InvalidTokenError()
  326. reset_data = AccountService.get_change_email_data(args["token"])
  327. if reset_data is None:
  328. raise InvalidTokenError()
  329. user_email = reset_data.get("email", "")
  330. if user_email != current_user.email:
  331. raise InvalidEmailError()
  332. else:
  333. with Session(db.engine) as session:
  334. account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
  335. if account is None:
  336. raise AccountNotFound()
  337. token = AccountService.send_change_email_email(
  338. account=account, email=args["email"], old_email=user_email, language=language, phase=args["phase"]
  339. )
  340. return {"result": "success", "data": token}
  341. class ChangeEmailCheckApi(Resource):
  342. @enable_change_email
  343. @setup_required
  344. @login_required
  345. @account_initialization_required
  346. def post(self):
  347. parser = reqparse.RequestParser()
  348. parser.add_argument("email", type=email, required=True, location="json")
  349. parser.add_argument("code", type=str, required=True, location="json")
  350. parser.add_argument("token", type=str, required=True, nullable=False, location="json")
  351. args = parser.parse_args()
  352. user_email = args["email"]
  353. is_change_email_error_rate_limit = AccountService.is_change_email_error_rate_limit(args["email"])
  354. if is_change_email_error_rate_limit:
  355. raise EmailChangeLimitError()
  356. token_data = AccountService.get_change_email_data(args["token"])
  357. if token_data is None:
  358. raise InvalidTokenError()
  359. if user_email != token_data.get("email"):
  360. raise InvalidEmailError()
  361. if args["code"] != token_data.get("code"):
  362. AccountService.add_change_email_error_rate_limit(args["email"])
  363. raise EmailCodeError()
  364. # Verified, revoke the first token
  365. AccountService.revoke_change_email_token(args["token"])
  366. # Refresh token data by generating a new token
  367. _, new_token = AccountService.generate_change_email_token(
  368. user_email, code=args["code"], old_email=token_data.get("old_email"), additional_data={}
  369. )
  370. AccountService.reset_change_email_error_rate_limit(args["email"])
  371. return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
  372. class ChangeEmailResetApi(Resource):
  373. @enable_change_email
  374. @setup_required
  375. @login_required
  376. @account_initialization_required
  377. @marshal_with(account_fields)
  378. def post(self):
  379. parser = reqparse.RequestParser()
  380. parser.add_argument("new_email", type=email, required=True, location="json")
  381. parser.add_argument("token", type=str, required=True, nullable=False, location="json")
  382. args = parser.parse_args()
  383. reset_data = AccountService.get_change_email_data(args["token"])
  384. if not reset_data:
  385. raise InvalidTokenError()
  386. AccountService.revoke_change_email_token(args["token"])
  387. if not AccountService.check_email_unique(args["new_email"]):
  388. raise EmailAlreadyInUseError()
  389. old_email = reset_data.get("old_email", "")
  390. if current_user.email != old_email:
  391. raise AccountNotFound()
  392. updated_account = AccountService.update_account(current_user, email=args["new_email"])
  393. return updated_account
  394. class CheckEmailUnique(Resource):
  395. @setup_required
  396. def post(self):
  397. parser = reqparse.RequestParser()
  398. parser.add_argument("email", type=email, required=True, location="json")
  399. args = parser.parse_args()
  400. if not AccountService.check_email_unique(args["email"]):
  401. raise EmailAlreadyInUseError()
  402. return {"result": "success"}
  403. # Register API resources
  404. api.add_resource(AccountInitApi, "/account/init")
  405. api.add_resource(AccountProfileApi, "/account/profile")
  406. api.add_resource(AccountNameApi, "/account/name")
  407. api.add_resource(AccountAvatarApi, "/account/avatar")
  408. api.add_resource(AccountInterfaceLanguageApi, "/account/interface-language")
  409. api.add_resource(AccountInterfaceThemeApi, "/account/interface-theme")
  410. api.add_resource(AccountTimezoneApi, "/account/timezone")
  411. api.add_resource(AccountPasswordApi, "/account/password")
  412. api.add_resource(AccountIntegrateApi, "/account/integrates")
  413. api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify")
  414. api.add_resource(AccountDeleteApi, "/account/delete")
  415. api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback")
  416. api.add_resource(EducationVerifyApi, "/account/education/verify")
  417. api.add_resource(EducationApi, "/account/education")
  418. api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete")
  419. # Change email
  420. api.add_resource(ChangeEmailSendEmailApi, "/account/change-email")
  421. api.add_resource(ChangeEmailCheckApi, "/account/change-email/validity")
  422. api.add_resource(ChangeEmailResetApi, "/account/change-email/reset")
  423. api.add_resource(CheckEmailUnique, "/account/change-email/check-email-unique")
  424. # api.add_resource(AccountEmailApi, '/account/email')
  425. # api.add_resource(AccountEmailVerifyApi, '/account/email-verify')