account.py 8.7 KB

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