account.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import datetime
  2. import pytz
  3. from flask import current_app, request
  4. from flask_login import current_user
  5. from flask_restful import Resource, fields, marshal_with, reqparse
  6. from constants.languages import supported_language
  7. from controllers.console import api
  8. from controllers.console.setup import setup_required
  9. from controllers.console.workspace.error import (
  10. AccountAlreadyInitedError,
  11. CurrentPasswordIncorrectError,
  12. InvalidInvitationCodeError,
  13. RepeatPasswordNotMatchError,
  14. )
  15. from controllers.console.wraps import account_initialization_required
  16. from extensions.ext_database import db
  17. from fields.member_fields import account_fields
  18. from libs.helper import TimestampField, timezone
  19. from libs.login import login_required
  20. from models.account import AccountIntegrate, InvitationCode
  21. from services.account_service import AccountService
  22. from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError
  23. class AccountInitApi(Resource):
  24. @setup_required
  25. @login_required
  26. def post(self):
  27. account = current_user
  28. if account.status == 'active':
  29. raise AccountAlreadyInitedError()
  30. parser = reqparse.RequestParser()
  31. if current_app.config['EDITION'] == 'CLOUD':
  32. parser.add_argument('invitation_code', type=str, location='json')
  33. parser.add_argument(
  34. 'interface_language', type=supported_language, required=True, location='json')
  35. parser.add_argument('timezone', type=timezone,
  36. required=True, location='json')
  37. args = parser.parse_args()
  38. if current_app.config['EDITION'] == 'CLOUD':
  39. if not args['invitation_code']:
  40. raise ValueError('invitation_code is required')
  41. # check invitation code
  42. invitation_code = db.session.query(InvitationCode).filter(
  43. InvitationCode.code == args['invitation_code'],
  44. InvitationCode.status == 'unused',
  45. ).first()
  46. if not invitation_code:
  47. raise InvalidInvitationCodeError()
  48. invitation_code.status = 'used'
  49. invitation_code.used_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
  50. invitation_code.used_by_tenant_id = account.current_tenant_id
  51. invitation_code.used_by_account_id = account.id
  52. account.interface_language = args['interface_language']
  53. account.timezone = args['timezone']
  54. account.interface_theme = 'light'
  55. account.status = 'active'
  56. account.initialized_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
  57. db.session.commit()
  58. return {'result': 'success'}
  59. class AccountProfileApi(Resource):
  60. @setup_required
  61. @login_required
  62. @account_initialization_required
  63. @marshal_with(account_fields)
  64. def get(self):
  65. return current_user
  66. class AccountNameApi(Resource):
  67. @setup_required
  68. @login_required
  69. @account_initialization_required
  70. @marshal_with(account_fields)
  71. def post(self):
  72. parser = reqparse.RequestParser()
  73. parser.add_argument('name', type=str, required=True, location='json')
  74. args = parser.parse_args()
  75. # Validate account name length
  76. if len(args['name']) < 3 or len(args['name']) > 30:
  77. raise ValueError(
  78. "Account name must be between 3 and 30 characters.")
  79. updated_account = AccountService.update_account(current_user, name=args['name'])
  80. return updated_account
  81. class AccountAvatarApi(Resource):
  82. @setup_required
  83. @login_required
  84. @account_initialization_required
  85. @marshal_with(account_fields)
  86. def post(self):
  87. parser = reqparse.RequestParser()
  88. parser.add_argument('avatar', type=str, required=True, location='json')
  89. args = parser.parse_args()
  90. updated_account = AccountService.update_account(current_user, avatar=args['avatar'])
  91. return updated_account
  92. class AccountInterfaceLanguageApi(Resource):
  93. @setup_required
  94. @login_required
  95. @account_initialization_required
  96. @marshal_with(account_fields)
  97. def post(self):
  98. parser = reqparse.RequestParser()
  99. parser.add_argument(
  100. 'interface_language', type=supported_language, required=True, location='json')
  101. args = parser.parse_args()
  102. updated_account = AccountService.update_account(current_user, interface_language=args['interface_language'])
  103. return updated_account
  104. class AccountInterfaceThemeApi(Resource):
  105. @setup_required
  106. @login_required
  107. @account_initialization_required
  108. @marshal_with(account_fields)
  109. def post(self):
  110. parser = reqparse.RequestParser()
  111. parser.add_argument('interface_theme', type=str, choices=[
  112. 'light', 'dark'], required=True, location='json')
  113. args = parser.parse_args()
  114. updated_account = AccountService.update_account(current_user, interface_theme=args['interface_theme'])
  115. return updated_account
  116. class AccountTimezoneApi(Resource):
  117. @setup_required
  118. @login_required
  119. @account_initialization_required
  120. @marshal_with(account_fields)
  121. def post(self):
  122. parser = reqparse.RequestParser()
  123. parser.add_argument('timezone', type=str,
  124. required=True, location='json')
  125. args = parser.parse_args()
  126. # Validate timezone string, e.g. America/New_York, Asia/Shanghai
  127. if args['timezone'] not in pytz.all_timezones:
  128. raise ValueError("Invalid timezone string.")
  129. updated_account = AccountService.update_account(current_user, timezone=args['timezone'])
  130. return updated_account
  131. class AccountPasswordApi(Resource):
  132. @setup_required
  133. @login_required
  134. @account_initialization_required
  135. @marshal_with(account_fields)
  136. def post(self):
  137. parser = reqparse.RequestParser()
  138. parser.add_argument('password', type=str,
  139. required=False, location='json')
  140. parser.add_argument('new_password', type=str,
  141. required=True, location='json')
  142. parser.add_argument('repeat_new_password', type=str,
  143. required=True, location='json')
  144. args = parser.parse_args()
  145. if args['new_password'] != args['repeat_new_password']:
  146. raise RepeatPasswordNotMatchError()
  147. try:
  148. AccountService.update_account_password(
  149. current_user, args['password'], args['new_password'])
  150. except ServiceCurrentPasswordIncorrectError:
  151. raise CurrentPasswordIncorrectError()
  152. return {"result": "success"}
  153. class AccountIntegrateApi(Resource):
  154. integrate_fields = {
  155. 'provider': fields.String,
  156. 'created_at': TimestampField,
  157. 'is_bound': fields.Boolean,
  158. 'link': fields.String
  159. }
  160. integrate_list_fields = {
  161. 'data': fields.List(fields.Nested(integrate_fields)),
  162. }
  163. @setup_required
  164. @login_required
  165. @account_initialization_required
  166. @marshal_with(integrate_list_fields)
  167. def get(self):
  168. account = current_user
  169. account_integrates = db.session.query(AccountIntegrate).filter(
  170. AccountIntegrate.account_id == account.id).all()
  171. base_url = request.url_root.rstrip('/')
  172. oauth_base_path = "/console/api/oauth/login"
  173. providers = ["github", "google"]
  174. integrate_data = []
  175. for provider in providers:
  176. existing_integrate = next((ai for ai in account_integrates if ai.provider == provider), None)
  177. if existing_integrate:
  178. integrate_data.append({
  179. 'id': existing_integrate.id,
  180. 'provider': provider,
  181. 'created_at': existing_integrate.created_at,
  182. 'is_bound': True,
  183. 'link': None
  184. })
  185. else:
  186. integrate_data.append({
  187. 'id': None,
  188. 'provider': provider,
  189. 'created_at': None,
  190. 'is_bound': False,
  191. 'link': f'{base_url}{oauth_base_path}/{provider}'
  192. })
  193. return {'data': integrate_data}
  194. # Register API resources
  195. api.add_resource(AccountInitApi, '/account/init')
  196. api.add_resource(AccountProfileApi, '/account/profile')
  197. api.add_resource(AccountNameApi, '/account/name')
  198. api.add_resource(AccountAvatarApi, '/account/avatar')
  199. api.add_resource(AccountInterfaceLanguageApi, '/account/interface-language')
  200. api.add_resource(AccountInterfaceThemeApi, '/account/interface-theme')
  201. api.add_resource(AccountTimezoneApi, '/account/timezone')
  202. api.add_resource(AccountPasswordApi, '/account/password')
  203. api.add_resource(AccountIntegrateApi, '/account/integrates')
  204. # api.add_resource(AccountEmailApi, '/account/email')
  205. # api.add_resource(AccountEmailVerifyApi, '/account/email-verify')