hiddifypanel 10.20.3__py3-none-any.whl → 10.30.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +2 -2
  3. hiddifypanel/base.py +17 -8
  4. hiddifypanel/cache.py +2 -51
  5. hiddifypanel/drivers/wireguard_api.py +24 -5
  6. hiddifypanel/hutils/convert.py +1 -1
  7. hiddifypanel/hutils/flask.py +28 -2
  8. hiddifypanel/hutils/importer/xui.py +6 -7
  9. hiddifypanel/hutils/network/__init__.py +1 -0
  10. hiddifypanel/hutils/network/cf_api.py +84 -0
  11. hiddifypanel/hutils/network/net.py +26 -49
  12. hiddifypanel/hutils/node/child.py +25 -7
  13. hiddifypanel/hutils/node/parent.py +7 -7
  14. hiddifypanel/hutils/node/shared.py +19 -6
  15. hiddifypanel/hutils/proxy/clash.py +1 -1
  16. hiddifypanel/hutils/proxy/shared.py +1 -1
  17. hiddifypanel/hutils/proxy/singbox.py +2 -3
  18. hiddifypanel/hutils/proxy/xray.py +12 -10
  19. hiddifypanel/hutils/proxy/xrayjson.py +26 -49
  20. hiddifypanel/hutils/utils.py +47 -3
  21. hiddifypanel/models/__init__.py +1 -1
  22. hiddifypanel/models/admin.py +9 -2
  23. hiddifypanel/models/base_account.py +3 -1
  24. hiddifypanel/models/config.py +5 -7
  25. hiddifypanel/models/config_enum.py +18 -6
  26. hiddifypanel/models/domain.py +82 -118
  27. hiddifypanel/models/user.py +44 -24
  28. hiddifypanel/panel/admin/Actions.py +6 -11
  29. hiddifypanel/panel/admin/AdminstratorAdmin.py +3 -9
  30. hiddifypanel/panel/admin/Backup.py +5 -8
  31. hiddifypanel/panel/admin/Dashboard.py +3 -4
  32. hiddifypanel/panel/admin/DomainAdmin.py +20 -15
  33. hiddifypanel/panel/admin/ProxyAdmin.py +3 -10
  34. hiddifypanel/panel/admin/QuickSetup.py +1 -1
  35. hiddifypanel/panel/admin/SettingAdmin.py +7 -5
  36. hiddifypanel/panel/admin/Terminal.py +0 -1
  37. hiddifypanel/panel/admin/UserAdmin.py +4 -3
  38. hiddifypanel/panel/cli.py +36 -23
  39. hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +2 -4
  40. hiddifypanel/panel/commercial/restapi/v1/tgbot.py +7 -4
  41. hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +17 -13
  42. hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +4 -3
  43. hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +28 -10
  44. hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +2 -19
  45. hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +27 -4
  46. hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +28 -9
  47. hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +1 -21
  48. hiddifypanel/panel/commercial/restapi/v2/parent/register_api.py +1 -1
  49. hiddifypanel/panel/commercial/restapi/v2/parent/schema.py +8 -4
  50. hiddifypanel/panel/commercial/restapi/v2/parent/sync_api.py +19 -3
  51. hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +48 -42
  52. hiddifypanel/panel/commercial/telegrambot/Usage.py +1 -1
  53. hiddifypanel/panel/commercial/telegrambot/admin.py +1 -1
  54. hiddifypanel/panel/commercial/telegrambot/information.py +1 -1
  55. hiddifypanel/panel/common.py +5 -11
  56. hiddifypanel/panel/hiddify.py +9 -20
  57. hiddifypanel/panel/init_db.py +31 -13
  58. hiddifypanel/panel/usage.py +38 -9
  59. hiddifypanel/panel/user/user.py +52 -32
  60. hiddifypanel/templates/admin-layout.html +2 -2
  61. hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
  62. hiddifypanel/translations/en/LC_MESSAGES/messages.po +80 -25
  63. hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
  64. hiddifypanel/translations/fa/LC_MESSAGES/messages.po +74 -20
  65. hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
  66. hiddifypanel/translations/pt/LC_MESSAGES/messages.po +60 -6
  67. hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
  68. hiddifypanel/translations/ru/LC_MESSAGES/messages.po +158 -78
  69. hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
  70. hiddifypanel/translations/zh/LC_MESSAGES/messages.po +60 -6
  71. hiddifypanel/translations.i18n/en.json +62 -22
  72. hiddifypanel/translations.i18n/fa.json +57 -17
  73. hiddifypanel/translations.i18n/pt.json +43 -3
  74. hiddifypanel/translations.i18n/ru.json +112 -72
  75. hiddifypanel/translations.i18n/zh.json +43 -3
  76. {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/METADATA +2 -1
  77. {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/RECORD +81 -81
  78. {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/WHEEL +1 -1
  79. hiddifypanel/panel/cf_api.py +0 -37
  80. {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/LICENSE.md +0 -0
  81. {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/entry_points.txt +0 -0
  82. {hiddifypanel-10.20.3.dist-info → hiddifypanel-10.30.0.dev0.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ from flask_bootstrap import SwitchField
16
16
  # from gettext import gettext as _
17
17
  from flask_classful import FlaskView
18
18
  from flask_wtf import FlaskForm
19
+ from bleach import clean as bleach_clean
19
20
 
20
21
 
21
22
  from hiddifypanel.models import BoolConfig, StrConfig, ConfigEnum, hconfig, ConfigCategory
@@ -66,7 +67,7 @@ class SettingAdmin(FlaskView):
66
67
  return render_template('config.html', form=form)
67
68
  if k == ConfigEnum.parent_panel and v != '':
68
69
  # v=(v+"/").replace("/admin",'')
69
- v = re.sub("(/admin/.*)", "/", v)
70
+ v = re.sub("(/admin/.*)", "/", v) + ("/" if not v.endswith("/") else "")
70
71
 
71
72
  if old_configs[k] != v:
72
73
  changed_configs[k] = v
@@ -91,6 +92,10 @@ class SettingAdmin(FlaskView):
91
92
  parent_apikey = uuid
92
93
 
93
94
  for k, v in changed_configs.items():
95
+ # html inputs santitizing
96
+ san_items = {ConfigEnum.branding_title, ConfigEnum.branding_site, ConfigEnum.branding_freetext}
97
+ if k in san_items:
98
+ v = bleach_clean(v)
94
99
  set_hconfig(k, v, commit=False)
95
100
 
96
101
  db.session.commit()
@@ -112,10 +117,7 @@ class SettingAdmin(FlaskView):
112
117
  # sync with parent if needed
113
118
  if hutils.node.is_child():
114
119
  if hutils.node.child.is_registered():
115
- if not hutils.node.child.sync_with_parent():
116
- hutils.flask.flash(_('child.sync-failed'), 'danger') # type: ignore
117
- else: # TODO: it's just for debuging
118
- hutils.flask.flash(_('child.sync-success')) # type: ignore
120
+ hutils.node.run_node_op_in_bg(hutils.node.child.sync_with_parent, *[hutils.node.child.SyncFields.hconfigs])
119
121
  else:
120
122
  name = hconfig(ConfigEnum.unique_id)
121
123
  parent_info = hutils.node.get_panel_info(hconfig(ConfigEnum.parent_domain), hconfig(ConfigEnum.parent_admin_proxy_path), parent_apikey)
@@ -1,4 +1,3 @@
1
- # #!/usr/bin/env python3
2
1
  # from hiddifypanel.database import db
3
2
  # import uuid
4
3
  # from flask_babel import gettext as _
@@ -151,7 +151,7 @@ class UserAdmin(AdminLTEModelView):
151
151
  <i class='fa-solid fa-arrow-up-right-from-square'></i>
152
152
  {_("Current Domain")} </a>"""
153
153
 
154
- domains = [d for d in get_panel_domains() if d.domain != request.host]
154
+ domains = [d for d in Domain.get_domains() if d.domain != request.host]
155
155
  return Markup(link + " ".join([hiddify.get_html_user_link(model, d) for d in domains]))
156
156
 
157
157
  # def _usage_formatter(view, context, model, name):
@@ -300,14 +300,15 @@ class UserAdmin(AdminLTEModelView):
300
300
  hiddify.quick_apply_users()
301
301
 
302
302
  if hutils.node.is_parent():
303
- hutils.node.parent.request_childs_to_sync()
303
+ hutils.node.run_node_op_in_bg(hutils.node.parent.request_childs_to_sync)
304
+
304
305
 
305
306
  def after_model_delete(self, model):
306
307
  user_driver.remove_client(model)
307
308
  hiddify.quick_apply_users()
308
309
 
309
310
  if hutils.node.is_parent():
310
- hutils.node.parent.request_childs_to_sync()
311
+ hutils.node.run_node_op_in_bg(hutils.node.parent.request_childs_to_sync)
311
312
 
312
313
  def get_list(self, page, sort_column, sort_desc, search, filters, page_size=50, *args, **kwargs):
313
314
  res = None
hiddifypanel/panel/cli.py CHANGED
@@ -53,7 +53,6 @@ def all_configs():
53
53
  configs = {
54
54
  "users": valid_users,
55
55
  "domains": [u.to_dict(dump_ports=True, dump_child_id=True) for u in Domain.query.filter(Domain.child_id.in_(host_child_ids)).all() if "*" not in u.domain],
56
- # "parent_domains": [hiddify.parent_domain_dict(u) for u in ParentDomain.query.all()],
57
56
  # "hconfigs": get_hconfigs(json=True),
58
57
  "chconfigs": get_hconfigs_childs(host_child_ids, json=True)
59
58
  }
@@ -70,7 +69,7 @@ def all_configs():
70
69
  configs['panel_links'] = []
71
70
  configs['panel_links'].append(hiddify.get_account_panel_link(owner, server_ip, is_https=False))
72
71
  configs['panel_links'].append(hiddify.get_account_panel_link(owner, server_ip))
73
- domains = get_panel_domains()
72
+ domains = Domain.get_domains()
74
73
 
75
74
  for d in domains:
76
75
  configs['panel_links'].append(hiddify.get_account_panel_link(owner, d.domain))
@@ -82,18 +81,13 @@ def update_usage():
82
81
  print(usage.update_local_usage())
83
82
 
84
83
 
85
- def test():
86
- print(ConfigEnum("auto_update1"))
87
-
88
-
89
84
  def admin_links():
90
-
91
85
  server_ip = hutils.network.get_ip_str(4)
92
86
  owner = AdminUser.get_super_admin()
93
87
 
94
88
  admin_links = f"Not Secure (do not use it - only if others not work):\n {hiddify.get_account_panel_link(owner, server_ip,is_https=True)}\n"
95
89
 
96
- domains = get_panel_domains()
90
+ domains = Domain.get_domains()
97
91
  admin_links += f"Secure:\n"
98
92
  if not any([d for d in domains if 'sslip.io' not in d.domain]):
99
93
  admin_links += f" (not signed) {hiddify.get_account_panel_link(owner, server_ip)}\n"
@@ -108,19 +102,15 @@ def admin_links():
108
102
  def admin_path():
109
103
  admin = AdminUser.get_super_admin()
110
104
  # WTF is the owner and server_id?
111
- domain = get_panel_domains()[0]
105
+ domain = Domain.get_domains()[0]
112
106
  print(hiddify.get_account_panel_link(admin, domain, prefere_path_only=True))
113
107
 
114
108
 
115
- # def get_this_host_domains():
116
- # current_child_ids
117
-
118
-
119
109
  def hysteria_domain_port():
120
110
  if not hconfig(ConfigEnum.hysteria_enable):
121
111
  return
122
112
  out = []
123
- for i, domain in enumerate(Domain.query.filter(Domain.mode.in_([DomainType.direct, DomainType.relay, DomainType.fake])).all()):
113
+ for domain in Domain.query.filter(Domain.mode.in_([DomainType.direct, DomainType.relay, DomainType.fake])).all():
124
114
  out.append(f"{domain.domain}:{int(hconfig(ConfigEnum.hysteria_port))+domain.id}")
125
115
  print(";".join(out))
126
116
 
@@ -129,26 +119,25 @@ def tuic_domain_port():
129
119
  if not hconfig(ConfigEnum.tuic_enable):
130
120
  return
131
121
  out = []
132
- for i, domain in enumerate(Domain.query.filter(Domain.mode.in_([DomainType.direct, DomainType.relay, DomainType.fake])).all()):
122
+ for domain in Domain.query.filter(Domain.mode.in_([DomainType.direct, DomainType.relay, DomainType.fake])).all():
133
123
  out.append(f"{domain}:{int(hconfig(ConfigEnum.tuic_port))+domain.id}")
134
124
  print(";".join(out))
135
125
 
136
126
 
137
127
  def init_app(app):
138
- # add multiple commands in a bulk
139
- # print(app.config['SQLALCHEMY_DATABASE_URI'] )
140
- for command in [hysteria_domain_port, tuic_domain_port, init_db, drop_db, all_configs, update_usage, test, admin_links, admin_path, backup, downgrade]:
128
+ for command in [hysteria_domain_port, tuic_domain_port, init_db, drop_db, all_configs, update_usage, admin_links, admin_path, backup, downgrade]:
141
129
  app.cli.add_command(app.cli.command()(command))
142
130
 
143
131
  @ app.cli.command()
144
132
  @ click.option("--domain", "-d")
145
- def add_domain(domain):
146
- # TODO: Fix this
133
+ @ click.option("--mode", "-m")
134
+ def add_domain(domain, mode):
147
135
  if Domain.query.filter(Domain.domain == domain).first():
148
136
  return "Domain already exist."
149
- d = Domain(domain=domain)
150
- if not hutils.node.is_parent():
151
- d.mode = DomainType.direct
137
+ d = Domain()
138
+ d.domain = domain
139
+ d.mode = mode
140
+ d.sub_link_only = True if mode == DomainType.sub_link_only else False
152
141
  db.session.add(d)
153
142
  db.session.commit()
154
143
  return "success"
@@ -236,3 +225,27 @@ def init_app(app):
236
225
  print('success')
237
226
  except Exception as e:
238
227
  print(f'failed to import xui data: Error: {e}')
228
+
229
+ @ app.cli.command()
230
+ def tgbot_info():
231
+ if not hconfig(ConfigEnum.telegram_bot_token):
232
+ print('You didn\'t specified your telegram bot token')
233
+ return
234
+
235
+ from hiddifypanel.panel.commercial.telegrambot import bot, register_bot
236
+ if not bot.token:
237
+ register_bot(True)
238
+ info = bot.get_me().to_dict()
239
+ hook_data = bot.get_webhook_info()
240
+ hook_info = {
241
+ 'url': hook_data.url,
242
+ 'ip': hook_data.ip_address,
243
+ 'last_error_msg': hook_data.last_error_message if hook_data.last_error_message else '',
244
+ 'last_error_time': datetime.datetime.fromtimestamp(int(hook_data.last_error_date)).strftime('%Y-%m-%d %H:%M:%S') if hook_data.last_error_date else ''
245
+ }
246
+
247
+ output = {
248
+ 'general': info,
249
+ 'webhook': hook_info
250
+ }
251
+ print(json.dumps(output, indent=4))
@@ -44,15 +44,13 @@ class ProxyDetailsAdmin(AdminLTEModelView):
44
44
 
45
45
  def after_model_change(self, form, model, is_created):
46
46
  if hutils.node.is_child():
47
- if not hutils.node.child.sync_with_parent():
48
- hutils.flask.flash(_('child.sync-failed'), 'danger') # type: ignore
47
+ hutils.node.run_node_op_in_bg(hutils.node.child.sync_with_parent, *[hutils.node.child.SyncFields.proxies])
49
48
  hutils.proxy.get_proxies.invalidate_all()
50
49
  pass
51
50
 
52
51
  def after_model_delete(self, model):
53
52
  if hutils.node.is_child():
54
- if not hutils.node.child.sync_with_parent():
55
- hutils.flask.flash(_('child.sync-failed'), 'danger') # type: ignore
53
+ hutils.node.run_node_op_in_bg(hutils.node.child.sync_with_parent, *[hutils.node.child.SyncFields.proxies])
56
54
  hutils.proxy.get_proxies.invalidate_all()
57
55
  pass
58
56
 
@@ -17,7 +17,7 @@ bot = telebot.TeleBot("", parse_mode="HTML", threaded=False, exception_handler=E
17
17
  bot.username = ''
18
18
 
19
19
 
20
- def register_bot(set_hook=False):
20
+ def register_bot(set_hook=False, remove_hook=False):
21
21
  try:
22
22
  global bot
23
23
  token = hconfig(ConfigEnum.telegram_bot_token)
@@ -27,9 +27,12 @@ def register_bot(set_hook=False):
27
27
  bot.username = bot.get_me().username
28
28
  except BaseException:
29
29
  pass
30
- # bot.remove_webhook()
31
- # time.sleep(0.1)
32
- domain = get_panel_domains()[0].domain
30
+ if remove_hook:
31
+ bot.remove_webhook()
32
+ domain = Domain.get_panel_link()
33
+ if not domain:
34
+ raise Exception('Cannot get valid domain for setting telegram bot webhook')
35
+
33
36
  admin_proxy_path = hconfig(ConfigEnum.proxy_path_admin)
34
37
 
35
38
  user_secret = AdminUser.get_super_admin_uuid()
@@ -1,7 +1,6 @@
1
- from apiflask import APIBlueprint, Schema
2
- from apiflask.fields import Integer, String
1
+ from apiflask import APIBlueprint
3
2
  from flask import g
4
- from hiddifypanel.models import AdminUser
3
+ from hiddifypanel.models import AdminUser, User
5
4
 
6
5
  bp = APIBlueprint("api_admin", __name__, url_prefix="/<proxy_path>/api/v2/admin/", enable_openapi=True)
7
6
 
@@ -14,21 +13,26 @@ def init_app(app):
14
13
  from .admin_user_api import AdminUserApi
15
14
  from .admin_users_api import AdminUsersApi
16
15
  from .admin_log_api import AdminLogApi
17
- bp.add_url_rule('/me/', view_func=AdminInfoApi)
18
- bp.add_url_rule('/server_status/', view_func=AdminServerStatusApi)
19
- bp.add_url_rule('/admin_user/<uuid:uuid>/', view_func=AdminUserApi)
20
- bp.add_url_rule('/admin_user/', view_func=AdminUsersApi)
21
- bp.add_url_rule('/log/', view_func=AdminLogApi)
16
+ bp.add_url_rule('/me/', view_func=AdminInfoApi) # type: ignore
17
+ bp.add_url_rule('/server_status/', view_func=AdminServerStatusApi) # type: ignore
18
+ bp.add_url_rule('/admin_user/<uuid:uuid>/', view_func=AdminUserApi) # type: ignore
19
+ bp.add_url_rule('/admin_user/', view_func=AdminUsersApi) # type: ignore
20
+ bp.add_url_rule('/log/', view_func=AdminLogApi) # type: ignore
22
21
 
23
22
  from .user_api import UserApi
24
23
  from .users_api import UsersApi
25
- bp.add_url_rule('/user/<uuid:uuid>/', view_func=UserApi)
26
- bp.add_url_rule('/user/', view_func=UsersApi)
24
+ bp.add_url_rule('/user/<uuid:uuid>/', view_func=UserApi) # type: ignore
25
+ bp.add_url_rule('/user/', view_func=UsersApi) # type: ignore
27
26
  app.register_blueprint(bp)
28
27
 
29
28
 
30
29
  def has_permission(model) -> bool:
31
30
  '''Check if the authenticated account has permission to do an action(get,insert,update,delete) on the another admin'''
32
- if not g.account.uuid != AdminUser.get_super_admin_uuid() and model.added_by != g.account.id: # type: ignore
33
- return False
34
- return True
31
+ if g.account.uuid == AdminUser.get_super_admin_uuid():
32
+ return True
33
+ if isinstance(model, AdminUser) and model.parent_admin_id == g.account.id:
34
+ return True
35
+ elif isinstance(model, User) and model.added_by == g.account.id:
36
+ return True
37
+
38
+ return False
@@ -3,7 +3,7 @@ from flask import g
3
3
  from flask.views import MethodView
4
4
  from apiflask import abort
5
5
  from hiddifypanel.auth import login_required
6
- from hiddifypanel.models.admin import AdminUser
6
+ from hiddifypanel.models.admin import AdminMode, AdminUser
7
7
  from hiddifypanel.models.config_enum import ConfigEnum, Lang
8
8
  from hiddifypanel.models.config import hconfig
9
9
  from hiddifypanel.models.role import Role
@@ -15,7 +15,6 @@ class AdminInfoApi(MethodView):
15
15
 
16
16
  @app.output(AdminSchema) # type: ignore
17
17
  def get(self):
18
- # admin = AdminUser.by_uuid(g.account.uuid) or abort(404, "user not found")
19
18
  admin = g.account or abort(404, "user not found")
20
19
 
21
20
  dto = AdminSchema()
@@ -24,7 +23,9 @@ class AdminInfoApi(MethodView):
24
23
  dto.uuid = admin.uuid # type: ignore
25
24
  dto.mode = admin.mode # type: ignore
26
25
  dto.can_add_admin = admin.can_add_admin # type: ignore
27
- dto.parent_admin_uuid = AdminUser.query.filter(AdminUser.id == admin.parent_admin_id).first().uuid or 'None' # type: ignore
26
+ if g.account.mode == AdminMode.super_admin:
27
+ if parent := AdminUser.by_id(admin.parent_admin_id):
28
+ dto.parent_admin_uuid = parent.uuid
28
29
  dto.telegram_id = admin.telegram_id or 0 # type: ignore
29
30
  dto.lang = Lang(hconfig(ConfigEnum.admin_lang)) # type: ignore
30
31
  return dto
@@ -6,7 +6,7 @@ from hiddifypanel.auth import login_required
6
6
  from hiddifypanel.models import *
7
7
 
8
8
  from . import has_permission
9
- from .schema import AdminSchema, PatchAdminSchema, SuccessfulSchema
9
+ from .schema import AdminSchema, PutAdminSchema, PatchAdminSchema, SuccessfulSchema
10
10
 
11
11
 
12
12
  class AdminUserApi(MethodView):
@@ -14,28 +14,46 @@ class AdminUserApi(MethodView):
14
14
 
15
15
  @app.output(AdminSchema) # type: ignore
16
16
  def get(self, uuid):
17
- admin = AdminUser.by_uuid(uuid) or abort(404, "admin not found")
17
+ admin = AdminUser.by_uuid(uuid) or abort(404, "Admin not found")
18
18
  if not has_permission(admin):
19
- abort(403, "You don't have permission to access this admin")
20
- return admin.to_dict() # type: ignore
19
+ abort(403, "you don't have permission to access this admin")
20
+ return admin.to_schema() # type: ignore
21
+
22
+ @app.input(PutAdminSchema, arg_name='data') # type: ignore
23
+ @app.output(SuccessfulSchema) # type: ignore
24
+ def put(self, uuid, data):
25
+ if AdminUser.by_uuid(uuid):
26
+ abort(400, "The admin exists")
27
+ data['uuid'] = uuid
28
+
29
+ if not data.get('added_by_uuid'):
30
+ data['added_by_uuid'] = g.account.uuid
31
+
32
+ _ = AdminUser.add_or_update(**data) or abort(502, "Unknown issue: Admin is not added")
33
+ return {'status': 200, 'msg': 'ok'}
21
34
 
22
35
  @app.input(PatchAdminSchema, arg_name='data') # type: ignore
23
36
  @app.output(SuccessfulSchema) # type: ignore
24
37
  def patch(self, uuid, data):
25
- admin = AdminUser.by_uuid(uuid) or abort(404, "admin not found")
38
+ admin = AdminUser.by_uuid(uuid) or abort(404, "Admin not found")
26
39
  if not has_permission(admin):
27
40
  abort(403, "You don't have permission to access this admin")
28
41
 
29
- data['uuid'] = uuid
30
- if not data.get('added_by_uuid'):
31
- data['added_by_uuid'] = g.account.uuid
42
+ for field in AdminUser.__table__.columns.keys(): # type: ignore
43
+ if field in ['id', 'parent_admin_id']:
44
+ continue
45
+ if field not in data:
46
+ data[field] = getattr(admin, field)
32
47
 
33
- AdminUser.add_or_update(**data)
48
+ _ = AdminUser.add_or_update(True, **data) or abort(502, "Unknown issue: Admin is not patched")
49
+ # the add_or_update doesn't update the uuid of AdminUser, so for now just delete old admin after adding new
50
+ if admin.uuid != data['uuid']:
51
+ admin.remove()
34
52
  return {'status': 200, 'msg': 'ok'}
35
53
 
36
54
  @app.output(SuccessfulSchema) # type: ignore
37
55
  def delete(self, uuid):
38
- admin = AdminUser.by_uuid(uuid) or abort(404, "admin not found")
56
+ admin = AdminUser.by_uuid(uuid) or abort(404, "Admin not found")
39
57
  if not has_permission(admin):
40
58
  abort(403, "You don't have permission to access this admin")
41
59
  admin.remove() # type: ignore
@@ -4,7 +4,7 @@ from apiflask import abort
4
4
  from flask.views import MethodView
5
5
  from hiddifypanel.auth import login_required
6
6
  from hiddifypanel.models.role import Role
7
- from .admin_user_api import AdminSchema, has_permission
7
+ from .admin_user_api import AdminSchema
8
8
  from hiddifypanel.models import AdminUser
9
9
 
10
10
 
@@ -14,21 +14,4 @@ class AdminUsersApi(MethodView):
14
14
  @app.output(AdminSchema(many=True)) # type: ignore
15
15
  def get(self):
16
16
  admins = AdminUser.query.filter(AdminUser.id.in_(g.account.recursive_sub_admins_ids())).all() or abort(404, "You have no admin")
17
- return [admin.to_dict() for admin in admins] # type: ignore
18
-
19
- @app.input(AdminSchema, arg_name='data') # type: ignore
20
- @app.output(AdminSchema) # type: ignore
21
- def put(self, data):
22
- uuid = data.get('uuid') or abort(422, "Parameter issue: 'uuid'")
23
- admin = AdminUser.by_uuid(uuid) # type: ignore
24
-
25
- if not data.get('added_by_uuid'):
26
- data['added_by_uuid'] = g.account.uuid
27
-
28
- # update check permission
29
- if admin:
30
- if not has_permission(admin):
31
- abort(403, "You don't have permission to access this admin")
32
-
33
- dbadmin = AdminUser.add_or_update(**data) or abort(502, "Unknown issue: Admin is not added")
34
- return dbadmin.to_dict() # type: ignore
17
+ return [admin.to_schema() for admin in admins] # type: ignore
@@ -27,7 +27,7 @@ class FriendlyUUID(fields.Field):
27
27
  if value is None:
28
28
  return None
29
29
  try:
30
- return uuid.UUID(value)
30
+ return str(uuid.UUID(value))
31
31
  except ValueError:
32
32
  self.fail('Invalid uuid')
33
33
 
@@ -71,6 +71,11 @@ class UserSchema(Schema):
71
71
  description="The last time the user's data usage was reset, in a JSON-friendly format",
72
72
  allow_none=True
73
73
  )
74
+ # expiry_time = Date(
75
+ # format='%Y-%m-%d',
76
+ # description="The expiry time of the user's package, in a JSON-friendly format",
77
+ # allow_none=True
78
+ # )
74
79
  comment = String(
75
80
  missing=None,
76
81
  allow_none=True,
@@ -115,13 +120,21 @@ class UserSchema(Schema):
115
120
  )
116
121
 
117
122
  lang = Enum(Lang, required=False, allow_none=True, description="The language of the user")
123
+ enable = Boolean(required=False, description="Whether the user is enabled or not")
124
+
125
+
126
+ class PutUserSchema(UserSchema):
127
+ def __init__(self, *args, **kwargs):
128
+ super().__init__(*args, **kwargs)
129
+ # the uuid is sent in the url path
130
+ self.fields['uuid'].required = False
118
131
 
119
132
 
120
133
  class PatchUserSchema(UserSchema):
121
134
  def __init__(self, *args, **kwargs):
122
135
  super().__init__(*args, **kwargs)
136
+ self.fields['uuid'].required = False
123
137
  self.fields['name'].required = False
124
- pass
125
138
 
126
139
  # endregion
127
140
 
@@ -134,21 +147,31 @@ class AdminSchema(Schema):
134
147
  uuid = FriendlyUUID(required=True, description='The unique identifier for the admin')
135
148
  mode = Enum(AdminMode, required=True, description='The mode for the admin')
136
149
  can_add_admin = Boolean(required=True, description='Whether the admin can add other admins')
137
- parent_admin_uuid = FriendlyUUID(description='The unique identifier for the parent admin', allow_none=True,
150
+ parent_admin_uuid = FriendlyUUID(required=False, description='The unique identifier for the parent admin', allow_none=True,
138
151
  # validate=OneOf([p.uuid for p in AdminUser.query.all()])
139
152
  )
140
153
  telegram_id = Integer(required=False, description='The Telegram ID associated with the admin', allow_none=True)
141
154
  lang = Enum(Lang, required=True)
155
+ max_users = Integer(required=False, description='The maximum number of users allowed', allow_none=True)
156
+ max_active_users = Integer(required=False, description='The maximum number of active users allowed', allow_none=True)
157
+
158
+
159
+ class PutAdminSchema(AdminSchema):
160
+ def __init__(self, *args, **kwargs):
161
+ super().__init__(*args, **kwargs)
162
+ # the uuid is sent in the url path
163
+ self.fields['uuid'].required = False
142
164
 
143
165
 
144
166
  class PatchAdminSchema(AdminSchema):
145
167
  def __init__(self, *args, **kwargs):
146
168
  super().__init__(*args, **kwargs)
169
+ self.fields['uuid'].required = False
147
170
  self.fields['name'].required = False
148
171
  self.fields['mode'].required = False
149
172
  self.fields['lang'].required = False
150
173
  self.fields['can_add_admin'].required = False
151
- pass
174
+
152
175
  # endregion
153
176
 
154
177
 
@@ -9,7 +9,7 @@ from hiddifypanel.drivers import user_driver
9
9
  from hiddifypanel.panel import hiddify
10
10
 
11
11
  from . import has_permission
12
- from .schema import UserSchema, PatchUserSchema, SuccessfulSchema
12
+ from .schema import UserSchema, PutUserSchema, PatchUserSchema, SuccessfulSchema
13
13
 
14
14
 
15
15
  class UserApi(MethodView):
@@ -17,11 +17,26 @@ class UserApi(MethodView):
17
17
 
18
18
  @app.output(UserSchema) # type: ignore
19
19
  def get(self, uuid):
20
- user = User.by_uuid(uuid) or abort(404, "user not found")
20
+ user = User.by_uuid(uuid) or abort(404, "User not found")
21
21
  if not has_permission(user):
22
22
  abort(403, "You don't have permission to access this user")
23
23
 
24
- return user.to_dict(False) # type: ignore
24
+ return user.to_schema() # type: ignore
25
+
26
+ @app.input(PutUserSchema, arg_name="data") # type: ignore
27
+ @app.output(SuccessfulSchema) # type: ignore
28
+ def put(self, uuid, data):
29
+ if User.by_uuid(uuid):
30
+ abort(400, 'The user exists')
31
+ data['uuid'] = uuid
32
+
33
+ if not data.get('added_by_uuid'):
34
+ data['added_by_uuid'] = g.account.uuid
35
+
36
+ dbuser = User.add_or_update(**data) or abort(502, "Unknown issue: User is not added")
37
+ user_driver.add_client(dbuser)
38
+ hiddify.quick_apply_users()
39
+ return {'status': 200, 'msg': 'ok'}
25
40
 
26
41
  @app.input(PatchUserSchema, arg_name="data") # type: ignore
27
42
  @app.output(SuccessfulSchema) # type: ignore
@@ -30,13 +45,17 @@ class UserApi(MethodView):
30
45
  if not has_permission(user):
31
46
  abort(403, "You don't have permission to access this user")
32
47
 
33
- data['uuid'] = uuid
34
- if not data.get('added_by_uuid'):
35
- data['added_by_uuid'] = g.account.uuid
48
+ for field in User.__table__.columns.keys(): # type: ignore
49
+ if field in ['id', 'expiry_time']:
50
+ continue
51
+ if field not in data:
52
+ data[field] = getattr(user, field)
36
53
 
37
- User.add_or_update(**data) # type: ignore
38
- user = User.by_uuid(uuid) or abort(502, "unknown issue! user is not added")
39
- user_driver.add_client(user)
54
+ dbuser = User.add_or_update(**data) or abort(502, "Unknown issue! User is not patched")
55
+ user_driver.add_client(dbuser)
56
+ # the add_or_update doesn't update the uuid of User, so for now just delete old user after adding new
57
+ if user.uuid != data['uuid']:
58
+ user.remove()
40
59
  hiddify.quick_apply_users()
41
60
  return {'status': 200, 'msg': 'ok'}
42
61
 
@@ -16,24 +16,4 @@ class UsersApi(MethodView):
16
16
  @app.output(UserSchema(many=True)) # type: ignore
17
17
  def get(self):
18
18
  users = User.query.filter(User.added_by.in_(g.account.recursive_sub_admins_ids())).all() or abort(404, "You have no user")
19
- return [user.to_dict(False) for user in users] # type: ignore
20
-
21
- @app.input(UserSchema, arg_name="data") # type: ignore
22
- @app.output(UserSchema) # type: ignore
23
- def put(self, data):
24
- uuid = data.get('uuid') or abort(422, "Parameter issue: 'uuid'")
25
- user = User.by_uuid(uuid) # type: ignore
26
-
27
- if not data.get('added_by_uuid'):
28
- data['added_by_uuid'] = g.account.uuid
29
-
30
- # update check permission
31
- if user:
32
- if not has_permission(user):
33
- abort(403, "You don't have permission to access this user")
34
- User.add_or_update(**data) # type: ignore
35
-
36
- dbuser = User.by_uuid(data['uuid']) or abort(502, "Unknown issue: User is not added")
37
- user_driver.add_client(dbuser)
38
- hiddify.quick_apply_users()
39
- return dbuser.to_dict(False) # type: ignore
19
+ return [user.to_schema() for user in users] # type: ignore
@@ -41,7 +41,7 @@ class RegisterApi(MethodView):
41
41
  logger.info("Adding users...")
42
42
  User.bulk_register(data['panel_data']['users'], commit=False)
43
43
  logger.info("Adding domains...")
44
- bulk_register_domains(data['panel_data']['domains'], commit=False, force_child_unique_id=child.unique_id)
44
+ Domain.bulk_register(data['panel_data']['domains'], commit=False, force_child_unique_id=child.unique_id)
45
45
  logger.info("Adding hconfigs...")
46
46
  bulk_register_configs(data['panel_data']['hconfigs'], commit=False, froce_child_unique_id=child.unique_id)
47
47
  logger.info("Adding proxies...")
@@ -64,12 +64,16 @@ class UsageInputOutputSchema(Schema):
64
64
 
65
65
  # region sync
66
66
  class SyncInputSchema(Schema):
67
+ domains = fields.List(fields.Nested(DomainSchema), required=False, description="The list of domains")
68
+ proxies = fields.List(fields.Nested(ProxySchema), required=False, description="The list of proxies")
69
+ hconfigs = fields.List(fields.Nested(HConfigSchema), required=False, description="The list of configs")
67
70
  # users = fields.List(fields.Nested(UserSchema),required=True,description="The list of users")
68
- domains = fields.List(fields.Nested(DomainSchema), required=True, description="The list of domains")
69
- proxies = fields.List(fields.Nested(ProxySchema), required=True, description="The list of proxies")
70
- # parent_domains = fields.List(fields.Nested(ParentDomainSchema),required=True,description="The list of parent domains")
71
71
  # admin_users = fields.List(fields.Nested(AdminSchema),required=True,description="The list of admin users")
72
- hconfigs = fields.List(fields.Nested(HConfigSchema), required=True, description="The list of configs")
72
+
73
+ def validate(self, data, **kwargs):
74
+ if not (data.get("domains") or data.get("proxies") or data.get("hconfigs")):
75
+ raise ValidationError("At least one field must exist (domains, proxies, or hconfigs)")
76
+ return data
73
77
 
74
78
 
75
79
  class SyncOutputSchema(Schema):
@@ -34,11 +34,27 @@ class SyncApi(MethodView):
34
34
 
35
35
  try:
36
36
  logger.info("Syncing domains...")
37
- bulk_register_domains(data['domains'], commit=False, force_child_unique_id=child.unique_id)
37
+ if data.get('domains'):
38
+ logger.info("Inserting domains into database")
39
+ Domain.bulk_register(data['domains'], commit=False, force_child_unique_id=child.unique_id)
40
+ else:
41
+ logger.info("Domains field is empty")
42
+
38
43
  logger.info("Syncing hconfigs...")
39
- bulk_register_configs(data['hconfigs'], commit=False, froce_child_unique_id=child.unique_id)
44
+ if data.get('hconfigs'):
45
+ logger.info("Inserting hconfigs into database")
46
+ bulk_register_configs(data['hconfigs'], commit=False, froce_child_unique_id=child.unique_id)
47
+ else:
48
+ logger.info("Hconfigs field is empty")
49
+
40
50
  logger.info("Syncing proxies...")
41
- Proxy.bulk_register(data['proxies'], commit=False, force_child_unique_id=child.unique_id)
51
+ if data.get('proxies'):
52
+ logger.info("Inserting proxies into database")
53
+ Proxy.bulk_register(data['proxies'], commit=False, force_child_unique_id=child.unique_id)
54
+ else:
55
+ logger.info("Proxies field is empty")
56
+
57
+ logger.info("Commit changes to database")
42
58
  db.session.commit() # type: ignore
43
59
  except Exception as err:
44
60
  with logger.contextualize(error=err):