hiddifypanel 10.30.6.dev0__py3-none-any.whl → 10.30.8.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 (48) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +2 -2
  3. hiddifypanel/auth.py +16 -0
  4. hiddifypanel/base.py +8 -1
  5. hiddifypanel/drivers/singbox_api.py +2 -0
  6. hiddifypanel/drivers/ssh_liberty_bridge_api.py +10 -0
  7. hiddifypanel/drivers/user_driver.py +5 -2
  8. hiddifypanel/drivers/wireguard_api.py +34 -44
  9. hiddifypanel/drivers/xray_api.py +18 -4
  10. hiddifypanel/hutils/auth.py +1 -1
  11. hiddifypanel/hutils/proxy/xray.py +1 -1
  12. hiddifypanel/hutils/proxy/xrayjson.py +20 -22
  13. hiddifypanel/models/admin.py +14 -8
  14. hiddifypanel/models/base_account.py +14 -6
  15. hiddifypanel/models/user.py +38 -25
  16. hiddifypanel/panel/admin/DomainAdmin.py +1 -1
  17. hiddifypanel/panel/admin/UserAdmin.py +20 -11
  18. hiddifypanel/panel/cli.py +1 -27
  19. hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +3 -1
  20. hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +1 -0
  21. hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py +1 -0
  22. hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +9 -20
  23. hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +14 -0
  24. hiddifypanel/panel/commercial/restapi/v2/admin/schema.py +19 -14
  25. hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +1 -0
  26. hiddifypanel/panel/commercial/restapi/v2/admin/system_actions.py +27 -0
  27. hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +12 -22
  28. hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +19 -1
  29. hiddifypanel/panel/commercial/restapi/v2/child/__init__.py +1 -1
  30. hiddifypanel/panel/commercial/restapi/v2/panel/ping_pong.py +12 -0
  31. hiddifypanel/panel/commercial/restapi/v2/panel/schema.py +5 -0
  32. hiddifypanel/panel/commercial/restapi/v2/parent/__init__.py +1 -1
  33. hiddifypanel/panel/commercial/restapi/v2/user/__init__.py +1 -1
  34. hiddifypanel/panel/hiddify.py +31 -0
  35. hiddifypanel/panel/init_db.py +1 -1
  36. hiddifypanel/panel/usage.py +27 -16
  37. hiddifypanel/templates/fake.html +316 -0
  38. hiddifypanel/translations.i18n/en.json +74 -33
  39. hiddifypanel/translations.i18n/fa.json +80 -118
  40. hiddifypanel/translations.i18n/pt.json +68 -315
  41. hiddifypanel/translations.i18n/ru.json +73 -168
  42. hiddifypanel/translations.i18n/zh.json +68 -296
  43. {hiddifypanel-10.30.6.dev0.dist-info → hiddifypanel-10.30.8.dev0.dist-info}/METADATA +1 -1
  44. {hiddifypanel-10.30.6.dev0.dist-info → hiddifypanel-10.30.8.dev0.dist-info}/RECORD +48 -47
  45. {hiddifypanel-10.30.6.dev0.dist-info → hiddifypanel-10.30.8.dev0.dist-info}/WHEEL +1 -1
  46. {hiddifypanel-10.30.6.dev0.dist-info → hiddifypanel-10.30.8.dev0.dist-info}/LICENSE.md +0 -0
  47. {hiddifypanel-10.30.6.dev0.dist-info → hiddifypanel-10.30.8.dev0.dist-info}/entry_points.txt +0 -0
  48. {hiddifypanel-10.30.6.dev0.dist-info → hiddifypanel-10.30.8.dev0.dist-info}/top_level.txt +0 -0
hiddifypanel/VERSION CHANGED
@@ -1 +1 @@
1
- 10.30.6.dev0
1
+ 10.30.8.dev0
hiddifypanel/VERSION.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__='10.30.6.dev0'
1
+ __version__='10.30.8.dev0'
2
2
  from datetime import datetime
3
- __release_date__= datetime.strptime('2024-07-05','%Y-%m-%d')
3
+ __release_date__= datetime.strptime('2024-07-10','%Y-%m-%d')
hiddifypanel/auth.py CHANGED
@@ -98,8 +98,24 @@ def login_user(user: AdminUser | User, remember=False, duration=None, force=Fals
98
98
 
99
99
 
100
100
  def login_required(roles: set[Role] | None = None, node_auth: bool = False):
101
+
102
+ def decorator(func):
103
+ from flask import has_app_context, current_app
104
+ # Conditionally apply x if has_app_context() is true
105
+ if has_app_context():
106
+ func = current_app.doc(security='Hiddify-API-Key')(func)
107
+
108
+ # Always apply y
109
+ func = login_required2(roles, node_auth)(func)
110
+ return func
111
+ return decorator
112
+
113
+
114
+ def login_required2(roles: set[Role] | None = None, node_auth: bool = False):
101
115
  '''When both roles and node_auth is set, means authentication can be done by either uuid or unique_id'''
116
+
102
117
  def wrapper(fn):
118
+
103
119
  @wraps(fn)
104
120
  def decorated_view(*args, **kwargs):
105
121
  # print('xxxx', current_account)
hiddifypanel/base.py CHANGED
@@ -37,7 +37,7 @@ def init_logger(app, cli):
37
37
 
38
38
  def create_app(*args, cli=False, **config):
39
39
 
40
- app = APIFlask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True, version='2.0.0', title="Hiddify API",
40
+ app = APIFlask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True, version='2.2.0', title="Hiddify API",
41
41
  openapi_blueprint_url_prefix="/<proxy_path>/api", docs_ui='elements', json_errors=False, enable_openapi=not cli)
42
42
  # app = Flask(__name__, static_url_path="/<proxy_path>/static/", instance_relative_config=True)
43
43
 
@@ -72,6 +72,13 @@ def create_app(*args, cli=False, **config):
72
72
  app.config['SESSION_REDIS'] = redis_client
73
73
  app.config['SESSION_PERMANENT'] = True
74
74
  app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=10)
75
+ app.security_schemes = { # equals to use config SECURITY_SCHEMES
76
+ 'Hiddify-API-Key': {
77
+ 'type': 'apiKey',
78
+ 'in': 'header',
79
+ 'name': 'Hiddify-API-Key',
80
+ }
81
+ }
75
82
  Session(app)
76
83
 
77
84
  app.jinja_env.line_statement_prefix = '%'
@@ -4,6 +4,7 @@ from .abstract_driver import DriverABS
4
4
  from flask import current_app
5
5
  import json
6
6
  from collections import defaultdict
7
+ from hiddifypanel.cache import cache
7
8
 
8
9
 
9
10
  class SingboxApi(DriverABS):
@@ -18,6 +19,7 @@ class SingboxApi(DriverABS):
18
19
  json_data = json.load(f)
19
20
  return {u.split("@")[0]: 1 for u in json_data['experimental']['v2ray_api']['stats']['users']}
20
21
 
22
+ @cache.cache(ttl=300)
21
23
  def get_inbound_tags(self):
22
24
  try:
23
25
  xray_client = self.get_singbox_client()
@@ -29,11 +29,21 @@ class SSHLibertyBridgeApi(DriverABS):
29
29
 
30
30
  def remove_client(self, user):
31
31
  redis_client = self.get_ssh_redis_client()
32
+ if user.ed25519_public_key is None:
33
+ members = redis_client.smembers(USERS_SET)
34
+ for member in members:
35
+ if member.startswith(user.uuid):
36
+ redis_client.srem(USERS_SET, member)
37
+
32
38
  redis_client.srem(USERS_SET, f'{user.uuid}::{user.ed25519_public_key}')
33
39
  redis_client.hdel(USERS_USAGE, f'{user.uuid}')
34
40
  redis_client.save()
35
41
 
36
42
  def get_all_usage(self, users):
43
+ redis_client = self.get_ssh_redis_client()
44
+ allusage = redis_client.hgetall(USERS_USAGE)
45
+ redis_client.delete(USERS_USAGE)
46
+ return {u: int(allusage.get(u.uuid) or 0) for u in users}
37
47
  return {u: self.get_usage_imp(u.uuid) for u in users}
38
48
 
39
49
  def get_usage_imp(self, client_uuid: str, reset: bool = True) -> int:
@@ -33,16 +33,19 @@ def get_enabled_users():
33
33
  for driver in enabled_drivers():
34
34
  try:
35
35
  for u, v in driver.get_enabled_users().items():
36
+ # print(u, "enabled", v, driver)
36
37
  if not v:
37
38
  continue
38
39
  d[u] += 1
39
40
  total += 1
40
41
  except Exception as e:
42
+ print(driver)
41
43
  hiddify.error(f'ERROR! {driver.__class__.__name__} has error in get_enabled users')
42
-
44
+ # print(d, total)
43
45
  res = defaultdict(bool)
44
46
  for u, v in d.items():
45
- res[u] = v >= total # ignore singbox
47
+ # res[u] = v >= total # ignore singbox
48
+ res[u] = v >= 1
46
49
  return res
47
50
 
48
51
 
@@ -4,40 +4,25 @@ import os
4
4
  from .abstract_driver import DriverABS
5
5
  from hiddifypanel.models import User, hconfig, ConfigEnum
6
6
  from hiddifypanel.panel.run_commander import Command, commander
7
+ import redis
8
+
9
+
10
+ USERS_USAGE = "wg:users-usage"
7
11
 
8
12
 
9
13
  class WireguardApi(DriverABS):
14
+ def get_redis_client(self):
15
+ if not hasattr(self, 'redis_client'):
16
+ self.redis_client = redis.from_url('unix:///opt/hiddify-manager/other/redis/run.sock?db=1')
17
+
18
+ return self.redis_client
19
+
10
20
  def is_enabled(self) -> bool:
11
21
  return hconfig(ConfigEnum.wireguard_enable)
12
-
13
- WG_LOCAL_USAGE_FILE_PATH = os.path.join('/opt/hiddify-manager/','hiddify-panel','wireguard_usages.json')
14
- OLD_WG_LOCAL_USAGE_FILE_PATH = os.path.join('/opt/hiddify-manager/','hiddify-panel','hiddify_usages.json')
15
22
 
16
23
  def __init__(self) -> None:
17
24
  super().__init__()
18
25
 
19
- if os.path.isfile(WireguardApi.OLD_WG_LOCAL_USAGE_FILE_PATH) and not os.path.isfile(WireguardApi.WG_LOCAL_USAGE_FILE_PATH):
20
- os.rename(WireguardApi.OLD_WG_LOCAL_USAGE_FILE_PATH,WireguardApi.WG_LOCAL_USAGE_FILE_PATH)
21
-
22
- if not self.is_usages_file_exists_and_json():
23
- self.init_empty_usages_file()
24
- # create empty local usage file
25
-
26
- def is_usages_file_exists_and_json(self) -> bool:
27
- if os.path.isfile(WireguardApi.WG_LOCAL_USAGE_FILE_PATH):
28
- try:
29
- # try to load it as a JSON
30
- self.__get_local_usage()
31
- return True
32
- except json.decoder.JSONDecodeError:
33
- os.remove(WireguardApi.WG_LOCAL_USAGE_FILE_PATH)
34
- return False
35
- return False
36
- def init_empty_usages_file(self):
37
- with open(WireguardApi.WG_LOCAL_USAGE_FILE_PATH, 'w+') as f:
38
- json.dump({}, f)
39
-
40
-
41
26
  def __get_wg_usages(self) -> dict:
42
27
  raw_output = commander(Command.update_wg_usage, run_in_background=False)
43
28
  data = {}
@@ -54,11 +39,13 @@ class WireguardApi(DriverABS):
54
39
  return data
55
40
 
56
41
  def __get_local_usage(self) -> dict:
57
- with open(WireguardApi.WG_LOCAL_USAGE_FILE_PATH, 'r') as f:
58
- data = json.load(f)
59
- return data
42
+ usage_data = self.get_redis_client() .get(USERS_USAGE)
43
+ if usage_data:
44
+ return json.loads(usage_data)
45
+
46
+ return {}
60
47
 
61
- def __sync_local_usages(self) -> dict:
48
+ def __sync_local_usages(self, users) -> dict:
62
49
  local_usage = self.__get_local_usage()
63
50
  wg_usage = self.__get_wg_usages()
64
51
  res = {}
@@ -66,16 +53,18 @@ class WireguardApi(DriverABS):
66
53
  for local_wg_pub in local_usage.copy().keys():
67
54
  if local_wg_pub not in wg_usage:
68
55
  del local_usage[local_wg_pub]
69
-
56
+ uuid_map = {u.wg_pub: u for u in users}
70
57
  for wg_pub, wg_usage in wg_usage.items():
58
+ user = uuid_map.get(wg_pub)
59
+ uuid = user.uuid if user else None
71
60
  if not local_usage.get(wg_pub):
72
- local_usage[wg_pub] = wg_usage
61
+ local_usage[wg_pub] = {"uuid": uuid, "usage": wg_usage}
73
62
  continue
74
- res[wg_pub] = self.calculate_reset(local_usage[wg_pub], wg_usage)
75
- local_usage[wg_pub] = wg_usage
63
+ res[wg_pub] = self.calculate_reset(local_usage[wg_pub]['usage'], wg_usage)
64
+ local_usage[wg_pub] = {"uuid": uuid, "usage": wg_usage}
65
+
66
+ self.get_redis_client().set(USERS_USAGE, json.dumps(local_usage))
76
67
 
77
- with open(WireguardApi.WG_LOCAL_USAGE_FILE_PATH, 'w') as f:
78
- json.dump(local_usage, f)
79
68
  return res
80
69
 
81
70
  def calculate_reset(self, last_usage: dict, current_usage: dict) -> dict:
@@ -94,15 +83,16 @@ class WireguardApi(DriverABS):
94
83
  if not hconfig(ConfigEnum.wireguard_enable):
95
84
  return {}
96
85
  usages = self.__get_wg_usages()
97
- wg_pubs = set(usages.keys())
98
-
99
- users = User.query.all()
100
- enabled = {}
101
- for u in users:
102
- if u.wg_pub in wg_pubs:
86
+ new_wg_pubs = set(usages.keys())
87
+ old_usages = self.__get_local_usage()
88
+ old_wg_pubs = set(old_usages.keys())
89
+ enabled = {u['uuid']: 1 for u in old_usages.values()}
90
+ not_included = new_wg_pubs - old_wg_pubs
91
+ if not_included:
92
+ users = User.query.filter(User.wg_pub.in_(not_included).all())
93
+ for u in users:
103
94
  enabled[u.uuid] = 1
104
- else:
105
- enabled[u.uuid] = 0
95
+
106
96
  return enabled
107
97
 
108
98
  def add_client(self, user):
@@ -114,7 +104,7 @@ class WireguardApi(DriverABS):
114
104
  def get_all_usage(self, users, reset=True):
115
105
  if not hconfig(ConfigEnum.wireguard_enable):
116
106
  return {}
117
- all_usages = self.__sync_local_usages()
107
+ all_usages = self.__sync_local_usages(users)
118
108
  res = {}
119
109
  for u in users:
120
110
  if use := all_usages.get(u.wg_pub):
@@ -2,6 +2,7 @@ import xtlsapi
2
2
  from hiddifypanel.models import *
3
3
  from .abstract_driver import DriverABS
4
4
  from collections import defaultdict
5
+ from hiddifypanel.cache import cache
5
6
 
6
7
 
7
8
  class XrayApi(DriverABS):
@@ -19,7 +20,18 @@ class XrayApi(DriverABS):
19
20
  if "user>>>" not in use.name:
20
21
  continue
21
22
  uuid = use.name.split(">>>")[1].split("@")[0]
22
- res[uuid] = 1
23
+ try:
24
+ t = "xtls"
25
+ protocol = "vless"
26
+ xray_client.add_client(t, f'{uuid}', f'{uuid}@hiddify.com', protocol=protocol, flow='xtls-rprx-vision', alter_id=0, cipher='chacha20_poly1305')
27
+ xray_client.remove_client(t, f'{uuid}@hiddify.com')
28
+ res[uuid] = 0
29
+ except xtlsapi.xtlsapi.exceptions.EmailAlreadyExists as e:
30
+ res[uuid] = 1
31
+ except Exception as e:
32
+ print(f"error {e}")
33
+ res[uuid] = 0
34
+
23
35
  return res
24
36
 
25
37
  # xray_client = self.get_xray_client()
@@ -40,15 +52,16 @@ class XrayApi(DriverABS):
40
52
  # enabled[uuid] = e
41
53
  # return enabled
42
54
 
55
+ # @cache.cache(ttl=300)
43
56
  def get_inbound_tags(self):
44
57
  try:
45
58
  xray_client = self.get_xray_client()
46
- inbounds = [inb.name.split(">>>")[1] for inb in xray_client.stats_query('inbound')]
59
+ inbounds = {inb.name.split(">>>")[1] for inb in xray_client.stats_query('inbound')}
47
60
  print(f"Success in get inbound tags {inbounds}")
48
61
  except Exception as e:
49
62
  print(f"error in get inbound tags {e}")
50
- inbounds = []
51
- return list(set(inbounds))
63
+ inbounds = {}
64
+ return list(inbounds)
52
65
 
53
66
  def add_client(self, user):
54
67
  uuid = user.uuid
@@ -65,6 +78,7 @@ class XrayApi(DriverABS):
65
78
  'v2ray': 'shadowsocks',
66
79
  'kcp': 'vless',
67
80
  'dispatcher': 'trojan',
81
+ 'reality': 'vless'
68
82
  }
69
83
 
70
84
  def proto(t):
@@ -6,7 +6,7 @@ from uuid import UUID
6
6
  def is_uuid_valid(uuid: str, version: int = 4) -> bool:
7
7
  try:
8
8
  uuid_obj = UUID(uuid, version=version)
9
- except ValueError:
9
+ except Exception:
10
10
  return False
11
11
  return str(uuid_obj) == uuid
12
12
 
@@ -82,7 +82,7 @@ def to_link(proxy: dict) -> str | dict:
82
82
  return "ShadowTLS is Not Supported for this platform"
83
83
  # return f'{baseurl}?plugin=v2ray-plugin&path={proxy["proxy_path"]}&host={proxy["fakedomain"]}&udp-over-tcp=true#{name_link}'
84
84
  if proxy['proto'] == 'v2ray':
85
- return f'{baseurl}?plugin=v2ray-plugin&mode=websocket&path={proxy["proxy_path"]}&host={proxy["fakedomain"]}&tls&udp-over-tcp=true#{name_link}'
85
+ return f'{baseurl}?plugin=v2ray-plugin&mode=websocket&path={proxy["proxy_path"]}&host={proxy["sni"]}&tls&udp-over-tcp=true#{name_link}'
86
86
 
87
87
  if proxy['proto'] == 'tuic':
88
88
  baseurl = f'tuic://{proxy["uuid"]}:{proxy["uuid"]}@{proxy["server"]}:{proxy["port"]}?congestion_control=cubic&udp_relay_mode=native&sni={proxy["sni"]}&alpn=h3'
@@ -8,7 +8,7 @@ from hiddifypanel.models import hconfig, ConfigEnum
8
8
  from .xray import is_muxable_agent, OUTBOUND_LEVEL
9
9
 
10
10
 
11
- def configs_as_json(domains: list[Domain], user: User, expire_days: int, remarks: str) -> str:
11
+ def configs_as_json(domains: list[Domain], user: User, expire_days: int, remarks: str) -> str:
12
12
  '''Returns xray configs as json'''
13
13
  all_configs = []
14
14
 
@@ -35,50 +35,48 @@ def configs_as_json(domains: list[Domain], user: User, expire_days: int, remark
35
35
  )
36
36
  # endregion
37
37
 
38
- # region show status (active/disable)
39
- active = True
40
38
  if not user.is_active:
39
+ # region show status (active/disable)
41
40
  tag = '✖ ' + (hutils.encode.url_encode('بسته شما به پایان رسید') if hconfig(ConfigEnum.lang) == 'fa' else 'Package Ended')
42
41
  # add user status
43
42
  all_configs.append(
44
43
  null_config(tag)
45
44
  )
46
- active = False
47
- # endregion
48
-
49
- if active:
45
+ # endregion
46
+ else:
50
47
  # TODO: seperate codes to small functions
51
48
  # TODO: check what are unsupported protocols in other apps
52
- unsupported_protos = []
53
- unsupported_transport = []
49
+ unsupported_protos = {}
50
+ unsupported_transport = {}
54
51
  if g.user_agent.get('is_v2rayng'):
55
52
  # TODO: ensure which protocols are not supported in v2rayng
56
- unsupported_protos = [ProxyProto.wireguard, ProxyProto.hysteria, ProxyProto.hysteria2, ProxyProto.tuic, ProxyProto.ss, ProxyProto.ssr, ProxyProto.ssh]
53
+ unsupported_protos = {ProxyProto.wireguard, ProxyProto.hysteria, ProxyProto.hysteria2,
54
+ ProxyProto.tuic, ProxyProto.ss, ProxyProto.ssr, ProxyProto.ssh}
57
55
  if not hutils.flask.is_client_version(hutils.flask.ClientVersion.v2ryang, 1, 8, 18):
58
- unsupported_transport = [ProxyTransport.httpupgrade]
56
+ unsupported_transport = {ProxyTransport.httpupgrade}
59
57
 
60
58
  # multiple outbounds needs multiple whole base config not just one with multiple outbounds (at least for v2rayng)
61
59
  # https://github.com/2dust/v2rayNG/pull/2827#issue-2127534078
62
60
  outbounds = []
63
61
  for proxy in hutils.proxy.get_valid_proxies(domains):
64
- if unsupported_protos and proxy['proto'] in unsupported_protos:
62
+ if proxy['proto'] in unsupported_protos:
65
63
  continue
66
- if unsupported_transport and proxy['transport'] in unsupported_transport:
64
+ if proxy['transport'] in unsupported_transport:
67
65
  continue
68
66
  outbound = to_xray(proxy)
69
- if 'msg' not in outbound:
70
- outbounds.append(outbound)
67
+ outbounds.append(outbound)
71
68
 
72
69
  base_config = json.loads(render_template('base_xray_config.json.j2', remarks=remarks))
73
70
  if len(outbounds) > 1:
74
71
  for out in outbounds:
75
- base_config['remarks'] = out['tag']
76
- base_config['outbounds'].insert(0, out)
77
- if all_configs:
78
- all_configs.insert(0, copy.deepcopy(base_config))
79
- else:
80
- all_configs.append(copy.deepcopy(base_config))
81
- del base_config['outbounds'][0]
72
+ base = copy.deepcopy(base_config)
73
+ base['remarks'] = out['tag']
74
+ base['outbounds'].insert(0, out)
75
+ # if all_configs:
76
+ # all_configs.insert(0, copy.deepcopy(base_config))
77
+ # else:
78
+ all_configs.append(base)
79
+
82
80
  else: # single outbound
83
81
  base_config['outbounds'].insert(0, outbounds[0])
84
82
  all_configs = base_config
@@ -1,5 +1,5 @@
1
1
  from enum import auto
2
- import uuid
2
+ from uuid import uuid4
3
3
  from flask import g
4
4
  from hiddifypanel.models.usage import DailyUsage
5
5
  from sqlalchemy import event, Column, Integer, Enum, Boolean, ForeignKey
@@ -84,6 +84,9 @@ class AdminUser(BaseAccount, SerializerMixin):
84
84
  uuid = str(uuid)
85
85
  account = AdminUser.query.filter(AdminUser.uuid == uuid).first()
86
86
  if not account and create:
87
+ from hiddifypanel import hutils
88
+ if not hutils.auth.is_uuid_valid(uuid):
89
+ uuid = str(uuid4())
87
90
  dbuser = AdminUser(uuid=uuid, name="unknown", parent_admin_id=AdminUser.current_admin_or_owner().id)
88
91
  db.session.add(dbuser)
89
92
  db.session.commit()
@@ -103,11 +106,14 @@ class AdminUser(BaseAccount, SerializerMixin):
103
106
  else:
104
107
  parent_admin = cls.by_uuid(parent, create=True)
105
108
  dbuser.parent_admin_id = parent_admin.id # type: ignore
106
-
107
- dbuser.mode = data.get('mode', AdminMode.agent)
108
- dbuser.can_add_admin = data.get('can_add_admin') or False
109
- dbuser.max_users = data.get('max_users', 100)
110
- dbuser.max_active_users = data.get('max_active_users', 100)
109
+ if data.get('mode') is not None:
110
+ dbuser.mode = data.get('mode', AdminMode.agent)
111
+ if data.get('can_add_admin') is not None:
112
+ dbuser.can_add_admin = data['can_add_admin']
113
+ if data.get('max_users') is not None:
114
+ dbuser.max_users = data['max_users']
115
+ if data.get('max_active_users') is not None:
116
+ dbuser.max_active_users = data['max_active_users']
111
117
  if commit:
112
118
  db.session.commit()
113
119
  return dbuser
@@ -158,10 +164,10 @@ class AdminUser(BaseAccount, SerializerMixin):
158
164
  return str(self.name)
159
165
 
160
166
  @staticmethod
161
- def get_super_admin():
167
+ def get_super_admin() -> "AdminUser":
162
168
  admin = AdminUser.by_id(1)
163
169
  if not admin:
164
- db.session.add(AdminUser(id=1, uuid=str(uuid.uuid4()), name="Owner", mode=AdminMode.super_admin, comment=""))
170
+ db.session.add(AdminUser(id=1, uuid=str(uuid4()), name="Owner", mode=AdminMode.super_admin, comment=""))
165
171
  db.session.commit()
166
172
 
167
173
  db_execute("update admin_user set id=1 where name='Owner'", commit=True)
@@ -61,13 +61,21 @@ class BaseAccount(db.Model, SerializerMixin, FlaskLoginUserMixin): # type: igno
61
61
  return cls.query.filter(cls.username == username, cls.password == password).first()
62
62
 
63
63
  @classmethod
64
- def add_or_update(cls, commit: bool = True, **data):
65
- db_account = cls.by_uuid(data['uuid'], create=True)
66
- db_account.name = data.get('name', '')
67
- db_account.comment = data.get('comment', '')
64
+ def add_or_update(cls, commit: bool = True, old_uuid=None, **data):
65
+ db_account: BaseAccount = cls.by_uuid(old_uuid or data.get('uuid'), create=True)
68
66
  from hiddifypanel import hutils
69
- db_account.telegram_id = hutils.convert.to_int(data.get('telegram_id'))
70
- db_account.lang = data.get('lang')
67
+ if hutils.auth.is_uuid_valid(data.get('uuid')):
68
+ db_account.uuid = data['uuid']
69
+
70
+ if data.get('name') is not None:
71
+ db_account.name = data.get('name')
72
+
73
+ if data.get('comment') is not None:
74
+ db_account.comment = data.get('comment')
75
+ if data.get('telegram_id') is not None:
76
+ db_account.telegram_id = hutils.convert.to_int(data.get('telegram_id'))
77
+ if data.get('lang') is not None:
78
+ db_account.lang = data.get('lang')
71
79
  if commit:
72
80
  db.session.commit() # type: ignore
73
81
  return db_account
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  from enum import auto
3
+ from uuid import uuid4
3
4
  from hiddifypanel.models.role import Role
4
5
  from dateutil import relativedelta
5
6
 
@@ -54,7 +55,8 @@ class UserDetail(db.Model, SerializerMixin):
54
55
 
55
56
  @property
56
57
  def devices(self):
57
- return [] if not self.connected_devices else self.connected_devices.split(",")
58
+ return []
59
+ # return [] if not self.connected_devices else self.connected_devices.split(",")
58
60
 
59
61
 
60
62
  class User(BaseAccount, SerializerMixin):
@@ -122,13 +124,14 @@ class User(BaseAccount, SerializerMixin):
122
124
  is_active = False
123
125
  elif self.remaining_days < 0:
124
126
  is_active = False
125
- elif len(self.devices) > max(3, self.max_ips):
126
- is_active = False
127
+ # elif len(self.devices) > max(3, self.max_ips):
128
+ # is_active = False
127
129
  return is_active
128
130
 
129
131
  @property
130
132
  def devices(self):
131
133
  res = {}
134
+ return res
132
135
  for detail in UserDetail.query.filter(UserDetail.user_id == self.id):
133
136
  for device in detail.devices:
134
137
  res[device] = 1
@@ -202,6 +205,10 @@ class User(BaseAccount, SerializerMixin):
202
205
  uuid = str(uuid)
203
206
  account = User.query.filter(User.uuid == uuid).first()
204
207
  if not account and create:
208
+ from hiddifypanel import hutils
209
+ if not hutils.auth.is_uuid_valid(uuid):
210
+ uuid = str(uuid4())
211
+
205
212
  dbuser = User(uuid=uuid, name="unknown", added_by=AdminUser.current_admin_or_owner().id)
206
213
  db.session.add(dbuser)
207
214
  db.session.commit()
@@ -216,11 +223,11 @@ class User(BaseAccount, SerializerMixin):
216
223
  @classmethod
217
224
  def add_or_update(cls, commit: bool = True, **data):
218
225
  from hiddifypanel import hutils
219
- dbuser = super().add_or_update(commit=commit, **data)
226
+ dbuser: User = super().add_or_update(commit=commit, **data)
220
227
  if data.get('added_by_uuid'):
221
228
  admin = AdminUser.by_uuid(data.get('added_by_uuid'), create=True) or AdminUser.current_admin_or_owner() # type: ignore
222
229
  dbuser.added_by = admin.id
223
- else:
230
+ elif not dbuser.added_by:
224
231
  dbuser.added_by = 1
225
232
 
226
233
  # if data.get('expiry_time', ''): #v4
@@ -230,45 +237,50 @@ class User(BaseAccount, SerializerMixin):
230
237
  # dbuser.start_date = last_reset_time
231
238
  # dbuser.package_days = (expiry_time - last_reset_time).days # type: ignore
232
239
  # el
233
- if 'package_days' in data:
240
+ if data.get('package_days') is not None:
234
241
  dbuser.package_days = data['package_days']
235
- if data.get('start_date', ''):
242
+
243
+ if data.get('start_date'):
236
244
  dbuser.start_date = hutils.convert.json_to_date(data['start_date'])
237
- else:
245
+ elif 'start_date' in data and data['start_date'] is None:
238
246
  dbuser.start_date = None
239
247
 
240
248
  if (c_GB := data.get('current_usage_GB')) is not None:
241
249
  dbuser.current_usage_GB = c_GB
242
250
  elif (c := data.get('current_usage')) is not None:
243
251
  dbuser.current_usage = c
244
- else:
252
+ elif dbuser.current_usage is None:
245
253
  dbuser.current_usage = 0
246
254
 
247
255
  if (l_GB := data.get('usage_limit_GB')) is not None:
248
256
  dbuser.usage_limit_GB = l_GB
249
257
  elif (l := data.get('usage_limit')) is not None:
250
258
  dbuser.usage_limit = l
251
- else:
259
+ elif dbuser.usage_limit_GB is None:
252
260
  dbuser.usage_limit_GB = 1000
253
261
 
254
- dbuser.enable = data.get('enable', True)
262
+ if data.get('enable') is not None:
263
+ dbuser.enable = data['enable']
255
264
 
256
- if data.get('ed25519_private_key', ''):
265
+ if data.get('ed25519_private_key', '') and data.get('ed25519_public_key', ''):
257
266
  dbuser.ed25519_private_key = data.get('ed25519_private_key', '')
258
267
  dbuser.ed25519_public_key = data.get('ed25519_public_key', '')
259
-
260
- dbuser.wg_pk = data.get('wg_pk', dbuser.wg_pk)
261
- dbuser.wg_pub = data.get('wg_pub', dbuser.wg_pub)
262
- dbuser.wg_psk = data.get('wg_psk', dbuser.wg_psk)
263
-
264
- mode = data.get('mode', UserMode.no_reset)
265
- if mode == 'disable':
266
- mode = UserMode.no_reset
267
- dbuser.enable = False
268
-
269
- dbuser.mode = mode
270
-
271
- dbuser.last_online = hutils.convert.json_to_time(data.get('last_online')) or datetime.datetime.min
268
+ if data.get('wg_pk') is not None:
269
+ dbuser.wg_pk = data['wg_pk']
270
+ if data.get('wg_pub') is not None:
271
+ dbuser.wg_pub = data['wg_pub']
272
+ if data.get('wg_psk') is not None:
273
+ dbuser.wg_psk = data['wg_psk']
274
+
275
+ if data.get('mode') is not None or dbuser.mode is None:
276
+ mode = data.get('mode', UserMode.no_reset)
277
+ if mode == 'disable':
278
+ mode = UserMode.no_reset
279
+ dbuser.enable = False
280
+ dbuser.mode = mode
281
+
282
+ if data.get('last_online') is not None:
283
+ dbuser.last_online = hutils.convert.json_to_time(data.get('last_online')) or datetime.datetime.min
272
284
  if commit:
273
285
  db.session.commit()
274
286
  return dbuser
@@ -305,6 +317,7 @@ class User(BaseAccount, SerializerMixin):
305
317
  'wg_pk': self.wg_pk,
306
318
  'wg_pub': self.wg_pub,
307
319
  'wg_psk': self.wg_psk,
320
+ 'is_active': self.is_active
308
321
  }
309
322
 
310
323
  # @staticmethod
@@ -114,7 +114,7 @@ class DomainAdmin(AdminLTEModelView):
114
114
  if not dip:
115
115
  dip = hutils.network.resolve_domain_with_api(model.domain)
116
116
  myip = hutils.network.get_ip(4)
117
- if myip == dip and model.mode == DomainType.direct:
117
+ if myip == dip and model.mode in [DomainType.direct, DomainType.sub_link_only]:
118
118
  badge_type = ''
119
119
  elif dip and model.mode != DomainType.direct and myip != dip:
120
120
  badge_type = 'warning'